SXXXXXXX_GitUtility/gitutility/gui/dialogs.py

467 lines
17 KiB
Python

# --- 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, List
# --- 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~^:?*\[]+(?<!\.)$"
if not re.match(pattern, name):
messagebox.showwarning(
"Input Error",
f"Invalid tag name format: '{name}'.\n"
f"Tags cannot contain spaces, control chars, or standard invalid path chars (~^:?*[]\\), "
f"and cannot start/end with '.' or '/', or contain '..', '.lock', '@{{'.",
parent=self,
)
self.name_entry.focus_set()
return False
if not msg:
messagebox.showwarning(
"Input Error",
"Tag message cannot be empty (for annotated tag).",
parent=self,
)
self.message_entry.focus_set() # Focus back to message entry
return False # Validation failed
return True # Validation successful
# Override
def apply(self) -> 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
)
class SelectSubmoduleDialog(simpledialog.Dialog):
"""Dialog to select a submodule from a list for cleaning."""
def __init__(
self,
parent,
title: str = "Select Submodule to Clean",
submodule_paths: List[str] = None,
):
self.submodule_paths = submodule_paths if submodule_paths else []
self.result: Optional[str] = None
self.selected_path_var = tk.StringVar()
super().__init__(parent, title=title)
def body(self, master: tk.Frame) -> Optional[tk.Widget]:
frame = ttk.Frame(master, padding="10")
frame.pack(fill="both", expand=True)
ttk.Label(
frame,
text="Select the submodule with an inconsistent state to clean from the repository:",
).pack(pady=(0, 10))
if not self.submodule_paths:
ttk.Label(
frame,
text="No registered submodules found in .gitmodules.",
foreground="red",
).pack()
return None # No items to select
self.combobox = ttk.Combobox(
frame,
textvariable=self.selected_path_var,
values=self.submodule_paths,
state="readonly",
width=50,
)
self.combobox.pack(pady=5)
if self.submodule_paths:
self.combobox.current(0) # Pre-select the first item
return self.combobox
def validate(self) -> bool:
if not self.selected_path_var.get():
messagebox.showwarning(
"Selection Required",
"You must select a submodule path from the list.",
parent=self,
)
return False
return True
def apply(self) -> None:
self.result = self.selected_path_var.get()
class AddSubmoduleDialog(simpledialog.Dialog):
"""Dialog to get a new submodule's URL and local path."""
def __init__(self, parent, title: str = "Add New Submodule"):
self.url_var = tk.StringVar()
self.path_var = tk.StringVar()
self.result: Optional[Tuple[str, str]] = None
super().__init__(parent, title=title)
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)
# Repository URL Row
ttk.Label(frame, text="Repository URL:").grid(
row=0, column=0, padx=5, pady=5, sticky="w"
)
self.url_entry = ttk.Entry(
frame, textvariable=self.url_var, width=60
) # <-- Larghezza aumentata
self.url_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
# Local Path Row
ttk.Label(frame, text="Local Path:").grid(
row=1, column=0, padx=5, pady=5, sticky="w"
)
self.path_entry = ttk.Entry(
frame, textvariable=self.path_var, width=60
) # <-- Larghezza aumentata
self.path_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
ttk.Label(
frame,
text="(e.g., libs/my-submodule)",
font=("Segoe UI", 8),
foreground="grey",
).grid(row=2, column=1, padx=5, sticky="w")
return self.url_entry # Initial focus
def validate(self) -> bool:
"""Validates the input."""
url = self.url_var.get().strip()
path = self.path_var.get().strip()
if not url:
messagebox.showwarning(
"Input Error", "Repository URL cannot be empty.", parent=self
)
self.url_entry.focus_set()
return False
if not path:
messagebox.showwarning(
"Input Error", "Local Path cannot be empty.", parent=self
)
self.path_entry.focus_set()
return False
# Basic check for invalid path characters
if any(char in path for char in ["<", ">", ":", '"', "|", "?", "*"]):
messagebox.showwarning(
"Input Error",
f"Local path '{path}' contains invalid characters.",
parent=self,
)
self.path_entry.focus_set()
return False
return True
def apply(self) -> None:
"""Stores the validated result."""
self.result = (
self.url_var.get().strip(),
self.path_var.get().strip().replace("\\", "/"), # Normalize path separators
)
# --- END OF FILE gitsync_tool/gui/dialogs.py ---