""" Pathology management window for adding, editing, and removing pathologies. """ import tkinter as tk from tkinter import messagebox, ttk from pathology_manager import Pathology, PathologyManager class PathologyManagementWindow: """Window for managing pathology configurations.""" def __init__( self, parent: tk.Tk, pathology_manager: PathologyManager, refresh_callback ): self.parent = parent self.pathology_manager = pathology_manager self.refresh_callback = refresh_callback # Create the window self.window = tk.Toplevel(parent) self.window.title("Manage Pathologies") self.window.geometry("800x500") self.window.resizable(True, True) # Make window modal self.window.transient(parent) self.window.grab_set() self._setup_ui() self._populate_pathology_list() # Center window self.window.update_idletasks() x = (self.window.winfo_screenwidth() // 2) - (800 // 2) y = (self.window.winfo_screenheight() // 2) - (500 // 2) self.window.geometry(f"800x500+{x}+{y}") def _setup_ui(self): """Set up the UI components.""" # Main frame main_frame = ttk.Frame(self.window, padding="10") main_frame.grid(row=0, column=0, sticky="nsew") self.window.grid_rowconfigure(0, weight=1) self.window.grid_columnconfigure(0, weight=1) # Pathology list list_frame = ttk.LabelFrame(main_frame, text="Pathologies", padding="5") list_frame.grid(row=0, column=0, sticky="nsew", pady=(0, 10)) main_frame.grid_rowconfigure(0, weight=1) main_frame.grid_columnconfigure(0, weight=1) # Treeview for pathology list columns = ( "Key", "Display Name", "Scale Info", "Color", "Default Enabled", "Scale Range", ) self.tree = ttk.Treeview(list_frame, columns=columns, show="headings") # Configure columns self.tree.heading("Key", text="Key") self.tree.heading("Display Name", text="Display Name") self.tree.heading("Scale Info", text="Scale Info") self.tree.heading("Color", text="Color") self.tree.heading("Default Enabled", text="Default Enabled") self.tree.heading("Scale Range", text="Scale Range") self.tree.column("Key", width=120) self.tree.column("Display Name", width=150) self.tree.column("Scale Info", width=150) self.tree.column("Color", width=80) self.tree.column("Default Enabled", width=100) self.tree.column("Scale Range", width=100) # Scrollbar for treeview scrollbar = ttk.Scrollbar( list_frame, orient="vertical", command=self.tree.yview ) self.tree.configure(yscrollcommand=scrollbar.set) self.tree.grid(row=0, column=0, sticky="nsew") scrollbar.grid(row=0, column=1, sticky="ns") list_frame.grid_rowconfigure(0, weight=1) list_frame.grid_columnconfigure(0, weight=1) # Buttons frame button_frame = ttk.Frame(main_frame) button_frame.grid(row=1, column=0, sticky="ew") ttk.Button( button_frame, text="Add Pathology", command=self._add_pathology ).pack(side="left", padx=(0, 5)) ttk.Button( button_frame, text="Edit Pathology", command=self._edit_pathology ).pack(side="left", padx=(0, 5)) ttk.Button( button_frame, text="Remove Pathology", command=self._remove_pathology ).pack(side="left", padx=(0, 5)) ttk.Button(button_frame, text="Close", command=self.window.destroy).pack( side="right" ) def _populate_pathology_list(self): """Populate the pathology list.""" # Clear existing items for item in self.tree.get_children(): self.tree.delete(item) # Add pathologies for pathology in self.pathology_manager.get_all_pathologies().values(): scale_range = f"{pathology.scale_min}-{pathology.scale_max}" self.tree.insert( "", "end", values=( pathology.key, pathology.display_name, pathology.scale_info, pathology.color, "Yes" if pathology.default_enabled else "No", scale_range, ), ) def _add_pathology(self): """Add a new pathology.""" PathologyEditDialog( self.window, self.pathology_manager, None, self._on_pathology_changed ) def _edit_pathology(self): """Edit selected pathology.""" selection = self.tree.selection() if not selection: messagebox.showwarning("No Selection", "Please select a pathology to edit.") return item = self.tree.item(selection[0]) pathology_key = item["values"][0] pathology = self.pathology_manager.get_pathology(pathology_key) if pathology: PathologyEditDialog( self.window, self.pathology_manager, pathology, self._on_pathology_changed, ) def _remove_pathology(self): """Remove selected pathology.""" selection = self.tree.selection() if not selection: messagebox.showwarning( "No Selection", "Please select a pathology to remove." ) return item = self.tree.item(selection[0]) pathology_key = item["values"][0] pathology_name = item["values"][1] if messagebox.askyesno( "Confirm Removal", f"Are you sure you want to remove '{pathology_name}'?\n\n" "This will also remove all associated data from your records!", ): if self.pathology_manager.remove_pathology(pathology_key): messagebox.showinfo( "Success", f"'{pathology_name}' removed successfully!" ) self._populate_pathology_list() self._refresh_main_app() else: messagebox.showerror("Error", f"Failed to remove '{pathology_name}'.") def _on_pathology_changed(self): """Handle pathology changes.""" self._populate_pathology_list() self._refresh_main_app() def _refresh_main_app(self): """Refresh the main application.""" if self.refresh_callback: self.refresh_callback() class PathologyEditDialog: """Dialog for adding/editing a pathology.""" def __init__( self, parent: tk.Toplevel, pathology_manager: PathologyManager, pathology: Pathology | None, callback, ): self.parent = parent self.pathology_manager = pathology_manager self.pathology = pathology self.callback = callback self.is_edit = pathology is not None # Create dialog self.dialog = tk.Toplevel(parent) self.dialog.title("Edit Pathology" if self.is_edit else "Add Pathology") self.dialog.geometry("450x400") self.dialog.resizable(False, False) # Make modal self.dialog.transient(parent) self.dialog.grab_set() self._setup_dialog() self._populate_fields() # Center dialog self.dialog.update_idletasks() x = parent.winfo_x() + (parent.winfo_width() // 2) - (450 // 2) y = parent.winfo_y() + (parent.winfo_height() // 2) - (400 // 2) self.dialog.geometry(f"450x400+{x}+{y}") def _setup_dialog(self): """Set up the dialog UI.""" # Main frame main_frame = ttk.Frame(self.dialog, padding="15") main_frame.grid(row=0, column=0, sticky="nsew") self.dialog.grid_rowconfigure(0, weight=1) self.dialog.grid_columnconfigure(0, weight=1) # Form fields self.key_var = tk.StringVar() self.name_var = tk.StringVar() self.scale_info_var = tk.StringVar() self.color_var = tk.StringVar() self.default_var = tk.BooleanVar() self.scale_min_var = tk.IntVar(value=0) self.scale_max_var = tk.IntVar(value=10) self.orientation_var = tk.StringVar(value="normal") # Key field ttk.Label(main_frame, text="Key:").grid( row=0, column=0, sticky="w", pady=(0, 5) ) key_entry = ttk.Entry(main_frame, textvariable=self.key_var, width=40) key_entry.grid(row=0, column=1, sticky="ew", pady=(0, 5)) ttk.Label(main_frame, text="(alphanumeric, underscores, hyphens only)").grid( row=0, column=2, sticky="w", padx=(5, 0), pady=(0, 5) ) # Display name field ttk.Label(main_frame, text="Display Name:").grid( row=1, column=0, sticky="w", pady=(0, 5) ) ttk.Entry(main_frame, textvariable=self.name_var, width=40).grid( row=1, column=1, sticky="ew", pady=(0, 5) ) # Scale info field ttk.Label(main_frame, text="Scale Info:").grid( row=2, column=0, sticky="w", pady=(0, 5) ) ttk.Entry(main_frame, textvariable=self.scale_info_var, width=40).grid( row=2, column=1, sticky="ew", pady=(0, 5) ) ttk.Label(main_frame, text='(e.g., "0:good, 10:bad")').grid( row=2, column=2, sticky="w", padx=(5, 0), pady=(0, 5) ) # Scale range scale_frame = ttk.Frame(main_frame) scale_frame.grid(row=3, column=1, sticky="ew", pady=(0, 5)) ttk.Label(main_frame, text="Scale Range:").grid( row=3, column=0, sticky="w", pady=(0, 5) ) ttk.Label(scale_frame, text="Min:").grid(row=0, column=0, sticky="w") ttk.Entry(scale_frame, textvariable=self.scale_min_var, width=5).grid( row=0, column=1, padx=(5, 10) ) ttk.Label(scale_frame, text="Max:").grid(row=0, column=2, sticky="w") ttk.Entry(scale_frame, textvariable=self.scale_max_var, width=5).grid( row=0, column=3, padx=5 ) # Scale orientation ttk.Label(main_frame, text="Scale Orientation:").grid( row=4, column=0, sticky="w", pady=(0, 5) ) orientation_frame = ttk.Frame(main_frame) orientation_frame.grid(row=4, column=1, sticky="ew", pady=(0, 5)) ttk.Radiobutton( orientation_frame, text="Normal (0=good)", variable=self.orientation_var, value="normal", ).grid(row=0, column=0, sticky="w") ttk.Radiobutton( orientation_frame, text="Inverted (0=bad)", variable=self.orientation_var, value="inverted", ).grid(row=0, column=1, sticky="w", padx=(20, 0)) # Color field ttk.Label(main_frame, text="Color:").grid( row=5, column=0, sticky="w", pady=(0, 5) ) ttk.Entry(main_frame, textvariable=self.color_var, width=40).grid( row=5, column=1, sticky="ew", pady=(0, 5) ) ttk.Label(main_frame, text="(hex format, e.g., #FF6B6B)").grid( row=5, column=2, sticky="w", padx=(5, 0), pady=(0, 5) ) # Default enabled checkbox ttk.Checkbutton( main_frame, text="Show in graph by default", variable=self.default_var ).grid(row=6, column=1, sticky="w", pady=(10, 15)) # Buttons button_frame = ttk.Frame(main_frame) button_frame.grid(row=7, column=0, columnspan=3, sticky="ew", pady=(10, 0)) ttk.Button(button_frame, text="Save", command=self._save_pathology).pack( side="right", padx=(5, 0) ) ttk.Button(button_frame, text="Cancel", command=self.dialog.destroy).pack( side="right" ) # Configure column weights main_frame.grid_columnconfigure(1, weight=1) # Focus on first field key_entry.focus() def _populate_fields(self): """Populate fields if editing.""" if self.pathology: self.key_var.set(self.pathology.key) self.name_var.set(self.pathology.display_name) self.scale_info_var.set(self.pathology.scale_info) self.color_var.set(self.pathology.color) self.default_var.set(self.pathology.default_enabled) self.scale_min_var.set(self.pathology.scale_min) self.scale_max_var.set(self.pathology.scale_max) self.orientation_var.set(self.pathology.scale_orientation) def _save_pathology(self): """Save the pathology.""" # Validate fields key = self.key_var.get().strip() name = self.name_var.get().strip() scale_info = self.scale_info_var.get().strip() color = self.color_var.get().strip() scale_min = self.scale_min_var.get() scale_max = self.scale_max_var.get() if not all([key, name, scale_info, color]): messagebox.showerror("Error", "All fields are required.") return # Validate key format (alphanumeric and underscores only) if not key.replace("_", "").replace("-", "").isalnum(): messagebox.showerror( "Error", "Key must contain only letters, numbers, underscores, and hyphens.", ) return # Validate scale range if scale_min >= scale_max: messagebox.showerror("Error", "Scale minimum must be less than maximum.") return # Validate color format if not color.startswith("#") or len(color) != 7: messagebox.showerror( "Error", "Color must be in hex format (e.g., #FF6B6B)." ) return try: int(color[1:], 16) # Validate hex color except ValueError: messagebox.showerror("Error", "Invalid hex color format.") return # Create pathology object new_pathology = Pathology( key=key, display_name=name, scale_info=scale_info, color=color, default_enabled=self.default_var.get(), scale_min=scale_min, scale_max=scale_max, scale_orientation=self.orientation_var.get(), ) # Save pathology success = False if self.is_edit: success = self.pathology_manager.update_pathology( self.pathology.key, new_pathology ) else: success = self.pathology_manager.add_pathology(new_pathology) if success: action = "updated" if self.is_edit else "added" messagebox.showinfo("Success", f"Pathology {action} successfully!") self.callback() self.dialog.destroy() else: action = "update" if self.is_edit else "add" messagebox.showerror("Error", f"Failed to {action} pathology.")