# --- FILE: gitsync_tool/gui/dialogs.py --- import tkinter as tk from tkinter import ttk, messagebox, simpledialog, filedialog import os import re # Per validazione nomi branch/tag from typing import Optional, Tuple # --- Tooltip (non necessario qui, i dialoghi sono semplici) --- # --- Costanti (non necessarie qui) --- class CreateTagDialog(simpledialog.Dialog): """ Dialog to get tag name and message from the user for creating an annotated Git tag. """ def __init__( self, parent, title: str = "Create New Tag", suggested_tag_name: str = "" ): """ Initialize the dialog. Args: parent: The parent window. title (str): The title for the dialog window. suggested_tag_name (str): A pre-filled suggestion for the tag name. """ self.tag_name_var = tk.StringVar() self.tag_message_var = tk.StringVar() # self.result will store the tuple (tag_name, tag_message) on success self.result: Optional[Tuple[str, str]] = None self.suggested_tag_name: str = suggested_tag_name # Call Dialog's __init__ AFTER setting up variables needed by body() super().__init__(parent, title=title) # Override def body(self, master: tk.Frame) -> Optional[tk.Widget]: """Creates the dialog body with entry fields.""" # Use a ttk.Frame for better padding and layout control frame = ttk.Frame(master, padding="10") frame.pack(fill="x", expand=True) frame.columnconfigure(1, weight=1) # Make the entry column expandable # Tag Name Row ttk.Label(frame, text="Tag Name:").grid( row=0, column=0, padx=5, pady=5, sticky="w" ) self.name_entry = ttk.Entry(frame, textvariable=self.tag_name_var, width=40) self.name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") # Pre-fill suggested name if provided if self.suggested_tag_name: self.tag_name_var.set(self.suggested_tag_name) # Tag Message Row ttk.Label(frame, text="Tag Message:").grid( row=1, column=0, padx=5, pady=5, sticky="w" ) self.message_entry = ttk.Entry( frame, textvariable=self.tag_message_var, width=40 ) self.message_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew") # Return the widget that should have initial focus return self.name_entry # Override def validate(self) -> bool: """Validates the input before closing the dialog.""" name = self.tag_name_var.get().strip() msg = self.tag_message_var.get().strip() if not name: messagebox.showwarning( "Input Error", "Tag name cannot be empty.", parent=self ) self.name_entry.focus_set() # Focus back to name entry return False # Validation failed # Basic Git tag name validation (adjust regex if needed) # Avoids common invalid characters and patterns like '..', '.git', ending ., / # Ref: https://git-scm.com/docs/git-check-ref-format pattern = r"^(?!\.|/|.*@\{|.*\\)(?!.*\.lock$)(?!.*\.\.$)[^ \t\n\r\f\v~^:?*\[]+(? None: """Processes the validated data and stores it in self.result.""" # Store the cleaned data as a tuple self.result = ( self.tag_name_var.get().strip(), self.tag_message_var.get().strip(), ) class CreateBranchDialog(simpledialog.Dialog): """Dialog to get a new branch name from the user.""" def __init__(self, parent, title: str = "Create New Branch"): """Initialize the dialog.""" self.branch_name_var = tk.StringVar() self.result: Optional[str] = None super().__init__(parent, title=title) # Override def body(self, master: tk.Frame) -> Optional[tk.Widget]: """Creates the dialog body.""" frame = ttk.Frame(master, padding="10") frame.pack(fill="x", expand=True) frame.columnconfigure(1, weight=1) ttk.Label(frame, text="New Branch Name:").grid( row=0, column=0, padx=5, pady=10, sticky="w" ) self.name_entry = ttk.Entry(frame, textvariable=self.branch_name_var, width=40) self.name_entry.grid(row=0, column=1, padx=5, pady=10, sticky="ew") return self.name_entry # Initial focus # Override def validate(self) -> bool: """Validates 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 ) self.name_entry.focus_set() return False # Git branch name validation (similar to tags but allows '/') # Ref: https://git-scm.com/docs/git-check-ref-format # Cannot start/end with '/', contain '..', spaces, control chars, ~^:?*[\ ] # Cannot be exactly 'HEAD'. pattern = r"^(?!\.|/)(?!.*@\{|.*\\)(?!.*\.lock$)(?!.*\.\.)(?!.*/$)[^ \t\n\r\f\v~^:?*\[\\]+$" if name.lower() == "head" or not re.match(pattern, name): messagebox.showwarning( "Input Error", f"Invalid branch name: '{name}'.\n" f"Check for spaces, invalid characters (~^:?*[]\\), consecutive dots, " f"starting/ending slashes or dots, '.lock' suffix, or using 'HEAD'.", parent=self, ) self.name_entry.focus_set() return False return True # Override def apply(self) -> None: """Stores the validated branch name.""" self.result = self.branch_name_var.get().strip() class CloneFromRemoteDialog(simpledialog.Dialog): """Dialog to get Remote URL and Local Parent Directory for cloning.""" def __init__(self, parent, title: str = "Clone Remote Repository"): """Initialize the dialog.""" self.remote_url_var = tk.StringVar() self.local_parent_dir_var = tk.StringVar() self.profile_name_var = tk.StringVar() # Optional profile name # self.result will store (url, parent_dir, profile_name_input) self.result: Optional[Tuple[str, str, str]] = None # Set suggested initial directory for the local folder # Default to user's home directory self.local_parent_dir_var.set(os.path.expanduser("~")) super().__init__(parent, title=title) # Override def body(self, master: tk.Frame) -> Optional[tk.Widget]: """Creates the dialog body.""" main_frame = ttk.Frame(master, padding="10") main_frame.pack(fill="both", expand=True) main_frame.columnconfigure(1, weight=1) # Entry column expands row_idx: int = 0 # Remote URL Row ttk.Label(main_frame, text="Remote Repository URL:").grid( row=row_idx, column=0, padx=5, pady=5, sticky="w" ) self.url_entry = ttk.Entry( main_frame, textvariable=self.remote_url_var, width=60 ) self.url_entry.grid( row=row_idx, column=1, columnspan=2, padx=5, pady=5, sticky="ew" ) # Consider adding Tooltip here if Tooltip class is imported row_idx += 1 # Local Parent Directory Row ttk.Label(main_frame, text="Clone into Directory:").grid( row=row_idx, column=0, padx=5, pady=5, sticky="w" ) self.dir_entry = ttk.Entry( main_frame, textvariable=self.local_parent_dir_var, width=60 ) self.dir_entry.grid(row=row_idx, column=1, padx=5, pady=5, sticky="ew") self.browse_button = ttk.Button( main_frame, text="Browse...", width=9, command=self._browse_local_dir ) self.browse_button.grid(row=row_idx, column=2, padx=(0, 5), pady=5, sticky="w") row_idx += 1 # Explanatory Label Row ttk.Label( main_frame, text="(A new sub-folder named after the repository will be created inside this directory)", font=("Segoe UI", 8), # Smaller font foreground="grey", ).grid(row=row_idx, column=1, columnspan=2, padx=5, pady=(0, 5), sticky="w") row_idx += 1 # New Profile Name Row (Optional) ttk.Label(main_frame, text="New Profile Name (Optional):").grid( row=row_idx, column=0, padx=5, pady=5, sticky="w" ) self.profile_entry = ttk.Entry( main_frame, textvariable=self.profile_name_var, width=60 ) self.profile_entry.grid( row=row_idx, column=1, columnspan=2, padx=5, pady=5, sticky="ew" ) # Consider adding Tooltip here row_idx += 1 return self.url_entry # Initial focus def _browse_local_dir(self) -> None: """Callback for the local directory browse button.""" current_path: str = self.local_parent_dir_var.get() initial_dir: str = ( current_path if os.path.isdir(current_path) else os.path.expanduser("~") ) directory: Optional[str] = filedialog.askdirectory( initialdir=initial_dir, title="Select Parent Directory for Clone", parent=self, # Modal to this dialog ) if directory: self.local_parent_dir_var.set(directory) # Override def validate(self) -> bool: """Validates the input fields before closing.""" url: str = self.remote_url_var.get().strip() parent_dir: str = self.local_parent_dir_var.get().strip() profile_name: str = self.profile_name_var.get().strip() if not url: messagebox.showwarning( "Input Error", "Remote Repository URL cannot be empty.", parent=self ) self.url_entry.focus_set() return False # Basic URL check (not exhaustive) if not ( url.startswith("http://") or url.startswith("https://") or url.startswith("ssh://") or ("@" in url and ":" in url) # Heuristic for git@server:path ): if not messagebox.askokcancel( "URL Format Warning", f"The URL '{url}' does not look like a standard HTTPS, HTTP, or SSH URL.\n\nProceed anyway?", icon=messagebox.WARNING, # Use constant parent=self, ): self.url_entry.focus_set() return False if not parent_dir: messagebox.showwarning( "Input Error", "Parent Local Directory cannot be empty.", parent=self ) self.dir_entry.focus_set() return False if not os.path.isdir(parent_dir): messagebox.showwarning( "Input Error", f"The selected parent directory does not exist:\n{parent_dir}", parent=self, ) self.dir_entry.focus_set() return False # Optional profile name validation could be added here if needed # e.g., check for invalid characters for profile names return True # Validation successful # Override def apply(self) -> None: """Stores the validated result.""" # Return a tuple with the cleaned values self.result = ( self.remote_url_var.get().strip(), self.local_parent_dir_var.get().strip(), self.profile_name_var.get().strip(), # Return empty string if not provided ) # --- END OF FILE gitsync_tool/gui/dialogs.py ---