# 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("<>", 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("", lambda e: self.update_svn_status_callback(self.svn_path_entry.get())) self.svn_path_entry.bind("", 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("<>") # 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("", lambda event: tooltip.showtip()) widget.bind("", 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()