441 lines
21 KiB
Python
441 lines
21 KiB
Python
# gui.py
|
|
import tkinter as tk
|
|
from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog # Import simpledialog explicitly
|
|
import logging # Import logging for logger type hinting if needed
|
|
|
|
# --- ADDED: Import the constant from the central location ---
|
|
from config_manager import DEFAULT_BACKUP_DIR
|
|
|
|
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):
|
|
"""
|
|
Initializes the MainFrame.
|
|
|
|
Args:
|
|
master (tk.Tk or ttk.Frame): The parent widget.
|
|
load_profile_settings_cb (callable): Called when profile selection changes. Signature: func(profile_name)
|
|
browse_folder_cb (callable): Called by Browse buttons. Signature: func(entry_widget_to_update)
|
|
update_svn_status_cb (callable): Called when SVN path might change. Signature: func(svn_path)
|
|
prepare_svn_for_git_cb (callable): Called by 'Prepare SVN' button. Signature: func()
|
|
create_git_bundle_cb (callable): Called by 'Create Bundle' button. Signature: func()
|
|
fetch_from_git_bundle_cb (callable): Called by 'Fetch Bundle' button. Signature: func()
|
|
config_manager_instance (ConfigManager): Instance to access config data if needed (e.g., defaults).
|
|
profile_sections_list (list): Initial list of profile names for the dropdown.
|
|
add_profile_cb (callable): Called by 'Add Profile' button. Signature: func()
|
|
remove_profile_cb (callable): Called by 'Remove Profile' button. Signature: func()
|
|
"""
|
|
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
|
|
|
|
# Store config manager and initial profiles if needed locally (e.g., for defaults)
|
|
self.config_manager = config_manager_instance
|
|
self.initial_profile_sections = profile_sections_list
|
|
|
|
# Style configuration (optional)
|
|
self.style = ttk.Style()
|
|
self.style.theme_use('clam') # Example theme, others: 'alt', 'default', 'classic'
|
|
|
|
# Pack the main frame itself into the master window/frame
|
|
# Padding provides space around the entire frame content
|
|
self.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
# --- Tkinter Variables ---
|
|
# Use Tkinter variables for widgets that need their state tracked or easily accessed
|
|
self.profile_var = tk.StringVar()
|
|
self.autobackup_var = tk.BooleanVar()
|
|
self.backup_dir_var = tk.StringVar()
|
|
self.autocommit_var = tk.BooleanVar()
|
|
# Entry widgets will be accessed directly (e.g., self.svn_path_entry.get())
|
|
|
|
# --- Widget Creation ---
|
|
# Create widgets in logical groups (frames)
|
|
self._create_profile_frame()
|
|
self._create_repo_frame()
|
|
self._create_backup_frame()
|
|
self._create_function_frame()
|
|
self._create_log_area() # Log area is created last, packed at bottom
|
|
|
|
# --- Initial State Configuration ---
|
|
# Set initial profile selection after dropdown is created
|
|
self._initialize_profile_selection()
|
|
# Set initial state of backup widgets based on autobackup checkbox
|
|
self.toggle_backup_dir()
|
|
# Initial update of SVN status indicator based on default loaded path
|
|
# The controller (GitSvnSyncApp) should call load_profile_settings which triggers this
|
|
# self.update_svn_status_callback(self.svn_path_entry.get()) # Initial call handled by controller
|
|
|
|
|
|
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)
|
|
|
|
# Profile dropdown (Combobox)
|
|
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 and bundle names."""
|
|
self.repo_frame = ttk.LabelFrame(self, text="Repository Configuration", padding=(10, 5))
|
|
self.repo_frame.pack(pady=5, fill="x")
|
|
|
|
# Row 0: SVN Path
|
|
ttk.Label(self.repo_frame, text="SVN Working Copy:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=3)
|
|
self.svn_path_entry = ttk.Entry(self.repo_frame, width=60) # Reference stored
|
|
self.svn_path_entry.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=3)
|
|
# Update status indicator when path potentially changes (focus out or explicit browse)
|
|
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())) # On pressing Enter
|
|
|
|
|
|
self.svn_path_browse_button = ttk.Button(
|
|
self.repo_frame, text="Browse...", width=9,
|
|
# Lambda ensures the correct entry widget is passed to the callback
|
|
command=lambda: self.browse_folder_callback(self.svn_path_entry)
|
|
)
|
|
self.svn_path_browse_button.grid(row=0, column=2, sticky=tk.W, padx=(0, 5), pady=3)
|
|
|
|
# SVN Status Indicator (visual feedback: red/green square)
|
|
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=3, sticky=tk.E, padx=(0, 5), pady=3)
|
|
# Tooltip for the indicator
|
|
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=0, sticky=tk.W, padx=5, pady=3)
|
|
self.usb_path_entry = ttk.Entry(self.repo_frame, width=60) # Reference stored
|
|
self.usb_path_entry.grid(row=1, column=1, 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=2, sticky=tk.W, padx=(0, 5), pady=3)
|
|
|
|
# Row 2: Bundle Names (Create)
|
|
ttk.Label(self.repo_frame, text="Create Bundle Name:").grid(row=2, column=0, sticky=tk.W, padx=5, pady=3)
|
|
self.bundle_name_entry = ttk.Entry(self.repo_frame, width=60) # Reference stored
|
|
self.bundle_name_entry.grid(row=2, column=1, sticky=tk.EW, padx=5, pady=3)
|
|
|
|
# Row 3: Bundle Names (Fetch)
|
|
ttk.Label(self.repo_frame, text="Fetch Bundle Name:").grid(row=3, column=0, sticky=tk.W, padx=5, pady=3)
|
|
self.bundle_updated_name_entry = ttk.Entry(self.repo_frame, width=60) # Reference stored
|
|
self.bundle_updated_name_entry.grid(row=3, column=1, sticky=tk.EW, padx=5, pady=3)
|
|
|
|
# Row 4: Autocommit Checkbox
|
|
self.autocommit_checkbox = ttk.Checkbutton(
|
|
self.repo_frame,
|
|
text="Autocommit changes before 'Create Bundle'",
|
|
variable=self.autocommit_var # Use Tkinter variable
|
|
)
|
|
self.autocommit_checkbox.grid(row=4, column=0, columnspan=3, sticky=tk.W, padx=5, pady=(5, 3)) # Span columns
|
|
|
|
# Configure column weights for horizontal resizing
|
|
self.repo_frame.columnconfigure(1, weight=1) # Allow entry fields to expand
|
|
|
|
|
|
def _create_backup_frame(self):
|
|
"""Creates the frame for backup configuration."""
|
|
self.backup_frame = ttk.LabelFrame(self, text="Backup Configuration", padding=(10, 5))
|
|
self.backup_frame.pack(pady=5, fill="x")
|
|
|
|
# Autobackup Checkbox
|
|
self.autobackup_checkbox = ttk.Checkbutton(
|
|
self.backup_frame,
|
|
text="Automatic Backup before Create/Fetch",
|
|
variable=self.autobackup_var, # Use Tkinter variable
|
|
command=self.toggle_backup_dir # Update state of other widgets when clicked
|
|
)
|
|
# Span ensures it takes width, sticky aligns it left
|
|
self.autobackup_checkbox.grid(row=0, column=0, columnspan=3, sticky=tk.W, padx=5, pady=(5, 0))
|
|
|
|
# Backup Directory Label, Entry, and Button
|
|
self.backup_dir_label = ttk.Label(self.backup_frame, text="Backup Directory:")
|
|
self.backup_dir_label.grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
|
|
|
|
self.backup_dir_entry = ttk.Entry(
|
|
self.backup_frame,
|
|
textvariable=self.backup_dir_var, # Use Tkinter variable
|
|
width=60,
|
|
state=tk.DISABLED # Initial state depends on checkbox (set by toggle_backup_dir)
|
|
)
|
|
self.backup_dir_entry.grid(row=1, column=1, 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, # Local method to handle browsing
|
|
state=tk.DISABLED # Initial state
|
|
)
|
|
self.backup_dir_button.grid(row=1, column=2, sticky=tk.W, padx=(0, 5), pady=5)
|
|
|
|
# Configure column weights for horizontal resizing
|
|
self.backup_frame.columnconfigure(1, weight=1) # Allow entry field to expand
|
|
|
|
|
|
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")
|
|
|
|
# Action Buttons - use callbacks provided by the controller
|
|
self.prepare_svn_button = ttk.Button(
|
|
self.function_frame,
|
|
text="Prepare SVN Repo",
|
|
command=self.prepare_svn_for_git_callback
|
|
)
|
|
# Pack buttons side-by-side with some padding
|
|
self.prepare_svn_button.pack(side=tk.LEFT, padx=5, pady=0)
|
|
|
|
self.create_bundle_button = ttk.Button(
|
|
self.function_frame,
|
|
text="Create Bundle",
|
|
command=self.create_git_bundle_callback
|
|
)
|
|
self.create_bundle_button.pack(side=tk.LEFT, padx=5, pady=0)
|
|
|
|
self.fetch_bundle_button = ttk.Button(
|
|
self.function_frame,
|
|
text="Fetch from Bundle",
|
|
command=self.fetch_from_git_bundle_callback
|
|
)
|
|
self.fetch_bundle_button.pack(side=tk.LEFT, padx=5, pady=0)
|
|
|
|
# Initially, action buttons might be disabled until a valid profile/path is set
|
|
# The controller (GitSvnSyncApp) will manage enabling/disabling these.
|
|
|
|
|
|
def _create_log_area(self):
|
|
"""Creates the scrolled text area for logging output."""
|
|
# Use a frame to contain the log area and potentially a label
|
|
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))
|
|
|
|
# Optional: Add a label for the log area
|
|
# log_label = ttk.Label(log_frame, text="Log Output:")
|
|
# log_label.pack(side=tk.TOP, anchor=tk.W, pady=(0, 2))
|
|
|
|
# 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."""
|
|
DEFAULT_PROFILE = "default" # Consider getting this from config_manager constants if possible
|
|
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, dropdown is empty
|
|
|
|
# --- GUI Update Methods ---
|
|
|
|
def toggle_backup_dir(self):
|
|
"""
|
|
Enables or disables the backup directory entry/button based on the autobackup checkbox.
|
|
"""
|
|
if self.autobackup_var.get():
|
|
new_state = tk.NORMAL
|
|
else:
|
|
new_state = tk.DISABLED
|
|
# Check if widgets exist before configuring (robustness)
|
|
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):
|
|
"""
|
|
Opens a directory selection dialog for the backup directory entry.
|
|
Uses the centrally defined DEFAULT_BACKUP_DIR.
|
|
"""
|
|
# 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.
|
|
|
|
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
|
|
self.profile_dropdown.event_generate("<<ComboboxSelected>>") # Trigger update if needed
|
|
|
|
|
|
# --- 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 ---
|
|
# Simple tooltip implementation for GUI elements
|
|
|
|
def create_tooltip(self, widget, text):
|
|
tooltip = Tooltip(widget, text)
|
|
widget.bind("<Enter>", lambda event: tooltip.showtip())
|
|
widget.bind("<Leave>", lambda event: tooltip.hidetip())
|
|
|
|
def update_tooltip(self, widget, text):
|
|
# Find existing tooltip if it exists and update text
|
|
# This assumes the tooltip instance is stored or accessible;
|
|
# simpler here to just re-bind with a new tooltip instance
|
|
self.create_tooltip(widget, text) # Re-creates tooltip with new text
|
|
|
|
# --- Tooltip Class (Simple Implementation) ---
|
|
# Place this outside the MainFrame class or in a separate utility module
|
|
|
|
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
|
|
x, y, _, _ = self.widget.bbox("insert") # Get widget location
|
|
x += self.widget.winfo_rootx() + 25 # Position tooltip slightly below and right
|
|
y += self.widget.winfo_rooty() + 25
|
|
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"+{x}+{y}") # Position the window
|
|
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:
|
|
tw.destroy() |