1249 lines
53 KiB
Python
1249 lines
53 KiB
Python
# gui.py
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from tkinter import scrolledtext
|
|
from tkinter import filedialog
|
|
from tkinter import messagebox
|
|
from tkinter import simpledialog
|
|
import logging
|
|
import os
|
|
import re # Ensure re is imported
|
|
|
|
# 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):
|
|
"""Initialize tooltip."""
|
|
self.widget = widget
|
|
self.text = text
|
|
self.tooltip_window = None
|
|
self.id = None
|
|
self.x = 0
|
|
self.y = 0
|
|
|
|
def showtip(self):
|
|
"""Display text in a tooltip window."""
|
|
# Hide any existing tooltip first
|
|
self.hidetip()
|
|
# Avoid error if widget is destroyed before showing tooltip
|
|
if not self.widget.winfo_exists():
|
|
return
|
|
try:
|
|
# Get widget position relative to widget itself
|
|
x_rel, y_rel, _, _ = self.widget.bbox("insert")
|
|
# Get widget position relative to screen
|
|
x_root = self.widget.winfo_rootx()
|
|
y_root = self.widget.winfo_rooty()
|
|
# Calculate final screen position with offset
|
|
x_pos = x_root + x_rel + 25
|
|
y_pos = y_root + y_rel + 25
|
|
except tk.TclError:
|
|
# Fallback position calculation if bbox fails (e.g., widget hidden)
|
|
x_root = self.widget.winfo_rootx()
|
|
y_root = self.widget.winfo_rooty()
|
|
widget_width = self.widget.winfo_width()
|
|
widget_height = self.widget.winfo_height()
|
|
x_pos = x_root + widget_width // 2
|
|
y_pos = y_root + widget_height + 5
|
|
|
|
# Create the tooltip window as a Toplevel
|
|
self.tooltip_window = tk.Toplevel(self.widget)
|
|
tw = self.tooltip_window
|
|
# Remove window decorations (border, title bar etc.)
|
|
tw.wm_overrideredirect(True)
|
|
# Position the window (ensure integer coordinates)
|
|
tw.wm_geometry(f"+{int(x_pos)}+{int(y_pos)}")
|
|
# Create the label inside the tooltip window
|
|
label = tk.Label(
|
|
tw,
|
|
text=self.text,
|
|
justify=tk.LEFT,
|
|
background="#ffffe0", # Light yellow background
|
|
relief=tk.SOLID,
|
|
borderwidth=1,
|
|
font=("tahoma", "8", "normal")
|
|
)
|
|
label.pack(ipadx=1)
|
|
|
|
def hidetip(self):
|
|
"""Hide the tooltip window."""
|
|
tw = self.tooltip_window
|
|
# Reset the reference
|
|
self.tooltip_window = None
|
|
# Destroy the window if it exists
|
|
if tw:
|
|
try:
|
|
# Check if window still exists before destroying
|
|
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):
|
|
""" Toplevel window for editing the .gitignore file. """
|
|
def __init__(self, master, gitignore_path, logger):
|
|
""" Initialize the editor window. """
|
|
super().__init__(master)
|
|
self.gitignore_path = gitignore_path
|
|
self.logger = logger
|
|
# Store original content to check for changes on close
|
|
self.original_content = ""
|
|
|
|
# --- Window Configuration ---
|
|
base_filename = os.path.basename(gitignore_path)
|
|
self.title(f"Edit {base_filename}")
|
|
self.geometry("600x450") # Initial size
|
|
self.minsize(400, 300) # Minimum resizeable dimensions
|
|
self.grab_set() # Make window modal (grab all events)
|
|
self.transient(master) # Keep window on top of parent
|
|
# Handle closing via window manager (X button)
|
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
|
|
# --- Widgets ---
|
|
# Main frame with padding
|
|
main_frame = ttk.Frame(self, padding="10")
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
# Configure grid weights for resizing text area
|
|
main_frame.rowconfigure(0, weight=1)
|
|
main_frame.columnconfigure(0, weight=1)
|
|
|
|
# ScrolledText widget for editing content
|
|
self.text_editor = scrolledtext.ScrolledText(
|
|
main_frame,
|
|
wrap=tk.WORD,
|
|
font=("Consolas", 10), # Monospaced font
|
|
undo=True # Enable undo/redo functionality
|
|
)
|
|
self.text_editor.grid(row=0, column=0, sticky="nsew", pady=(0, 10))
|
|
|
|
# Frame for buttons at the bottom
|
|
button_frame = ttk.Frame(main_frame)
|
|
button_frame.grid(row=1, column=0, sticky="ew")
|
|
# Configure button frame columns to center buttons
|
|
button_frame.columnconfigure(0, weight=1) # Spacer left
|
|
button_frame.columnconfigure(3, weight=1) # Spacer 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 right-center
|
|
|
|
# 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 left-center
|
|
|
|
# --- Load Initial Content ---
|
|
self._load_file()
|
|
# Center window relative to parent after creation
|
|
self._center_window(master)
|
|
# Set initial focus to the text editor
|
|
self.text_editor.focus_set()
|
|
|
|
def _center_window(self, parent):
|
|
"""Centers the editor window relative to its parent."""
|
|
self.update_idletasks() # Process pending geometry changes
|
|
# Get parent window geometry and position
|
|
parent_x = parent.winfo_rootx()
|
|
parent_y = parent.winfo_rooty()
|
|
parent_width = parent.winfo_width()
|
|
parent_height = parent.winfo_height()
|
|
# Get self (editor window) geometry
|
|
win_width = self.winfo_width()
|
|
win_height = self.winfo_height()
|
|
# Calculate position for centering
|
|
x_pos = parent_x + (parent_width // 2) - (win_width // 2)
|
|
y_pos = parent_y + (parent_height // 2) - (win_height // 2)
|
|
# Basic screen boundary check to prevent window going off-screen
|
|
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))
|
|
# Apply the calculated position using wm_geometry
|
|
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:
|
|
content = "" # Default to empty content
|
|
if os.path.exists(self.gitignore_path):
|
|
# Read file content with specified encoding and error handling
|
|
with open(self.gitignore_path, 'r',
|
|
encoding='utf-8', errors='replace') as f:
|
|
content = f.read()
|
|
self.logger.debug(".gitignore content loaded successfully.")
|
|
else:
|
|
# File doesn't exist
|
|
self.logger.info(f"'{self.gitignore_path}' does not exist.")
|
|
|
|
# Store original content and update editor text
|
|
self.original_content = content
|
|
self.text_editor.delete("1.0", tk.END) # Clear existing content first
|
|
self.text_editor.insert(tk.END, self.original_content)
|
|
# Reset undo stack after programmatically changing text
|
|
self.text_editor.edit_reset()
|
|
|
|
except IOError as e:
|
|
# Handle file reading errors specifically
|
|
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 # Show error relative to this dialog
|
|
)
|
|
except Exception as e:
|
|
# Handle other unexpected errors during file loading
|
|
self.logger.exception(f"Unexpected error loading file: {e}")
|
|
messagebox.showerror(
|
|
"Unexpected Error",
|
|
f"An unexpected error occurred loading the file:\n{e}",
|
|
parent=self
|
|
)
|
|
|
|
def _save_file(self):
|
|
"""Saves the current editor content to the .gitignore file."""
|
|
# Get content from text widget, remove trailing whitespace
|
|
current_content = self.text_editor.get("1.0", tk.END).rstrip()
|
|
# Add a single trailing newline if content is not empty
|
|
if current_content:
|
|
current_content += "\n"
|
|
|
|
# Normalize original content similarly for accurate comparison
|
|
normalized_original = self.original_content.rstrip()
|
|
if normalized_original:
|
|
normalized_original += "\n"
|
|
|
|
# Check if content has actually changed
|
|
if current_content == normalized_original:
|
|
self.logger.info("No changes detected in .gitignore. Skipping save.")
|
|
return True # Indicate success (no action needed)
|
|
|
|
# Proceed with saving if content changed
|
|
self.logger.info(f"Saving changes to: {self.gitignore_path}")
|
|
try:
|
|
# Write content to file with UTF-8 encoding and consistent newline
|
|
with open(self.gitignore_path, 'w', encoding='utf-8', newline='\n') as f:
|
|
f.write(current_content)
|
|
self.logger.info(".gitignore file saved successfully.")
|
|
# Update original content baseline after successful save
|
|
self.original_content = current_content
|
|
# Reset undo stack after saving changes
|
|
self.text_editor.edit_reset()
|
|
return True # Indicate save success
|
|
except IOError as e:
|
|
# Handle file writing errors
|
|
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 save failure
|
|
except Exception as e:
|
|
# Handle other unexpected errors during saving
|
|
self.logger.exception(f"Unexpected error saving file: {e}")
|
|
messagebox.showerror("Unexpected Error",
|
|
f"An unexpected error occurred saving file:\n{e}",
|
|
parent=self)
|
|
return False
|
|
|
|
def _save_and_close(self):
|
|
"""Saves the file and closes the window if save is successful."""
|
|
save_successful = self._save_file()
|
|
if save_successful:
|
|
# Close window only if save succeeded or no changes were made
|
|
self.destroy()
|
|
|
|
def _on_close(self):
|
|
"""Handles closing the window (Cancel button or WM close button)."""
|
|
# Get current content and normalize it
|
|
current_content = self.text_editor.get("1.0", tk.END).rstrip()
|
|
if current_content:
|
|
current_content += "\n"
|
|
# Normalize original content
|
|
normalized_original = self.original_content.rstrip()
|
|
if normalized_original:
|
|
normalized_original += "\n"
|
|
|
|
# Check if content has changed since loading/last save
|
|
if current_content != normalized_original:
|
|
# Ask user about saving changes (Yes/No/Cancel)
|
|
response = messagebox.askyesnocancel(
|
|
"Unsaved Changes",
|
|
"You have unsaved changes.\nSave before closing?",
|
|
parent=self
|
|
)
|
|
if response is True: # User chose Yes (Save)
|
|
# Attempt save, close only if successful
|
|
self._save_and_close()
|
|
elif response is False: # User chose No (Discard)
|
|
self.logger.warning("Discarding unsaved changes in editor.")
|
|
self.destroy() # Close immediately
|
|
# else (response is None - Cancel): Do nothing, keep window open
|
|
else:
|
|
# No changes detected, simply close the window
|
|
self.destroy()
|
|
# --- End Gitignore Editor Window ---
|
|
|
|
|
|
# --- Create Tag Dialog ---
|
|
class CreateTagDialog(simpledialog.Dialog):
|
|
""" Dialog to get new tag name and message. """
|
|
def __init__(self, parent, title="Create New Tag"):
|
|
"""Initialize the dialog."""
|
|
self.tag_name_var = tk.StringVar()
|
|
self.tag_message_var = tk.StringVar()
|
|
self.result = None # Stores (name, message) tuple on success
|
|
# Call Dialog constructor AFTER initializing variables
|
|
super().__init__(parent, title=title)
|
|
|
|
def body(self, master):
|
|
"""Create dialog body with input fields."""
|
|
name_label = ttk.Label(master, text="Tag Name:")
|
|
name_label.grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
|
|
self.name_entry = ttk.Entry(
|
|
master,
|
|
textvariable=self.tag_name_var,
|
|
width=40
|
|
)
|
|
self.name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
|
|
|
message_label = ttk.Label(master, text="Tag Message:")
|
|
message_label.grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
|
|
|
self.message_entry = ttk.Entry(
|
|
master,
|
|
textvariable=self.tag_message_var,
|
|
width=40
|
|
)
|
|
self.message_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
|
|
|
|
# Configure column to allow entry widgets to expand horizontally
|
|
master.columnconfigure(1, weight=1)
|
|
# Return the widget that should have initial focus
|
|
return self.name_entry
|
|
|
|
def validate(self):
|
|
"""Validate the input fields are not empty and name format is valid."""
|
|
name = self.tag_name_var.get().strip()
|
|
message = self.tag_message_var.get().strip()
|
|
|
|
# Check for empty fields
|
|
if not name:
|
|
messagebox.showwarning("Input Error", "Tag name cannot be empty.",
|
|
parent=self)
|
|
return 0 # Fail validation
|
|
if not message:
|
|
messagebox.showwarning("Input Error", "Tag message cannot be empty.",
|
|
parent=self)
|
|
return 0 # Fail validation
|
|
|
|
# Validate tag name format using regex (ensure 're' is imported)
|
|
pattern = r"^(?![./]|.*([./]{2,}|[.]$|\.lock$|@\{))[^ \t\n\r\f\v~^:?*[\\]+(?<!\.)$"
|
|
if not re.match(pattern, name):
|
|
messagebox.showwarning(
|
|
"Input Error",
|
|
"Invalid tag name format.\nAvoid spaces, '..', ending '.', etc.",
|
|
parent=self
|
|
)
|
|
return 0 # Fail validation
|
|
|
|
# All checks passed
|
|
return 1 # Validation successful
|
|
|
|
def apply(self):
|
|
"""Process the data if validation succeeds."""
|
|
# Store the validated results as a tuple
|
|
name = self.tag_name_var.get().strip()
|
|
message = self.tag_message_var.get().strip()
|
|
self.result = (name, message)
|
|
# --- End Create Tag Dialog ---
|
|
|
|
|
|
# --- Create Branch Dialog ---
|
|
class CreateBranchDialog(simpledialog.Dialog):
|
|
""" Dialog to get new branch name. """
|
|
def __init__(self, parent, title="Create New Branch"):
|
|
"""Initialize the dialog."""
|
|
self.branch_name_var = tk.StringVar()
|
|
self.result = None # Stores branch name on success
|
|
super().__init__(parent, title=title)
|
|
|
|
def body(self, master):
|
|
"""Create dialog body with input field."""
|
|
branch_label = ttk.Label(master, text="New Branch Name:")
|
|
branch_label.grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
|
|
self.name_entry = ttk.Entry(
|
|
master,
|
|
textvariable=self.branch_name_var,
|
|
width=40
|
|
)
|
|
self.name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
|
|
|
# TODO: Add option for start point (tag/branch)? Requires more UI elements.
|
|
# Configure column to allow entry to expand horizontally
|
|
master.columnconfigure(1, weight=1)
|
|
# Set initial focus
|
|
return self.name_entry
|
|
|
|
def validate(self):
|
|
"""Validate the branch name input."""
|
|
name = self.branch_name_var.get().strip()
|
|
if not name:
|
|
messagebox.showwarning("Input Error", "Branch name cannot be empty.",
|
|
parent=self)
|
|
return 0 # Fail validation
|
|
|
|
# Basic Git branch name validation (ensure 're' is imported)
|
|
pattern = r"^(?![./]|.*([./]{2,}|[.]$|[/]$|@\{))[^ \t\n\r\f\v~^:?*[\\]+(?<!\.lock)$"
|
|
if not re.match(pattern, name):
|
|
messagebox.showwarning("Input Error", "Invalid branch name format.",
|
|
parent=self)
|
|
return 0 # Fail validation
|
|
|
|
return 1 # Validation successful
|
|
|
|
def apply(self):
|
|
"""Process the data if validation succeeds."""
|
|
# Store the validated branch name
|
|
self.result = self.branch_name_var.get().strip()
|
|
# --- End Create Branch Dialog ---
|
|
|
|
|
|
class MainFrame(ttk.Frame):
|
|
"""
|
|
The main frame containing all GUI elements, organized with tabs.
|
|
"""
|
|
GREEN = "#90EE90" # Light Green
|
|
RED = "#F08080" # Light Coral
|
|
|
|
def __init__(self, master, load_profile_settings_cb, # browse_folder_cb REMOVED
|
|
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, save_profile_cb, manual_commit_cb,
|
|
refresh_tags_cb, create_tag_cb, checkout_tag_cb, delete_tag_cb,
|
|
refresh_branches_cb, create_branch_cb, switch_branch_cb, delete_branch_cb):
|
|
""" Initializes the MainFrame with callbacks and components. """
|
|
super().__init__(master)
|
|
self.master = master
|
|
|
|
# --- Store Callbacks ---
|
|
self.load_profile_settings_callback = load_profile_settings_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
|
|
self.save_profile_callback = save_profile_cb
|
|
self.manual_commit_callback = manual_commit_cb
|
|
# Tag callbacks
|
|
self.refresh_tags_callback = refresh_tags_cb
|
|
self.create_tag_callback = create_tag_cb
|
|
self.checkout_tag_callback = checkout_tag_cb
|
|
self.delete_tag_callback = delete_tag_cb
|
|
# Branch callbacks
|
|
self.refresh_branches_callback = refresh_branches_cb
|
|
self.create_branch_callback = create_branch_cb
|
|
self.switch_branch_callback = switch_branch_cb
|
|
self.delete_branch_callback = delete_branch_cb
|
|
|
|
# Store other instances/data
|
|
self.config_manager = config_manager_instance
|
|
self.initial_profile_sections = profile_sections_list
|
|
|
|
# --- Style ---
|
|
self.style = ttk.Style()
|
|
self.style.theme_use('clam')
|
|
|
|
# --- Pack Main Frame ---
|
|
self.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=(10, 0))
|
|
|
|
# --- 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()
|
|
self.backup_exclude_extensions_var = tk.StringVar()
|
|
self.current_branch_var = tk.StringVar(value="<N/A>")
|
|
|
|
# --- Create Main Layout Sections ---
|
|
self._create_profile_frame()
|
|
self._create_notebook_with_tabs()
|
|
self._create_function_frame()
|
|
self._create_log_area()
|
|
|
|
# --- Initial State Configuration ---
|
|
self._initialize_profile_selection()
|
|
self.toggle_backup_dir()
|
|
|
|
|
|
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=(0, 5), fill="x")
|
|
self.profile_frame.columnconfigure(1, weight=1)
|
|
|
|
profile_label = ttk.Label(self.profile_frame, text="Profile:")
|
|
profile_label.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", width=35, values=self.initial_profile_sections
|
|
)
|
|
self.profile_dropdown.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=5)
|
|
self.profile_dropdown.bind(
|
|
"<<ComboboxSelected>>",
|
|
lambda e: self.load_profile_settings_callback(self.profile_var.get())
|
|
)
|
|
self.profile_var.trace_add(
|
|
"write",
|
|
lambda *a: self.load_profile_settings_callback(self.profile_var.get())
|
|
)
|
|
|
|
self.save_settings_button = ttk.Button(
|
|
self.profile_frame, text="Save Settings",
|
|
command=self.save_profile_callback
|
|
)
|
|
self.save_settings_button.grid(row=0, column=2, sticky=tk.W,
|
|
padx=(5, 2), pady=5)
|
|
self.create_tooltip(self.save_settings_button,
|
|
"Save settings for selected profile.")
|
|
|
|
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=3, sticky=tk.W,
|
|
padx=(2, 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=4, sticky=tk.W,
|
|
padx=(2, 5), pady=5)
|
|
|
|
|
|
def _create_notebook_with_tabs(self):
|
|
"""Creates the main Notebook widget and its tabs."""
|
|
self.notebook = ttk.Notebook(self, padding=(0, 5, 0, 0))
|
|
self.notebook.pack(pady=5, padx=0, fill="both", expand=True)
|
|
|
|
# Create frames for each tab's content
|
|
self.setup_tab_frame = ttk.Frame(self.notebook, padding=(10))
|
|
self.commit_branch_tab_frame = ttk.Frame(self.notebook, padding=(10))
|
|
self.tags_gitignore_tab_frame = ttk.Frame(self.notebook, padding=(10))
|
|
|
|
# Add frames as tabs
|
|
self.notebook.add(self.setup_tab_frame, text=' Setup & Backup ')
|
|
self.notebook.add(self.commit_branch_tab_frame, text=' Commit & Branches ')
|
|
self.notebook.add(self.tags_gitignore_tab_frame, text=' Tags & Gitignore ')
|
|
|
|
# Populate each tab with its widgets
|
|
self._populate_setup_tab(self.setup_tab_frame)
|
|
self._populate_commit_branch_tab(self.commit_branch_tab_frame)
|
|
self._populate_tags_gitignore_tab(self.tags_gitignore_tab_frame)
|
|
|
|
|
|
def _populate_setup_tab(self, parent_tab_frame):
|
|
"""Creates and places widgets for the Setup & Backup tab."""
|
|
# Create and pack sub-frames within this tab
|
|
repo_paths_frame = self._create_repo_paths_frame(parent_tab_frame)
|
|
repo_paths_frame.pack(pady=(0, 5), fill="x", expand=False)
|
|
|
|
backup_config_frame = self._create_backup_config_frame(parent_tab_frame)
|
|
backup_config_frame.pack(pady=5, fill="x", expand=False)
|
|
|
|
|
|
def _create_repo_paths_frame(self, parent):
|
|
"""Creates the sub-frame for repository paths and bundle names."""
|
|
frame = ttk.LabelFrame(parent, text="Repository & Bundle Paths",
|
|
padding=(10, 5))
|
|
col_label = 0
|
|
col_entry = 1
|
|
col_button = 2
|
|
col_indicator = 3
|
|
# Configure entry column to expand horizontally
|
|
frame.columnconfigure(col_entry, weight=1)
|
|
|
|
# Row 0: SVN Path
|
|
svn_label = ttk.Label(frame, text="SVN Working Copy:")
|
|
svn_label.grid(row=0, column=col_label, sticky=tk.W, padx=5, pady=3)
|
|
self.svn_path_entry = ttk.Entry(frame, width=60)
|
|
self.svn_path_entry.grid(row=0, column=col_entry, sticky=tk.EW, padx=5, pady=3)
|
|
# Bind events to trigger status updates
|
|
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()))
|
|
# Button uses local browse_folder method
|
|
self.svn_path_browse_button = ttk.Button(
|
|
frame,
|
|
text="Browse...",
|
|
width=9,
|
|
command=lambda w=self.svn_path_entry: self.browse_folder(w)
|
|
)
|
|
self.svn_path_browse_button.grid(row=0, column=col_button, sticky=tk.W, padx=(0, 5), pady=3)
|
|
# Status Indicator
|
|
self.svn_status_indicator = tk.Label(
|
|
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, "Git repo status (Green=Ready, Red=Not Ready)")
|
|
|
|
# Row 1: USB/Bundle Target Path
|
|
usb_label = ttk.Label(frame, text="Bundle Target Dir:")
|
|
usb_label.grid(row=1, column=col_label, sticky=tk.W, padx=5, pady=3)
|
|
self.usb_path_entry = ttk.Entry(frame, width=60)
|
|
self.usb_path_entry.grid(row=1, column=col_entry, sticky=tk.EW, padx=5, pady=3)
|
|
# Button uses local browse_folder method
|
|
self.usb_path_browse_button = ttk.Button(
|
|
frame,
|
|
text="Browse...",
|
|
width=9,
|
|
command=lambda w=self.usb_path_entry: self.browse_folder(w)
|
|
)
|
|
self.usb_path_browse_button.grid(row=1, column=col_button, sticky=tk.W, padx=(0, 5), pady=3)
|
|
|
|
# Row 2: Create Bundle Name
|
|
create_label = ttk.Label(frame, text="Create Bundle Name:")
|
|
create_label.grid(row=2, column=col_label, sticky=tk.W, padx=5, pady=3)
|
|
self.bundle_name_entry = ttk.Entry(frame, width=60)
|
|
self.bundle_name_entry.grid(row=2, column=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3)
|
|
|
|
# Row 3: Fetch Bundle Name
|
|
fetch_label = ttk.Label(frame, text="Fetch Bundle Name:")
|
|
fetch_label.grid(row=3, column=col_label, sticky=tk.W, padx=5, pady=3)
|
|
self.bundle_updated_name_entry = ttk.Entry(frame, width=60)
|
|
self.bundle_updated_name_entry.grid(row=3, column=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3)
|
|
|
|
return frame
|
|
|
|
|
|
def _create_backup_config_frame(self, parent):
|
|
"""Creates the sub-frame for backup configuration."""
|
|
frame = ttk.LabelFrame(parent, text="Backup Configuration (ZIP)", padding=(10, 5))
|
|
col_label = 0
|
|
col_entry = 1
|
|
col_button = 2
|
|
frame.columnconfigure(col_entry, weight=1) # Entry expands
|
|
|
|
# Row 0: Autobackup Checkbox
|
|
self.autobackup_checkbox = ttk.Checkbutton(
|
|
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=3, sticky=tk.W, padx=5, pady=(5, 0))
|
|
|
|
# Row 1: Backup Directory
|
|
backup_dir_label = ttk.Label(frame, text="Backup Directory:")
|
|
backup_dir_label.grid(row=1, column=col_label, sticky=tk.W, padx=5, pady=5)
|
|
self.backup_dir_entry = ttk.Entry(
|
|
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)
|
|
# Button uses local browse_backup_dir method
|
|
self.backup_dir_button = ttk.Button(
|
|
frame,
|
|
text="Browse...",
|
|
width=9,
|
|
command=self.browse_backup_dir, # Call local method
|
|
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
|
|
exclude_label = ttk.Label(frame, text="Exclude Extensions:")
|
|
exclude_label.grid(row=2, column=col_label, sticky=tk.W, padx=5, pady=5)
|
|
self.backup_exclude_entry = ttk.Entry(
|
|
frame,
|
|
textvariable=self.backup_exclude_extensions_var,
|
|
width=60
|
|
)
|
|
# Span entry across entry and button columns
|
|
self.backup_exclude_entry.grid(row=2, column=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=5)
|
|
self.create_tooltip(self.backup_exclude_entry, "Comma-separated (e.g., .log,.tmp)")
|
|
|
|
return frame
|
|
|
|
|
|
def _populate_commit_branch_tab(self, parent_tab_frame):
|
|
"""Creates and places widgets for the Commit & Branches tab."""
|
|
# Configure overall tab columns/rows for expansion
|
|
parent_tab_frame.columnconfigure(0, weight=1) # Listbox column expands
|
|
parent_tab_frame.rowconfigure(1, weight=1) # Branch subframe row expands
|
|
|
|
# Create Commit sub-frame (positioned at top)
|
|
commit_subframe = self._create_commit_management_frame(parent_tab_frame)
|
|
commit_subframe.grid(row=0, column=0, columnspan=2, sticky="ew", padx=0, pady=(0, 10))
|
|
|
|
# Create Branch sub-frame (positioned below commit)
|
|
branch_subframe = self._create_branch_management_frame(parent_tab_frame)
|
|
branch_subframe.grid(row=1, column=0, columnspan=2, sticky="nsew", padx=0, pady=0)
|
|
|
|
|
|
def _create_commit_management_frame(self, parent):
|
|
"""Creates the sub-frame for commit message and actions."""
|
|
frame = ttk.LabelFrame(parent, text="Commit", padding=5)
|
|
# Configure internal columns
|
|
frame.columnconfigure(1, weight=1) # Entry expands
|
|
|
|
# Row 0: Autocommit Checkbox
|
|
self.autocommit_checkbox = ttk.Checkbutton(
|
|
frame,
|
|
text="Autocommit before 'Create Bundle' (uses message below)",
|
|
variable=self.autocommit_var,
|
|
state=tk.DISABLED # State depends on repo readiness
|
|
)
|
|
self.autocommit_checkbox.grid(row=0, column=0, columnspan=3, sticky="w", padx=5, pady=(5, 3))
|
|
self.create_tooltip(self.autocommit_checkbox, "If checked, commit changes before Create Bundle.")
|
|
|
|
# Row 1: Commit Message Entry + Manual Commit Button
|
|
commit_msg_label = ttk.Label(frame, text="Commit Message:")
|
|
commit_msg_label.grid(row=1, column=0, sticky="w", padx=5, pady=3)
|
|
|
|
self.commit_message_entry = ttk.Entry(
|
|
frame,
|
|
textvariable=self.commit_message_var,
|
|
width=50, # Adjust width as needed
|
|
state=tk.DISABLED # State depends on repo readiness
|
|
)
|
|
self.commit_message_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=3)
|
|
self.create_tooltip(self.commit_message_entry, "Message for manual commit or autocommit.")
|
|
|
|
# Manual Commit Button
|
|
self.commit_button = ttk.Button(
|
|
frame,
|
|
text="Commit Changes",
|
|
width=15, # Adjusted width
|
|
command=self.manual_commit_callback, # Connect to controller
|
|
state=tk.DISABLED # State depends on repo readiness
|
|
)
|
|
self.commit_button.grid(row=1, column=2, sticky="w", padx=(5, 0), pady=3)
|
|
self.create_tooltip(self.commit_button, "Manually commit staged changes with this message.")
|
|
|
|
return frame
|
|
|
|
|
|
def _create_branch_management_frame(self, parent):
|
|
"""Creates the sub-frame for branch operations."""
|
|
frame = ttk.LabelFrame(parent, text="Branches", padding=5)
|
|
# Configure grid columns within this frame
|
|
frame.columnconfigure(1, weight=1) # Listbox column expands
|
|
frame.rowconfigure(2, weight=1) # Listbox row expands
|
|
|
|
# Row 0: Current Branch Display
|
|
current_branch_label = ttk.Label(frame, text="Current Branch:")
|
|
current_branch_label.grid(row=0, column=0, sticky="w", padx=5, pady=3)
|
|
|
|
self.current_branch_display = ttk.Label(
|
|
frame,
|
|
textvariable=self.current_branch_var, # Use Tkinter variable
|
|
font=("Segoe UI", 9, "bold"), # Style for emphasis
|
|
relief=tk.SUNKEN, # Sunken appearance
|
|
padding=(3, 1) # Internal padding
|
|
)
|
|
# Span display across listbox and button columns? Or just listbox? Let's span 2
|
|
self.current_branch_display.grid(row=0, column=1, columnspan=2, sticky="ew", padx=5, pady=3)
|
|
self.create_tooltip(self.current_branch_display, "The currently active branch or state.")
|
|
|
|
# Row 1: Listbox Label
|
|
branch_list_label = ttk.Label(frame, text="Local Branches:")
|
|
# Span label across all columns below it
|
|
branch_list_label.grid(row=1, column=0, columnspan=4, sticky="w", padx=5, pady=(10, 0))
|
|
|
|
# Row 2: Listbox + Scrollbar Frame (Spans first 3 columns)
|
|
branch_list_frame = ttk.Frame(frame)
|
|
branch_list_frame.grid(row=2, column=0, columnspan=3, sticky="nsew", padx=5, pady=(0, 5))
|
|
branch_list_frame.rowconfigure(0, weight=1)
|
|
branch_list_frame.columnconfigure(0, weight=1)
|
|
|
|
self.branch_listbox = tk.Listbox(
|
|
branch_list_frame,
|
|
height=5, # Initial height
|
|
exportselection=False,
|
|
selectmode=tk.SINGLE,
|
|
font=("Consolas", 9) # Monospaced font for potential alignment
|
|
)
|
|
self.branch_listbox.grid(row=0, column=0, sticky="nsew")
|
|
|
|
branch_scrollbar = ttk.Scrollbar(
|
|
branch_list_frame,
|
|
orient=tk.VERTICAL,
|
|
command=self.branch_listbox.yview
|
|
)
|
|
branch_scrollbar.grid(row=0, column=1, sticky="ns")
|
|
self.branch_listbox.config(yscrollcommand=branch_scrollbar.set)
|
|
self.create_tooltip(self.branch_listbox, "Select a branch for actions (Switch, Delete).")
|
|
|
|
# Row 2, Column 3: Vertical Button Frame for Branch Actions
|
|
branch_button_frame = ttk.Frame(frame)
|
|
# Place it in the 4th column (index 3), aligned with listbox row
|
|
branch_button_frame.grid(row=2, column=3, sticky="ns", padx=(10, 5), pady=(0, 5))
|
|
|
|
button_width_branch = 18 # Consistent width
|
|
|
|
self.refresh_branches_button = ttk.Button(
|
|
branch_button_frame, text="Refresh List", width=button_width_branch,
|
|
command=self.refresh_branches_callback, state=tk.DISABLED
|
|
)
|
|
self.refresh_branches_button.pack(side=tk.TOP, fill=tk.X, pady=(0, 3))
|
|
self.create_tooltip(self.refresh_branches_button, "Reload branch list.")
|
|
|
|
self.create_branch_button = ttk.Button(
|
|
branch_button_frame, text="Create Branch...", width=button_width_branch,
|
|
command=self.create_branch_callback, state=tk.DISABLED
|
|
)
|
|
self.create_branch_button.pack(side=tk.TOP, fill=tk.X, pady=3)
|
|
self.create_tooltip(self.create_branch_button, "Create a new local branch.")
|
|
|
|
self.switch_branch_button = ttk.Button(
|
|
branch_button_frame, text="Switch to Selected", width=button_width_branch,
|
|
command=self.switch_branch_callback, state=tk.DISABLED
|
|
)
|
|
self.switch_branch_button.pack(side=tk.TOP, fill=tk.X, pady=3)
|
|
self.create_tooltip(self.switch_branch_button, "Checkout the selected branch.")
|
|
|
|
self.delete_branch_button = ttk.Button(
|
|
branch_button_frame, text="Delete Selected", width=button_width_branch,
|
|
command=self.delete_branch_callback, state=tk.DISABLED
|
|
)
|
|
self.delete_branch_button.pack(side=tk.TOP, fill=tk.X, pady=(3, 0))
|
|
self.create_tooltip(self.delete_branch_button, "Delete selected local branch.")
|
|
|
|
return frame # Return the created frame
|
|
|
|
|
|
def _populate_tags_gitignore_tab(self, parent_tab_frame):
|
|
"""Creates and places widgets for the Tags & Gitignore tab."""
|
|
# Configure grid: listbox expands, button column fixed width
|
|
parent_tab_frame.columnconfigure(0, weight=1)
|
|
parent_tab_frame.rowconfigure(0, weight=1) # Listbox row expands vertically
|
|
|
|
# --- Tag Management Section (Left part of the tab) ---
|
|
tag_list_frame = self._create_tag_management_frame(parent_tab_frame)
|
|
# Span both rows (tag list and potential future rows)
|
|
tag_list_frame.grid(row=0, column=0, rowspan=2,
|
|
sticky="nsew", padx=(0, 5), pady=5)
|
|
|
|
# --- Tag/Gitignore Actions Section (Right part, vertical buttons) ---
|
|
tag_action_frame = self._create_tag_action_frame(parent_tab_frame)
|
|
# Span both rows to align vertically
|
|
tag_action_frame.grid(row=0, column=1, rowspan=2,
|
|
sticky="ns", padx=(5, 0), pady=5)
|
|
|
|
|
|
def _create_tag_management_frame(self, parent):
|
|
"""Creates the sub-frame containing the tag listbox."""
|
|
frame = ttk.LabelFrame(parent, text="Tags", padding=5)
|
|
# Configure internal grid for expansion
|
|
frame.rowconfigure(0, weight=1)
|
|
frame.columnconfigure(0, weight=1)
|
|
|
|
# Listbox for tags
|
|
self.tag_listbox = tk.Listbox(
|
|
frame,
|
|
height=8, # More visible rows for tags
|
|
exportselection=False,
|
|
selectmode=tk.SINGLE,
|
|
font=("Consolas", 9) # Monospaced font for alignment
|
|
)
|
|
self.tag_listbox.grid(row=0, column=0, sticky="nsew")
|
|
|
|
# Scrollbar for tag listbox
|
|
tag_scrollbar = ttk.Scrollbar(
|
|
frame,
|
|
orient=tk.VERTICAL,
|
|
command=self.tag_listbox.yview
|
|
)
|
|
tag_scrollbar.grid(row=0, column=1, sticky="ns")
|
|
self.tag_listbox.config(yscrollcommand=tag_scrollbar.set)
|
|
self.create_tooltip(self.tag_listbox,
|
|
"Tags (newest first) with messages. Select for actions.")
|
|
return frame
|
|
|
|
|
|
def _create_tag_action_frame(self, parent):
|
|
"""Creates the vertical frame for Tag and Gitignore action buttons."""
|
|
frame = ttk.Frame(parent) # Simple frame container
|
|
# Consistent button width for this column
|
|
button_width = 18
|
|
|
|
# Refresh Tags Button
|
|
self.refresh_tags_button = ttk.Button(
|
|
frame, text="Refresh Tags", width=button_width,
|
|
command=self.refresh_tags_callback, state=tk.DISABLED
|
|
)
|
|
self.refresh_tags_button.pack(side=tk.TOP, fill=tk.X, pady=(0, 3))
|
|
self.create_tooltip(self.refresh_tags_button, "Reload tag list.")
|
|
|
|
# Create Tag Button
|
|
self.create_tag_button = ttk.Button(
|
|
frame, text="Create Tag...", width=button_width,
|
|
command=self.create_tag_callback, state=tk.DISABLED
|
|
)
|
|
self.create_tag_button.pack(side=tk.TOP, fill=tk.X, pady=3)
|
|
self.create_tooltip(self.create_tag_button,
|
|
"Commit changes (if message provided) & create tag.")
|
|
|
|
# Checkout Tag Button
|
|
self.checkout_tag_button = ttk.Button(
|
|
frame, text="Checkout Selected Tag", width=button_width,
|
|
command=self.checkout_tag_callback, state=tk.DISABLED
|
|
)
|
|
self.checkout_tag_button.pack(side=tk.TOP, fill=tk.X, pady=3)
|
|
self.create_tooltip(self.checkout_tag_button,
|
|
"Switch to selected tag (Detached HEAD).")
|
|
|
|
# Delete Tag Button
|
|
self.delete_tag_button = ttk.Button(
|
|
frame, text="Delete Selected Tag", width=button_width,
|
|
command=self.delete_tag_callback, state=tk.DISABLED # Connect callback
|
|
)
|
|
self.delete_tag_button.pack(side=tk.TOP, fill=tk.X, pady=3)
|
|
self.create_tooltip(self.delete_tag_button, "Delete selected tag locally.")
|
|
|
|
# Edit .gitignore Button
|
|
self.edit_gitignore_button = ttk.Button(
|
|
frame, text="Edit .gitignore", width=button_width,
|
|
command=self.open_gitignore_editor_callback, state=tk.DISABLED
|
|
)
|
|
self.edit_gitignore_button.pack(side=tk.TOP, fill=tk.X, pady=(3, 0))
|
|
self.create_tooltip(self.edit_gitignore_button,
|
|
"Open editor for the .gitignore file.")
|
|
|
|
return frame
|
|
|
|
|
|
def _create_function_frame(self):
|
|
"""Creates the frame holding the Core Action buttons (below tabs)."""
|
|
self.function_frame = ttk.LabelFrame(
|
|
self, text="Core Actions", padding=(10, 10)
|
|
)
|
|
# Pack below notebook, but above log area
|
|
self.function_frame.pack(pady=(5, 5), fill="x", anchor=tk.N)
|
|
|
|
# Sub-frame to center the buttons horizontally
|
|
button_subframe = ttk.Frame(self.function_frame)
|
|
button_subframe.pack() # Default pack centers content
|
|
|
|
# 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)",
|
|
command=self.manual_backup_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 root window
|
|
# Pack at the very bottom, allow expansion
|
|
log_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True,
|
|
padx=10, pady=(0, 10)) # Padding only below
|
|
|
|
self.log_text = scrolledtext.ScrolledText(
|
|
log_frame, height=8, width=100, # Adjusted height
|
|
font=("Consolas", 9), wrap=tk.WORD, state=tk.DISABLED
|
|
)
|
|
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."""
|
|
try:
|
|
from config_manager import DEFAULT_PROFILE
|
|
except ImportError:
|
|
DEFAULT_PROFILE = "default" # Fallback
|
|
|
|
# Set dropdown value based on available profiles
|
|
if DEFAULT_PROFILE in self.initial_profile_sections:
|
|
self.profile_var.set(DEFAULT_PROFILE)
|
|
elif self.initial_profile_sections:
|
|
# Select first available profile if default not found
|
|
self.profile_var.set(self.initial_profile_sections[0])
|
|
# else: profile_var remains empty if no profiles exist
|
|
|
|
|
|
# --- ADDED: browse_folder method (moved from GitUtilityApp) ---
|
|
def browse_folder(self, entry_widget):
|
|
"""
|
|
Opens a folder selection dialog and updates the specified Entry widget.
|
|
"""
|
|
# Suggest initial directory
|
|
current_path = entry_widget.get()
|
|
initial_dir = current_path if os.path.isdir(current_path) else \
|
|
os.path.expanduser("~")
|
|
|
|
# Show dialog using tkinter's filedialog
|
|
directory = filedialog.askdirectory(
|
|
initialdir=initial_dir,
|
|
title="Select Directory",
|
|
parent=self.master # Make dialog modal
|
|
)
|
|
|
|
if directory: # If a directory was selected
|
|
# Update the entry widget
|
|
entry_widget.delete(0, tk.END)
|
|
entry_widget.insert(0, directory)
|
|
# Trigger controller's status update if SVN path changed
|
|
if entry_widget == self.svn_path_entry:
|
|
self.update_svn_status_callback(directory)
|
|
# else: User cancelled
|
|
|
|
|
|
# --- GUI Update Methods ---
|
|
def toggle_backup_dir(self):
|
|
"""Enables/disables backup directory widgets based on 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)
|
|
|
|
|
|
def browse_backup_dir(self):
|
|
"""Opens folder dialog specifically for the backup directory."""
|
|
# Use the local browse_folder method for consistency
|
|
if hasattr(self, 'backup_dir_entry'):
|
|
self.browse_folder(self.backup_dir_entry)
|
|
|
|
|
|
def update_svn_indicator(self, is_prepared):
|
|
"""Updates only the indicator color and Prepare button state."""
|
|
color = self.GREEN if is_prepared else self.RED
|
|
state = tk.DISABLED if is_prepared else tk.NORMAL
|
|
tip = "Repo Prepared" if is_prepared else "Repo Not Prepared"
|
|
|
|
# Update indicator's background and tooltip
|
|
if hasattr(self, 'svn_status_indicator'):
|
|
self.svn_status_indicator.config(background=color)
|
|
self.update_tooltip(self.svn_status_indicator, tip)
|
|
# Update Prepare button's state
|
|
if hasattr(self, 'prepare_svn_button'):
|
|
self.prepare_svn_button.config(state=state)
|
|
|
|
|
|
def update_profile_dropdown(self, sections):
|
|
"""Updates the profile combobox list."""
|
|
if hasattr(self, 'profile_dropdown'):
|
|
current = self.profile_var.get()
|
|
# Set new values for the dropdown
|
|
self.profile_dropdown['values'] = sections
|
|
# Maintain selection logic
|
|
if sections:
|
|
if current in sections:
|
|
# Setting same value might not trigger trace, but is correct state
|
|
self.profile_var.set(current)
|
|
elif "default" in sections:
|
|
self.profile_var.set("default") # Triggers load
|
|
else:
|
|
self.profile_var.set(sections[0]) # Triggers load
|
|
else:
|
|
self.profile_var.set("") # Triggers load with empty
|
|
|
|
|
|
def update_tag_list(self, tags_with_subjects):
|
|
"""Clears and repopulates tag listbox with name and subject."""
|
|
if not hasattr(self, 'tag_listbox'):
|
|
# Log error if listbox doesn't exist when called
|
|
logging.error("Cannot update tag list: Listbox widget not found.")
|
|
return
|
|
try:
|
|
self.tag_listbox.delete(0, tk.END) # Clear list
|
|
if tags_with_subjects:
|
|
# Reset text color if it was previously greyed out
|
|
try:
|
|
current_fg = self.tag_listbox.cget("fg")
|
|
if current_fg == "grey":
|
|
# Use standard system text color name
|
|
self.tag_listbox.config(fg='SystemWindowText')
|
|
except tk.TclError:
|
|
# Fallback if SystemWindowText is unknown
|
|
try:
|
|
self.tag_listbox.config(fg='black')
|
|
except tk.TclError:
|
|
pass # Ignore color setting errors if all fails
|
|
|
|
# Insert formatted tag strings
|
|
for name, subject in tags_with_subjects:
|
|
# Use tab separation for basic alignment
|
|
display = f"{name}\t({subject})"
|
|
self.tag_listbox.insert(tk.END, display)
|
|
else:
|
|
# Show placeholder text if no tags
|
|
self.tag_listbox.insert(tk.END, "(No tags found)")
|
|
try:
|
|
self.tag_listbox.config(fg="grey") # Dim placeholder text
|
|
except tk.TclError:
|
|
pass # Ignore color setting errors
|
|
|
|
except tk.TclError as e:
|
|
logging.error(f"TclError updating tag listbox: {e}")
|
|
except Exception as e:
|
|
logging.error(f"Error updating tag listbox: {e}", exc_info=True)
|
|
|
|
|
|
def get_selected_tag(self):
|
|
"""Returns the name only of the selected tag."""
|
|
tag_name = None
|
|
if hasattr(self, 'tag_listbox'):
|
|
indices = self.tag_listbox.curselection()
|
|
# Check if there is a selection (curselection returns tuple)
|
|
if indices:
|
|
selected_index = indices[0] # Get the index
|
|
item = self.tag_listbox.get(selected_index) # Get text at index
|
|
# Ignore placeholder text
|
|
if item != "(No tags found)":
|
|
# Extract name (text before the first tab)
|
|
tag_name = item.split('\t', 1)[0]
|
|
tag_name = tag_name.strip() # Remove any extra spaces
|
|
return tag_name # Return name or None
|
|
|
|
|
|
def update_branch_list(self, branches):
|
|
"""Clears and repopulates the branch listbox."""
|
|
if not hasattr(self, 'branch_listbox'):
|
|
logging.error("Branch listbox missing for update.")
|
|
return
|
|
try:
|
|
current = self.current_branch_var.get() # Get displayed current branch
|
|
self.branch_listbox.delete(0, tk.END) # Clear list
|
|
if branches:
|
|
# Reset color if needed
|
|
try:
|
|
if self.branch_listbox.cget("fg") == "grey":
|
|
self.branch_listbox.config(fg='SystemWindowText')
|
|
except tk.TclError: pass
|
|
# Insert branches, highlight current
|
|
for branch in branches:
|
|
is_current = (branch == current)
|
|
# Add '*' prefix for current branch display
|
|
display_name = f"* {branch}" if is_current else f" {branch}"
|
|
self.branch_listbox.insert(tk.END, display_name)
|
|
# Apply styling for current branch (if needed)
|
|
if is_current:
|
|
self.branch_listbox.itemconfig(
|
|
tk.END, {'fg': 'blue', 'selectbackground': 'lightblue'}
|
|
)
|
|
else:
|
|
# Show placeholder if no branches
|
|
self.branch_listbox.insert(tk.END, "(No local branches?)")
|
|
try:
|
|
self.branch_listbox.config(fg="grey") # Dim placeholder
|
|
except tk.TclError: pass
|
|
except tk.TclError as e:
|
|
logging.error(f"TclError updating branches: {e}")
|
|
except Exception as e:
|
|
logging.error(f"Error updating branches: {e}", exc_info=True)
|
|
|
|
|
|
def get_selected_branch(self):
|
|
"""Returns the name only of the selected branch."""
|
|
branch_name = None
|
|
if hasattr(self, 'branch_listbox'):
|
|
indices = self.branch_listbox.curselection()
|
|
if indices:
|
|
item = self.branch_listbox.get(indices[0])
|
|
# Remove potential '*' prefix and leading/trailing whitespace
|
|
branch_name = item.lstrip("* ").strip()
|
|
return branch_name # Return name or None
|
|
|
|
|
|
def set_current_branch_display(self, branch_name):
|
|
"""Updates the label showing the current branch."""
|
|
if hasattr(self, 'current_branch_var'):
|
|
# Set display text, handling None or empty string
|
|
display_text = branch_name if branch_name else "(DETACHED or N/A)"
|
|
self.current_branch_var.set(display_text)
|
|
|
|
|
|
# --- Dialog Wrappers ---
|
|
def ask_new_profile_name(self):
|
|
"""Asks the user for a new profile name."""
|
|
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 Helpers ---
|
|
def create_tooltip(self, widget, text):
|
|
"""Creates a tooltip for a given widget."""
|
|
tooltip = Tooltip(widget, text)
|
|
# Use add='+' to ensure other bindings are not overwritten
|
|
widget.bind("<Enter>", lambda e, tt=tooltip: tt.showtip(), add='+')
|
|
widget.bind("<Leave>", lambda e, tt=tooltip: tt.hidetip(), add='+')
|
|
# Hide tooltip also when clicking the widget
|
|
widget.bind("<ButtonPress>", lambda e, tt=tooltip: tt.hidetip(), add='+')
|
|
|
|
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>")
|
|
# Re-create the tooltip with the new text
|
|
self.create_tooltip(widget, text) |