SXXXXXXX_GitUtility/gitsync_tool/gui/editors.py
2025-05-05 10:11:21 +02:00

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 gitsync_tool.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 ---