SXXXXXXX_GitUtility/gui.py
2025-04-07 12:30:51 +02:00

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)