312 lines
13 KiB
Python
312 lines
13 KiB
Python
# --- FILE: gitsync_tool/gui/editors.py ---
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, scrolledtext
|
|
import os
|
|
from typing import Optional, Callable
|
|
|
|
# Usa import assoluto per il log handler
|
|
from gitutility.logging_setup import log_handler
|
|
|
|
|
|
class GitignoreEditorWindow(tk.Toplevel):
|
|
"""
|
|
Toplevel window for editing the .gitignore file.
|
|
Provides basic text editing capabilities and save/cancel options.
|
|
Calls a callback function upon successful save.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
master: tk.Misc,
|
|
gitignore_path: str,
|
|
on_save_success_callback: Optional[Callable[[], None]] = None,
|
|
):
|
|
"""
|
|
Initialize the Gitignore Editor window.
|
|
|
|
Args:
|
|
master: The parent window.
|
|
gitignore_path (str): The absolute path to the .gitignore file.
|
|
on_save_success_callback (Optional[Callable]): A function to call
|
|
after successfully saving.
|
|
"""
|
|
super().__init__(master)
|
|
self.gitignore_path: str = gitignore_path
|
|
self.original_content: str = ""
|
|
self.on_save_success_callback: Optional[Callable[[], None]] = (
|
|
on_save_success_callback
|
|
)
|
|
|
|
self.title(f"Edit {os.path.basename(gitignore_path)}")
|
|
self.geometry("600x450")
|
|
self.minsize(400, 300)
|
|
|
|
# Make the window modal
|
|
self.grab_set()
|
|
self.transient(master)
|
|
# Handle window close button (X)
|
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
|
|
# --- Create Widgets ---
|
|
# Main frame
|
|
main_frame = ttk.Frame(self, padding="10")
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
main_frame.rowconfigure(0, weight=1) # Text editor area expands
|
|
main_frame.columnconfigure(0, weight=1) # Text editor area expands
|
|
|
|
# ScrolledText Editor
|
|
self.text_editor = scrolledtext.ScrolledText(
|
|
main_frame,
|
|
wrap=tk.WORD,
|
|
font=("Consolas", 10), # Monospaced font is good for code/config
|
|
undo=True, # Enable basic undo/redo
|
|
padx=5,
|
|
pady=5,
|
|
borderwidth=1,
|
|
relief=tk.SUNKEN,
|
|
)
|
|
self.text_editor.grid(row=0, column=0, sticky="nsew", pady=(0, 10))
|
|
|
|
# Button Frame (aligned to the right)
|
|
button_frame = ttk.Frame(main_frame)
|
|
button_frame.grid(row=1, column=0, sticky="e") # Use sticky="e"
|
|
|
|
self.save_button = ttk.Button(
|
|
button_frame,
|
|
text="Save and Close",
|
|
command=self._save_and_close,
|
|
# Consider adding tooltip if Tooltip class is imported/available
|
|
)
|
|
self.save_button.pack(side=tk.RIGHT, padx=5) # Pack buttons from right to left
|
|
|
|
self.cancel_button = ttk.Button(
|
|
button_frame,
|
|
text="Cancel",
|
|
command=self._on_close,
|
|
# Consider adding tooltip
|
|
)
|
|
self.cancel_button.pack(side=tk.RIGHT) # Pack next to save button
|
|
|
|
# Load initial content and center window
|
|
self._load_file()
|
|
self._center_window(master)
|
|
|
|
# Wait for this window to be closed before returning control
|
|
# This makes it behave like a modal dialog
|
|
self.wait_window()
|
|
|
|
def _center_window(self, parent: tk.Misc) -> None:
|
|
"""Centers the Toplevel window relative to its parent."""
|
|
func_name = "_center_window (GitignoreEditor)"
|
|
try:
|
|
self.update_idletasks() # Ensure window dimensions are calculated
|
|
# Get parent geometry
|
|
parent_x: int = parent.winfo_rootx()
|
|
parent_y: int = parent.winfo_rooty()
|
|
parent_w: int = parent.winfo_width()
|
|
parent_h: int = parent.winfo_height()
|
|
# Get self geometry
|
|
win_w: int = self.winfo_width()
|
|
win_h: int = self.winfo_height()
|
|
# Calculate position
|
|
pos_x: int = parent_x + (parent_w // 2) - (win_w // 2)
|
|
pos_y: int = parent_y + (parent_h // 2) - (win_h // 2)
|
|
# Keep window on screen (simple check)
|
|
screen_w: int = self.winfo_screenwidth()
|
|
screen_h: int = self.winfo_screenheight()
|
|
pos_x = max(0, min(pos_x, screen_w - win_w))
|
|
pos_y = max(0, min(pos_y, screen_h - win_h))
|
|
# Set geometry
|
|
self.geometry(f"+{int(pos_x)}+{int(pos_y)}")
|
|
except Exception as e:
|
|
log_handler.log_error(
|
|
f"Could not center GitignoreEditorWindow: {e}", func_name=func_name
|
|
)
|
|
|
|
def _load_file(self) -> None:
|
|
"""Loads the content of the .gitignore file into the text editor."""
|
|
func_name = "_load_file (GitignoreEditor)"
|
|
log_handler.log_info(
|
|
f"Loading gitignore file: {self.gitignore_path}", func_name=func_name
|
|
)
|
|
content: str = ""
|
|
try:
|
|
# Check if the file exists before trying to read
|
|
if os.path.exists(self.gitignore_path):
|
|
# Open with UTF-8, replace errors if any decoding issues occur
|
|
with open(
|
|
self.gitignore_path, "r", encoding="utf-8", errors="replace"
|
|
) as f:
|
|
content = f.read()
|
|
log_handler.log_debug(
|
|
f"Read {len(content)} chars from existing .gitignore.",
|
|
func_name=func_name,
|
|
)
|
|
else:
|
|
# File doesn't exist, treat as empty content
|
|
log_handler.log_info(
|
|
f".gitignore does not exist at path: {self.gitignore_path}. Starting with empty editor.",
|
|
func_name=func_name,
|
|
)
|
|
content = (
|
|
"# Enter patterns for files/directories to ignore (one per line)\n"
|
|
)
|
|
content += "# Lines starting with # are comments.\n"
|
|
content += "# Example: build/, *.log, __pycache__/\n"
|
|
|
|
# Store original content for change detection
|
|
self.original_content = content
|
|
|
|
# Populate the text editor widget
|
|
self.text_editor.config(state=tk.NORMAL) # Ensure editable
|
|
self.text_editor.delete("1.0", tk.END) # Clear existing content
|
|
self.text_editor.insert(tk.END, content) # Insert loaded content
|
|
self.text_editor.edit_reset() # Reset undo/redo stack
|
|
self.text_editor.focus_set() # Set focus to the editor
|
|
|
|
except IOError as e:
|
|
log_handler.log_error(
|
|
f"I/O error loading .gitignore file '{self.gitignore_path}': {e}",
|
|
func_name=func_name,
|
|
)
|
|
messagebox.showerror(
|
|
"Load Error", f"Error reading .gitignore:\n{e}", parent=self
|
|
)
|
|
# Disable editor if loading fails critically
|
|
self.text_editor.config(state=tk.DISABLED)
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Unexpected error loading .gitignore file '{self.gitignore_path}': {e}",
|
|
func_name=func_name,
|
|
)
|
|
messagebox.showerror(
|
|
"Load Error", f"Unexpected error loading file:\n{e}", parent=self
|
|
)
|
|
self.text_editor.config(state=tk.DISABLED)
|
|
|
|
def _has_changes(self) -> bool:
|
|
"""Checks if the editor content differs from the originally loaded content."""
|
|
try:
|
|
# Get current content, excluding the trailing newline Tk adds
|
|
current_content: str = self.text_editor.get("1.0", "end-1c")
|
|
# Compare with the stored original content
|
|
return current_content != self.original_content
|
|
except Exception as e:
|
|
# Assume changes if error occurs during check
|
|
log_handler.log_error(
|
|
f"Error checking for changes in editor: {e}", func_name="_has_changes"
|
|
)
|
|
return True
|
|
|
|
def _save_file(self) -> bool:
|
|
"""Saves the current editor content to the .gitignore file."""
|
|
func_name = "_save_file (GitignoreEditor)"
|
|
|
|
# Avoid saving if there are no actual changes
|
|
if not self._has_changes():
|
|
log_handler.log_info(
|
|
"No changes detected in .gitignore, skipping save.", func_name=func_name
|
|
)
|
|
return True # Consider "no changes" as a successful save action
|
|
|
|
# Get current content from the editor
|
|
current_content: str = self.text_editor.get("1.0", "end-1c")
|
|
log_handler.log_info(
|
|
f"Saving changes to .gitignore file: {self.gitignore_path}",
|
|
func_name=func_name,
|
|
)
|
|
|
|
try:
|
|
# Write content using UTF-8 and ensure consistent newline handling
|
|
with open(self.gitignore_path, "w", encoding="utf-8", newline="\n") as f:
|
|
f.write(current_content)
|
|
log_handler.log_info(".gitignore saved successfully.", func_name=func_name)
|
|
|
|
# Update original content state and editor's modified flag
|
|
self.original_content = current_content
|
|
self.text_editor.edit_modified(False) # Mark as not modified since save
|
|
return True # Indicate success
|
|
except IOError as e:
|
|
log_handler.log_error(
|
|
f"I/O error saving .gitignore file '{self.gitignore_path}': {e}",
|
|
func_name=func_name,
|
|
)
|
|
messagebox.showerror(
|
|
"Save Error", f"Error writing .gitignore:\n{e}", parent=self
|
|
)
|
|
return False # Indicate failure
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Unexpected error saving .gitignore file '{self.gitignore_path}': {e}",
|
|
func_name=func_name,
|
|
)
|
|
messagebox.showerror(
|
|
"Save Error", f"Unexpected error saving file:\n{e}", parent=self
|
|
)
|
|
return False # Indicate failure
|
|
|
|
def _save_and_close(self) -> None:
|
|
"""Saves the file and closes the window, calling callback on success."""
|
|
func_name = "_save_and_close (GitignoreEditor)"
|
|
save_successful: bool = self._save_file()
|
|
|
|
if save_successful:
|
|
log_handler.log_debug(
|
|
"Save successful, checking and calling callback if available.",
|
|
func_name=func_name,
|
|
)
|
|
# Call the success callback if provided and callable
|
|
if self.on_save_success_callback and callable(
|
|
self.on_save_success_callback
|
|
):
|
|
try:
|
|
log_handler.log_info(
|
|
"Executing on_save_success_callback.", func_name=func_name
|
|
)
|
|
self.on_save_success_callback()
|
|
except Exception as cb_e:
|
|
log_handler.log_exception(
|
|
f"Error occurred in on_save_success_callback: {cb_e}",
|
|
func_name=func_name,
|
|
)
|
|
# Inform user about the callback error, but proceed with closing
|
|
messagebox.showwarning(
|
|
"Callback Error",
|
|
"File saved successfully, but an error occurred during the post-save action.\nPlease check the application logs.",
|
|
parent=self,
|
|
)
|
|
# Close the editor window after successful save (and callback attempt)
|
|
self.destroy()
|
|
|
|
def _on_close(self) -> None:
|
|
"""Handles window close event (X button or Cancel button)."""
|
|
func_name = "_on_close (GitignoreEditor)"
|
|
# Check for unsaved changes before closing
|
|
if self._has_changes():
|
|
# Ask user whether to save, discard, or cancel closing
|
|
response = messagebox.askyesnocancel(
|
|
"Unsaved Changes",
|
|
"There are unsaved changes in the editor.\nDo you want to save them before closing?",
|
|
parent=self, # Ensure message box is modal to this window
|
|
)
|
|
if response is True:
|
|
# User chose Yes: attempt save and close
|
|
self._save_and_close()
|
|
elif response is False:
|
|
# User chose No: discard changes and close
|
|
log_handler.log_warning(
|
|
"Discarding unsaved .gitignore changes.", func_name=func_name
|
|
)
|
|
self.destroy()
|
|
# else (response is None): User chose Cancel - do nothing, keep editor open
|
|
else:
|
|
# No changes: simply close the window
|
|
log_handler.log_debug(
|
|
"No changes detected, closing editor.", func_name=func_name
|
|
)
|
|
self.destroy()
|
|
|
|
|
|
# --- END OF FILE gitsync_tool/gui/editors.py ---
|