SXXXXXXX_GitUtility/gui.py
2025-04-07 10:36:40 +02:00

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()