699 lines
32 KiB
Python
699 lines
32 KiB
Python
# gui.py
|
|
import tkinter as tk
|
|
from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog
|
|
import logging
|
|
import os # Import os for path operations
|
|
|
|
# Import constant from the central location
|
|
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):
|
|
"""Display text in a tooltip window."""
|
|
self.hidetip() # Hide any existing tooltip first
|
|
if not self.widget.winfo_exists(): return # Avoid error if widget destroyed
|
|
try:
|
|
# Get widget position relative to screen
|
|
x, y, _, _ = self.widget.bbox("insert") # Get widget location relative to widget itself
|
|
x += self.widget.winfo_rootx() + 25 # Add screen coordinates and offset
|
|
y += self.widget.winfo_rooty() + 25
|
|
except tk.TclError: # Handle cases where bbox might fail (e.g., widget not visible)
|
|
# Fallback position calculation
|
|
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) # Create new top-level window
|
|
tw.wm_overrideredirect(True) # Remove window decorations (border, title bar)
|
|
tw.wm_geometry(f"+{int(x)}+{int(y)}") # Position the window (ensure integer coordinates)
|
|
label = tk.Label(tw, text=self.text, justify=tk.LEFT,
|
|
background="#ffffe0", relief=tk.SOLID, borderwidth=1, # Light yellow background
|
|
font=("tahoma", "8", "normal"))
|
|
label.pack(ipadx=1)
|
|
|
|
def hidetip(self):
|
|
"""Hide the tooltip window."""
|
|
tw = self.tooltip_window
|
|
self.tooltip_window = None
|
|
if tw:
|
|
try:
|
|
if tw.winfo_exists():
|
|
tw.destroy()
|
|
except tk.TclError: # Handle cases where window might already be destroyed
|
|
pass
|
|
# --- End Tooltip Class ---
|
|
|
|
|
|
# --- Gitignore Editor Window Class ---
|
|
class GitignoreEditorWindow(tk.Toplevel):
|
|
"""
|
|
A Toplevel window for editing the .gitignore file.
|
|
"""
|
|
def __init__(self, master, gitignore_path, logger):
|
|
"""
|
|
Initializes the editor window.
|
|
|
|
Args:
|
|
master (tk.Widget): The parent widget (usually the main app window).
|
|
gitignore_path (str): The full path to the .gitignore file.
|
|
logger (logging.Logger): Logger instance for logging actions.
|
|
"""
|
|
super().__init__(master)
|
|
self.gitignore_path = gitignore_path
|
|
self.logger = logger
|
|
self.original_content = "" # Store original content to check for changes
|
|
|
|
# --- Window Configuration ---
|
|
self.title(f"Edit {os.path.basename(gitignore_path)}")
|
|
self.geometry("600x450") # Set initial size, slightly larger height
|
|
self.minsize(400, 300) # Set minimum size
|
|
# Make window modal (grab focus)
|
|
self.grab_set()
|
|
# Make window appear on top of the master window
|
|
self.transient(master)
|
|
# Handle closing via window manager (X button)
|
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
|
|
# --- Widgets ---
|
|
# Main frame
|
|
main_frame = ttk.Frame(self, padding="10")
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
# Configure grid weights for resizing
|
|
main_frame.rowconfigure(0, weight=1) # Text editor row expands
|
|
main_frame.columnconfigure(0, weight=1) # Text editor column expands
|
|
|
|
# ScrolledText widget for editing
|
|
self.text_editor = scrolledtext.ScrolledText(
|
|
main_frame,
|
|
wrap=tk.WORD,
|
|
font=("Consolas", 10), # Use a suitable font
|
|
undo=True # Enable undo/redo
|
|
)
|
|
self.text_editor.grid(row=0, column=0, sticky="nsew", pady=(0, 10)) # Use grid, expand in all directions
|
|
|
|
# Button frame (using grid within main_frame)
|
|
button_frame = ttk.Frame(main_frame)
|
|
button_frame.grid(row=1, column=0, sticky="ew") # Place below text editor, stretch horizontally
|
|
# Center buttons within the button frame
|
|
button_frame.columnconfigure(0, weight=1) # Make space on the left
|
|
button_frame.columnconfigure(3, weight=1) # Make space on the right
|
|
|
|
# Save button
|
|
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) # Place in middle-right column
|
|
|
|
# Cancel button
|
|
self.cancel_button = ttk.Button(button_frame, text="Cancel", command=self._on_close)
|
|
self.cancel_button.grid(row=0, column=1, padx=5) # Place in middle-left column
|
|
|
|
# --- Load File Content ---
|
|
self._load_file()
|
|
|
|
# Center window relative to parent (call after widgets are created)
|
|
self._center_window(master)
|
|
|
|
# Set focus to the text editor
|
|
self.text_editor.focus_set()
|
|
|
|
|
|
def _center_window(self, parent):
|
|
"""Centers the window relative to its parent."""
|
|
self.update_idletasks() # Ensure window size is calculated
|
|
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()
|
|
|
|
# Calculate position, ensuring it stays within screen bounds (basic check)
|
|
x_pos = parent_x + (parent_width // 2) - (win_width // 2)
|
|
y_pos = parent_y + (parent_height // 2) - (win_height // 2)
|
|
|
|
# Adjust if going off-screen (simple version)
|
|
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):
|
|
"""Loads the content of the .gitignore file into the editor."""
|
|
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()
|
|
self.text_editor.delete("1.0", tk.END) # Clear previous content
|
|
self.text_editor.insert(tk.END, self.original_content)
|
|
self.text_editor.edit_reset() # Reset undo stack after loading
|
|
self.logger.debug(".gitignore content loaded successfully.")
|
|
else:
|
|
self.logger.info(f"'{self.gitignore_path}' does not exist. Editor is empty.")
|
|
self.original_content = ""
|
|
self.text_editor.delete("1.0", tk.END)
|
|
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 the .gitignore 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"An unexpected error occurred while loading the file:\n{e}",
|
|
parent=self)
|
|
|
|
|
|
def _save_file(self):
|
|
"""Saves the current content of the editor to the .gitignore file."""
|
|
# Get content, ensure it ends with a single newline if not empty
|
|
current_content = self.text_editor.get("1.0", tk.END).rstrip()
|
|
if current_content:
|
|
current_content += "\n"
|
|
|
|
# Normalize original content similarly for comparison
|
|
normalized_original = self.original_content.rstrip()
|
|
if normalized_original:
|
|
normalized_original += "\n"
|
|
|
|
if current_content == normalized_original:
|
|
self.logger.info("No changes detected in .gitignore content. Skipping save.")
|
|
return True # Indicate success (no save needed)
|
|
|
|
self.logger.info(f"Saving changes to: {self.gitignore_path}")
|
|
try:
|
|
# Ensure directory exists before writing (though unlikely needed for .gitignore)
|
|
# os.makedirs(os.path.dirname(self.gitignore_path), exist_ok=True)
|
|
with open(self.gitignore_path, 'w', encoding='utf-8', newline='\n') as f: # Use newline='\n' for consistency
|
|
f.write(current_content)
|
|
self.logger.info(".gitignore file saved successfully.")
|
|
self.original_content = current_content # Update original content after save
|
|
self.text_editor.edit_reset() # Reset undo stack after saving
|
|
return True # Indicate success
|
|
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 the .gitignore file:\n{e}",
|
|
parent=self)
|
|
return False # Indicate failure
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected error saving {self.gitignore_path}: {e}")
|
|
messagebox.showerror("Unexpected Error",
|
|
f"An unexpected error occurred while saving the file:\n{e}",
|
|
parent=self)
|
|
return False
|
|
|
|
|
|
def _save_and_close(self):
|
|
"""Saves the file and closes the window if save is successful."""
|
|
if self._save_file():
|
|
self.destroy() # Close window only if save succeeded or no changes
|
|
|
|
|
|
def _on_close(self):
|
|
"""Handles closing the window (Cancel button or X button)."""
|
|
# Check if content changed
|
|
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:
|
|
# Use askyesnocancel for three options
|
|
response = messagebox.askyesnocancel("Unsaved Changes",
|
|
"You have unsaved changes.\nSave before closing?",
|
|
parent=self)
|
|
if response is True: # Yes, save and close
|
|
self._save_and_close() # This handles save status and closes if successful
|
|
elif response is False: # No, discard and close
|
|
self.logger.warning("Discarding unsaved changes in .gitignore editor.")
|
|
self.destroy()
|
|
# Else (Cancel): Do nothing, keep window open
|
|
else:
|
|
# No changes, just close
|
|
self.destroy()
|
|
|
|
# --- End Gitignore Editor Window ---
|
|
|
|
|
|
class MainFrame(ttk.Frame):
|
|
"""
|
|
The main frame containing all GUI elements for the Git SVN Sync Tool.
|
|
Manages widget creation, layout, and basic GUI state updates.
|
|
Callbacks for actions are provided by the main application controller.
|
|
"""
|
|
# Define constants for colors used in the GUI
|
|
GREEN = "#90EE90" # Light Green for success/prepared status
|
|
RED = "#F08080" # Light Coral for error/unprepared status
|
|
|
|
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): # Added callback
|
|
"""
|
|
Initializes the MainFrame.
|
|
|
|
Args:
|
|
master (tk.Tk or ttk.Frame): The parent widget.
|
|
load_profile_settings_cb (callable): Called when profile selection changes.
|
|
browse_folder_cb (callable): Called by Browse buttons.
|
|
update_svn_status_cb (callable): Called when SVN path might change.
|
|
prepare_svn_for_git_cb (callable): Called by 'Prepare SVN' button.
|
|
create_git_bundle_cb (callable): Called by 'Create Bundle' button.
|
|
fetch_from_git_bundle_cb (callable): Called by 'Fetch Bundle' button.
|
|
config_manager_instance (ConfigManager): Instance to access config data.
|
|
profile_sections_list (list): Initial list of profile names.
|
|
add_profile_cb (callable): Called by 'Add Profile' button.
|
|
remove_profile_cb (callable): Called by 'Remove Profile' button.
|
|
manual_backup_cb (callable): Called by 'Backup Now' button.
|
|
open_gitignore_editor_cb (callable): Called by 'Edit .gitignore' button.
|
|
"""
|
|
super().__init__(master)
|
|
self.master = master
|
|
|
|
# Store callbacks provided by the controller (GitSvnSyncApp)
|
|
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 # Store callback
|
|
|
|
# Store config manager and initial profiles if needed locally
|
|
self.config_manager = config_manager_instance
|
|
self.initial_profile_sections = profile_sections_list
|
|
|
|
# Style configuration
|
|
self.style = ttk.Style()
|
|
self.style.theme_use('clam') # Example theme
|
|
|
|
# Pack the main frame
|
|
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()
|
|
self.commit_message_var = tk.StringVar() # For custom commit message
|
|
self.backup_exclude_extensions_var = tk.StringVar() # For backup exclusions
|
|
|
|
# --- Widget Creation ---
|
|
self._create_profile_frame()
|
|
self._create_repo_frame() # Modified
|
|
self._create_backup_frame()
|
|
self._create_function_frame()
|
|
self._create_log_area()
|
|
|
|
# --- Initial State Configuration ---
|
|
self._initialize_profile_selection()
|
|
self.toggle_backup_dir()
|
|
# Initial status update is handled by the controller after loading profile
|
|
|
|
|
|
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")
|
|
|
|
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", # Prevent typing custom values
|
|
width=35, # Adjust width as needed
|
|
values=self.initial_profile_sections # Set initial values
|
|
)
|
|
self.profile_dropdown.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=5) # EW = stretch horizontally
|
|
# When selection changes, call the controller's load function
|
|
self.profile_dropdown.bind("<<ComboboxSelected>>",
|
|
lambda event: self.load_profile_settings_callback(self.profile_var.get()))
|
|
# Also trace the variable for programmatic changes
|
|
self.profile_var.trace_add("write",
|
|
lambda *args: self.load_profile_settings_callback(self.profile_var.get()))
|
|
|
|
# Profile management buttons
|
|
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=2, sticky=tk.W, padx=(5, 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=3, sticky=tk.W, padx=(2, 5), pady=5)
|
|
|
|
# Allow the dropdown column to expand horizontally
|
|
self.profile_frame.columnconfigure(1, weight=1)
|
|
|
|
|
|
def _create_repo_frame(self):
|
|
"""Creates the frame for repository paths, bundle names, and commit message."""
|
|
self.repo_frame = ttk.LabelFrame(self, text="Repository Configuration", padding=(10, 5))
|
|
self.repo_frame.pack(pady=5, fill="x")
|
|
# Define column indices for clarity and easier adjustment
|
|
col_label = 0
|
|
col_entry = 1
|
|
col_entry_span = 1 # Entry widgets usually span 1 logical column
|
|
col_button1 = col_entry + col_entry_span # Column index for first button (e.g., Browse)
|
|
col_button2 = col_button1 + 1 # Column index for second button (e.g., Edit .gitignore)
|
|
col_indicator = col_button2 + 1 # Column index for status indicator (at the far right)
|
|
|
|
# Configure grid columns weights
|
|
self.repo_frame.columnconfigure(col_entry, weight=1) # Allow main entry fields to expand
|
|
|
|
# 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)
|
|
# Span entry up to the first button column
|
|
self.svn_path_entry.grid(row=0, column=col_entry, columnspan=col_button1 - 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)
|
|
)
|
|
# Place browse button in its designated column
|
|
self.svn_path_browse_button.grid(row=0, column=col_button1, sticky=tk.W, padx=(0, 5), pady=3)
|
|
# Place indicator at the far right
|
|
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)
|
|
# Span entry up to the browse button
|
|
self.usb_path_entry.grid(row=1, column=col_entry, columnspan=col_button1 - 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_button1, 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)
|
|
# Span entry across its column and button columns if needed (up to indicator)
|
|
self.bundle_name_entry.grid(row=2, column=col_entry, columnspan=(col_indicator - col_entry), sticky=tk.EW, padx=5, pady=3)
|
|
|
|
# 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=(col_indicator - col_entry), sticky=tk.EW, padx=5, pady=3)
|
|
|
|
# Row 4: Commit Message + Edit Button
|
|
ttk.Label(self.repo_frame, text="Commit Message:").grid(row=4, column=col_label, sticky=tk.W, padx=5, pady=3)
|
|
self.commit_message_entry = ttk.Entry(
|
|
self.repo_frame,
|
|
textvariable=self.commit_message_var,
|
|
width=60
|
|
)
|
|
# Span entry up to the first button column
|
|
self.commit_message_entry.grid(row=4, column=col_entry, columnspan=col_button1 - col_entry, sticky=tk.EW, padx=5, pady=3)
|
|
self.create_tooltip(self.commit_message_entry, "Optional message for autocommit. If empty, a default message is used.")
|
|
|
|
# Edit .gitignore Button
|
|
self.edit_gitignore_button = ttk.Button(
|
|
self.repo_frame,
|
|
text="Edit .gitignore",
|
|
width=12, # Adjust width as needed
|
|
command=self.open_gitignore_editor_callback, # Use the new callback
|
|
state=tk.DISABLED # Initially disabled, enabled by controller
|
|
)
|
|
# Place button next to commit message entry, in the first button column
|
|
self.edit_gitignore_button.grid(row=4, column=col_button1, sticky=tk.W, padx=(0, 5), pady=3)
|
|
self.create_tooltip(self.edit_gitignore_button, "Open editor for the .gitignore file in the SVN Working Copy.")
|
|
|
|
# Row 5: Autocommit Checkbox
|
|
self.autocommit_checkbox = ttk.Checkbutton(
|
|
self.repo_frame,
|
|
text="Autocommit changes before 'Create Bundle'",
|
|
variable=self.autocommit_var
|
|
)
|
|
# Span across all columns used, up to and including indicator column
|
|
self.autocommit_checkbox.grid(row=5, column=0, columnspan=col_indicator + 1, sticky=tk.W, padx=5, pady=(5, 3))
|
|
|
|
|
|
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")
|
|
# Define columns
|
|
col_label = 0
|
|
col_entry = 1
|
|
col_button = 2
|
|
|
|
# Configure column weights
|
|
self.backup_frame.columnconfigure(col_entry, weight=1) # Allow entry fields to expand
|
|
|
|
# Row 0: Autobackup Checkbox
|
|
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))
|
|
|
|
# Row 1: Backup Directory
|
|
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)
|
|
|
|
# Row 2: Exclude Extensions
|
|
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, # Use Tkinter variable
|
|
width=60
|
|
)
|
|
# Span across entry and button columns
|
|
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)")
|
|
|
|
|
|
def _create_function_frame(self):
|
|
"""Creates the frame holding the main action buttons."""
|
|
self.function_frame = ttk.LabelFrame(self, text="Actions", padding=(10, 10))
|
|
self.function_frame.pack(pady=5, fill="x", anchor=tk.N) # Anchor North
|
|
|
|
# Use a sub-frame to group buttons
|
|
button_subframe = ttk.Frame(self.function_frame)
|
|
button_subframe.pack(fill=tk.X) # Fill horizontally
|
|
|
|
# Prepare SVN button
|
|
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)
|
|
|
|
# Create Bundle button
|
|
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)
|
|
|
|
# Fetch Bundle button
|
|
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)
|
|
|
|
# Manual Backup Button
|
|
self.manual_backup_button = ttk.Button(
|
|
button_subframe,
|
|
text="Backup Now (ZIP)", # Indicate ZIP format
|
|
command=self.manual_backup_callback # Use the new callback
|
|
)
|
|
self.manual_backup_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
|
|
def _create_log_area(self):
|
|
"""Creates the scrolled text area for logging output."""
|
|
log_frame = ttk.Frame(self.master) # Attach to master, below MainFrame content
|
|
log_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=10, pady=(5, 10))
|
|
|
|
# ScrolledText widget for log messages
|
|
self.log_text = scrolledtext.ScrolledText(
|
|
log_frame,
|
|
height=12, # Adjust height as desired
|
|
width=100, # Adjust width as desired
|
|
font=("Consolas", 9), # Use a monospaced font like Consolas or Courier New
|
|
wrap=tk.WORD, # Wrap lines at word boundaries
|
|
state=tk.DISABLED # Start in read-only state
|
|
)
|
|
self.log_text.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
|
|
|
|
|
|
def _initialize_profile_selection(self):
|
|
"""Sets the initial value of the profile dropdown."""
|
|
# Use constant defined in config_manager if available, otherwise hardcode 'default'
|
|
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: # If default not found, select the first available
|
|
self.profile_var.set(self.initial_profile_sections[0])
|
|
# else: No profiles exist, variable remains empty
|
|
|
|
|
|
# --- GUI Update Methods ---
|
|
|
|
def toggle_backup_dir(self):
|
|
"""
|
|
Enables or disables the backup directory entry/button based on the autobackup checkbox.
|
|
"""
|
|
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)
|
|
# Exclude entry state is independent of autobackup checkbox
|
|
|
|
|
|
def browse_backup_dir(self):
|
|
"""Opens a directory selection dialog for the backup directory entry."""
|
|
# Suggest initial directory based on current entry or the default
|
|
initial_dir = self.backup_dir_var.get() or DEFAULT_BACKUP_DIR # Use IMPORTED constant
|
|
dirname = filedialog.askdirectory(
|
|
initialdir=initial_dir,
|
|
title="Select Backup Directory",
|
|
parent=self.master # Ensure dialog is modal to the main window
|
|
)
|
|
if dirname: # Only update if a directory was actually selected
|
|
self.backup_dir_var.set(dirname)
|
|
|
|
|
|
def update_svn_indicator(self, is_prepared):
|
|
"""
|
|
Updates the visual indicator (color) for SVN preparation status and toggles
|
|
the 'Prepare' button state accordingly. (Edit gitignore button state is handled separately)
|
|
|
|
Args:
|
|
is_prepared (bool): True if the SVN repo has a '.git' directory, False otherwise.
|
|
"""
|
|
if is_prepared:
|
|
indicator_color = self.GREEN
|
|
prepare_button_state = tk.DISABLED
|
|
tooltip_text = "Repository is prepared ('.git' found)"
|
|
else:
|
|
indicator_color = self.RED
|
|
prepare_button_state = tk.NORMAL
|
|
tooltip_text = "Repository not prepared ('.git' not found)"
|
|
|
|
# Update indicator color
|
|
if hasattr(self, 'svn_status_indicator'):
|
|
self.svn_status_indicator.config(background=indicator_color)
|
|
self.update_tooltip(self.svn_status_indicator, tooltip_text)
|
|
|
|
# Update prepare button state
|
|
if hasattr(self, 'prepare_svn_button'):
|
|
self.prepare_svn_button.config(state=prepare_button_state)
|
|
|
|
|
|
def update_profile_dropdown(self, sections):
|
|
"""
|
|
Updates the list of profiles shown in the combobox.
|
|
|
|
Args:
|
|
sections (list): The new list of profile names.
|
|
"""
|
|
if hasattr(self, 'profile_dropdown'): # Check if dropdown exists
|
|
current_profile = self.profile_var.get()
|
|
self.profile_dropdown['values'] = sections
|
|
|
|
# Maintain selection if possible, otherwise select default or first, or clear
|
|
if sections:
|
|
if current_profile in sections:
|
|
self.profile_var.set(current_profile) # Keep current selection
|
|
elif "default" in sections:
|
|
self.profile_var.set("default") # Select default if available
|
|
else:
|
|
self.profile_var.set(sections[0]) # Select the first available
|
|
else:
|
|
self.profile_var.set("") # No profiles left, clear selection
|
|
# Optionally trigger the callback if needed after programmatic change
|
|
# self.load_profile_settings_callback(self.profile_var.get())
|
|
|
|
|
|
# --- Dialog Wrappers ---
|
|
# These provide a consistent way to show standard dialogs via the GUI frame
|
|
|
|
def ask_new_profile_name(self):
|
|
"""Asks the user for a new profile name using a simple dialog."""
|
|
# parent=self.master makes the dialog modal to the main window
|
|
return simpledialog.askstring("Add Profile", "Enter new profile name:", parent=self.master)
|
|
|
|
def show_error(self, title, message):
|
|
"""Displays an error message box."""
|
|
messagebox.showerror(title, message, parent=self.master)
|
|
|
|
def show_info(self, title, message):
|
|
"""Displays an information message box."""
|
|
messagebox.showinfo(title, message, parent=self.master)
|
|
|
|
def show_warning(self, title, message):
|
|
"""Displays a warning message box."""
|
|
messagebox.showwarning(title, message, parent=self.master)
|
|
|
|
def ask_yes_no(self, title, message):
|
|
"""Displays a yes/no confirmation dialog."""
|
|
return messagebox.askyesno(title, message, parent=self.master)
|
|
|
|
|
|
# --- Tooltip Helper Methods ---
|
|
# Simple tooltip implementation for GUI elements
|
|
|
|
def create_tooltip(self, widget, text):
|
|
"""Creates a tooltip for a given widget."""
|
|
tooltip = Tooltip(widget, text)
|
|
# Use add='+' to avoid overwriting other bindings
|
|
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='+') # Hide on click
|
|
|
|
|
|
def update_tooltip(self, widget, text):
|
|
"""Updates the text of an existing tooltip (by re-creating it)."""
|
|
# Simple approach: Remove old bindings and create new tooltip
|
|
widget.unbind("<Enter>")
|
|
widget.unbind("<Leave>")
|
|
widget.unbind("<ButtonPress>")
|
|
self.create_tooltip(widget, text) |