import logging import os import sys import tkinter as tk from collections.abc import Callable from datetime import datetime from tkinter import messagebox, ttk from typing import Any from PIL import Image, ImageTk class UIManager: """Handle UI creation and management for the application.""" def __init__(self, root: tk.Tk, logger: logging.Logger) -> None: self.root: tk.Tk = root self.logger: logging.Logger = logger def setup_application_icon(self, img_path: str) -> bool: """Set up the application icon.""" try: self.logger.info(f"Trying to load icon from: {img_path}") # Try to find the icon in various locations # Check if we're in PyInstaller bundle if not os.path.exists(img_path) and hasattr(sys, "_MEIPASS"): # PyInstaller creates a temp folder and stores path in _MEIPASS base_path: str = sys._MEIPASS potential_paths: list[str] = [ os.path.join(base_path, os.path.basename(img_path)), os.path.join(base_path, "chart-671.png"), ] for path in potential_paths: if os.path.exists(path): self.logger.info(f"Found icon in PyInstaller bundle: {path}") img_path = path break icon_image: Image.Image = Image.open(img_path) icon_image = icon_image.resize( size=(32, 32), resample=Image.Resampling.NEAREST ) icon_photo: ImageTk.PhotoImage = ImageTk.PhotoImage(image=icon_image) self.root.iconphoto(True, icon_photo) self.root.wm_iconphoto(True, icon_photo) return True except FileNotFoundError: self.logger.warning(f"Icon file not found at {img_path}") return False except Exception as e: self.logger.error(f"Error setting icon: {str(e)}") return False def create_input_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]: """Create and configure the input frame with all widgets.""" # Create main container for the scrollable input frame main_container = ttk.LabelFrame(parent_frame, text="New Entry") main_container.grid(row=1, column=0, padx=10, pady=10, sticky="nsew") main_container.grid_rowconfigure(0, weight=1) main_container.grid_columnconfigure(0, weight=1) # Create canvas and scrollbar for scrolling canvas = tk.Canvas(main_container, highlightthickness=0) scrollbar = ttk.Scrollbar( main_container, orient="vertical", command=canvas.yview ) canvas.configure(yscrollcommand=scrollbar.set) # Create the actual input frame inside the canvas input_frame = ttk.Frame(canvas) input_frame.grid_columnconfigure(1, weight=1) # Place canvas and scrollbar in the container canvas.grid(row=0, column=0, sticky="nsew") scrollbar.grid(row=0, column=1, sticky="ns") # Create window in canvas for the input frame canvas_window = canvas.create_window((0, 0), window=input_frame, anchor="nw") # Configure canvas window width to fill available space def configure_canvas_width(event=None): canvas_width = canvas.winfo_width() canvas.itemconfig(canvas_window, width=canvas_width) # Configure canvas scrolling def configure_scroll_region(event=None): canvas.configure(scrollregion=canvas.bbox("all")) def on_mousewheel(event): # Check if canvas is scrollable before scrolling if canvas.cget("scrollregion"): canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") def on_mousewheel_linux_up(event): # Linux mouse wheel up if canvas.cget("scrollregion"): canvas.yview_scroll(-1, "units") def on_mousewheel_linux_down(event): # Linux mouse wheel down if canvas.cget("scrollregion"): canvas.yview_scroll(1, "units") input_frame.bind("", configure_scroll_region) canvas.bind("", configure_canvas_width) # Bind mouse wheel events to canvas and main container canvas.bind("", on_mousewheel) # Windows/Linux canvas.bind("", on_mousewheel_linux_up) # Linux canvas.bind("", on_mousewheel_linux_down) # Linux main_container.bind("", on_mousewheel) # Windows/Linux main_container.bind("", on_mousewheel_linux_up) # Linux main_container.bind("", on_mousewheel_linux_down) # Linux # Bind mouse wheel to input frame and its children for better scrolling self._bind_mousewheel_to_widget_tree(input_frame, canvas) # Set focus to canvas to ensure it receives scroll events canvas.focus_set() # Add mouse enter event to manage focus for scrolling def on_mouse_enter(event): canvas.focus_set() main_container.bind("", on_mouse_enter) canvas.bind("", on_mouse_enter) # Create variables for symptoms symptom_vars: dict[str, tk.IntVar] = { "depression": tk.IntVar(value=0), "anxiety": tk.IntVar(value=0), "sleep": tk.IntVar(value=0), "appetite": tk.IntVar(value=0), } # Create scales for symptoms symptom_labels: list[tuple[str, str]] = [ ("Depression (0-10):", "depression"), ("Anxiety (0-10):", "anxiety"), ("Sleep Quality (0-10):", "sleep"), ("Appetite (0-10):", "appetite"), ] for idx, (label, var_name) in enumerate(symptom_labels): ttk.Label(input_frame, text=label).grid( row=idx, column=0, sticky="w", padx=5, pady=2 ) ttk.Scale( input_frame, from_=0, to=10, orient=tk.HORIZONTAL, variable=symptom_vars[var_name], ).grid(row=idx, column=1, sticky="ew") # Medicine tracking section (simplified) ttk.Label(input_frame, text="Treatment:").grid( row=4, column=0, sticky="w", padx=5, pady=2 ) medicine_frame = ttk.LabelFrame(input_frame, text="Medicine") medicine_frame.grid(row=4, column=1, padx=0, pady=10, sticky="nsew") medicine_frame.grid_columnconfigure(0, weight=1) # Store medicine variables (checkboxes only) medicine_vars: dict[str, tuple[tk.IntVar, str]] = { "bupropion": (tk.IntVar(value=0), "Bupropion 150/300 mg"), "hydroxyzine": (tk.IntVar(value=0), "Hydroxyzine 25mg"), "gabapentin": (tk.IntVar(value=0), "Gabapentin 100mg"), "propranolol": (tk.IntVar(value=0), "Propranolol 10mg"), "quetiapine": (tk.IntVar(value=0), "Quetiapine 25mg"), } for idx, (_med_name, (var, text)) in enumerate(medicine_vars.items()): # Just checkbox for medicine taken ttk.Checkbutton(medicine_frame, text=text, variable=var).grid( row=idx, column=0, sticky="w", padx=5, pady=2 ) # Note and Date fields note_var: tk.StringVar = tk.StringVar() date_var: tk.StringVar = tk.StringVar() ttk.Label(input_frame, text="Note:").grid( row=5, column=0, sticky="w", padx=5, pady=2 ) ttk.Entry(input_frame, textvariable=note_var).grid( row=5, column=1, sticky="ew", padx=5, pady=2 ) ttk.Label(input_frame, text="Date (mm/dd/yyyy):").grid( row=6, column=0, sticky="w", padx=5, pady=2 ) ttk.Entry(input_frame, textvariable=date_var, justify="center").grid( row=6, column=1, sticky="ew", padx=5, pady=2 ) # Set default date to today date_var.set(datetime.now().strftime("%m/%d/%Y")) # Ensure mouse wheel binding is applied to all newly created widgets main_container.update_idletasks() canvas.configure(scrollregion=canvas.bbox("all")) self._bind_mousewheel_to_widget_tree(input_frame, canvas) # Return all UI elements and variables return { "frame": main_container, "symptom_vars": symptom_vars, "medicine_vars": medicine_vars, "note_var": note_var, "date_var": date_var, } def create_table_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]: """Create and configure the table frame with a treeview.""" table_frame: ttk.LabelFrame = ttk.LabelFrame( parent_frame, text="Log (Double-click to edit)" ) table_frame.grid(row=1, column=1, padx=10, pady=10, sticky="nsew") # Configure table frame to expand table_frame.grid_rowconfigure(0, weight=1) table_frame.grid_columnconfigure(0, weight=1) columns: list[str] = [ "Date", "Depression", "Anxiety", "Sleep", "Appetite", "Bupropion", "Hydroxyzine", "Gabapentin", "Propranolol", "Quetiapine", "Note", ] tree: ttk.Treeview = ttk.Treeview(table_frame, columns=columns, show="headings") col_labels: list[str] = [ "Date", "Depression", "Anxiety", "Sleep", "Appetite", "Bupropion 150/300 mg", "Hydroxyzine 25mg", "Gabapentin 100mg", "Propranolol 10mg", "Quetiapine 25mg", "Note", ] for col, label in zip(columns, col_labels, strict=False): tree.heading(col, text=label) col_settings: list[tuple[str, int, str]] = [ ("Date", 80, "center"), ("Depression", 80, "center"), ("Anxiety", 80, "center"), ("Sleep", 80, "center"), ("Appetite", 80, "center"), ("Bupropion", 120, "center"), ("Hydroxyzine", 120, "center"), ("Gabapentin", 120, "center"), ("Propranolol", 120, "center"), ("Quetiapine", 120, "center"), ("Note", 300, "w"), ] for col, width, anchor in col_settings: tree.column(col, width=width, anchor=anchor) tree.pack(side="left", fill="both", expand=True) # Add scrollbar scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview) tree.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side="right", fill="y") return {"frame": table_frame, "tree": tree} def create_graph_frame(self, parent_frame: ttk.Frame) -> ttk.LabelFrame: """Create and configure the graph frame.""" graph_frame: ttk.LabelFrame = ttk.LabelFrame(parent_frame, text="Evolution") graph_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky="nsew") return graph_frame def add_action_buttons( self, frame: ttk.Frame, buttons_config: list[dict[str, Any]] ) -> ttk.Frame: """Add buttons to a frame based on configuration.""" button_frame: ttk.Frame = ttk.Frame(frame) button_frame.grid(row=7, column=0, columnspan=2, pady=10) for btn_config in buttons_config: ttk.Button( button_frame, text=btn_config["text"], command=btn_config["command"], ).pack( side="left", padx=5, fill=btn_config.get("fill", None), expand=btn_config.get("expand", False), ) return button_frame def create_edit_window( self, values: tuple[str, ...], callbacks: dict[str, Callable] ) -> tk.Toplevel: """Create a new window for editing an entry with improved UI.""" edit_win: tk.Toplevel = tk.Toplevel(master=self.root) edit_win.title("Edit Entry") edit_win.transient(self.root) # Make window modal edit_win.minsize(600, 700) edit_win.geometry("800x800") # Create scrollable container canvas = tk.Canvas(edit_win, highlightthickness=0) scrollbar = ttk.Scrollbar(edit_win, orient="vertical", command=canvas.yview) canvas.configure(yscrollcommand=scrollbar.set) # Configure main container with padding inside the canvas main_container = ttk.Frame(canvas, padding="20") # Pack canvas and scrollbar canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # Create window in canvas for the main container canvas_window = canvas.create_window((0, 0), window=main_container, anchor="nw") # Configure grid for main container main_container.grid_columnconfigure(0, weight=1) # Configure scrolling def configure_scroll_region(event=None): canvas.configure(scrollregion=canvas.bbox("all")) def configure_canvas_width(event=None): canvas_width = canvas.winfo_width() canvas.itemconfig(canvas_window, width=canvas_width) def on_mousewheel(event): # Check if canvas is scrollable before scrolling if canvas.cget("scrollregion"): canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") def on_mousewheel_linux_up(event): # Linux mouse wheel up if canvas.cget("scrollregion"): canvas.yview_scroll(-1, "units") def on_mousewheel_linux_down(event): # Linux mouse wheel down if canvas.cget("scrollregion"): canvas.yview_scroll(1, "units") main_container.bind("", configure_scroll_region) canvas.bind("", configure_canvas_width) # Bind mouse wheel events to canvas and edit window canvas.bind("", on_mousewheel) # Windows/Linux canvas.bind("", on_mousewheel_linux_up) # Linux canvas.bind("", on_mousewheel_linux_down) # Linux edit_win.bind("", on_mousewheel) # Windows/Linux edit_win.bind("", on_mousewheel_linux_up) # Linux edit_win.bind("", on_mousewheel_linux_down) # Linux # Bind mouse wheel to main container and its children for better scrolling self._bind_mousewheel_to_widget_tree(main_container, canvas) # Set focus to canvas to ensure it receives scroll events canvas.focus_set() # Add mouse enter event to manage focus for scrolling def on_mouse_enter(event): canvas.focus_set() edit_win.bind("", on_mouse_enter) canvas.bind("", on_mouse_enter) # Unpack values - handle both old and new CSV formats if len(values) == 10: # Old format: date, dep, anx, slp, app, bup, hydro, gaba, prop, note date, dep, anx, slp, app, bup, hydro, gaba, prop, note = values bup_doses, hydro_doses, gaba_doses, prop_doses, quet_doses = ( "", "", "", "", "", ) quet = 0 elif len(values) == 14: # Old new format with dose tracking (without quetiapine) ( date, dep, anx, slp, app, bup, bup_doses, hydro, hydro_doses, gaba, gaba_doses, prop, prop_doses, note, ) = values quet, quet_doses = 0, "" elif len(values) == 16: # New format with quetiapine and dose tracking ( date, dep, anx, slp, app, bup, bup_doses, hydro, hydro_doses, gaba, gaba_doses, prop, prop_doses, quet, quet_doses, note, ) = values else: # Fallback for unexpected format self.logger.warning(f"Unexpected number of values in edit: {len(values)}") # Pad with default values values_list = list(values) + [""] * (16 - len(values)) ( date, dep, anx, slp, app, bup, bup_doses, hydro, hydro_doses, gaba, gaba_doses, prop, prop_doses, quet, quet_doses, note, ) = values_list[:16] # Create improved UI sections vars_dict = self._create_edit_ui( main_container, date, dep, anx, slp, app, bup, hydro, gaba, prop, quet, note, { "bupropion": bup_doses, "hydroxyzine": hydro_doses, "gabapentin": gaba_doses, "propranolol": prop_doses, "quetiapine": quet_doses, }, ) # Add action buttons self._add_edit_buttons(main_container, vars_dict, callbacks, edit_win) # Update scroll region after adding all content edit_win.update_idletasks() canvas.configure(scrollregion=canvas.bbox("all")) # Ensure mouse wheel binding is applied to all newly created widgets self._bind_mousewheel_to_widget_tree(main_container, canvas) # Make window modal edit_win.focus_set() edit_win.grab_set() return edit_win def _create_edit_ui( self, parent: ttk.Frame, date: str, dep: int, anx: int, slp: int, app: int, bup: int, hydro: int, gaba: int, prop: int, quet: int, note: str, dose_data: dict[str, str], ) -> dict[str, Any]: """Create UI layout for edit window with organized sections.""" vars_dict = {} row = 0 # Header with entry date header_frame = ttk.Frame(parent) header_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20)) header_frame.grid_columnconfigure(1, weight=1) ttk.Label( header_frame, text="Editing Entry for:", font=("TkDefaultFont", 12, "bold") ).grid(row=0, column=0, sticky="w") vars_dict["date"] = tk.StringVar(value=str(date)) date_entry = ttk.Entry( header_frame, textvariable=vars_dict["date"], font=("TkDefaultFont", 12), width=15, ) date_entry.grid(row=0, column=1, sticky="w", padx=(10, 0)) row += 1 # Symptoms section symptoms_frame = ttk.LabelFrame( parent, text="Daily Symptoms (0-10 scale)", padding="15" ) symptoms_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15)) symptoms_frame.grid_columnconfigure(1, weight=1) # Create symptom scales with better layout symptoms = [ ("Depression", "depression", dep), ("Anxiety", "anxiety", anx), ("Sleep Quality", "sleep", slp), ("Appetite", "appetite", app), ] for i, (label, key, value) in enumerate(symptoms): self._create_symptom_scale(symptoms_frame, i, label, key, value, vars_dict) row += 1 # Medications section meds_frame = ttk.LabelFrame(parent, text="Medications Taken", padding="15") meds_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15)) meds_frame.grid_columnconfigure(0, weight=1) # Create medicine checkboxes with better styling med_vars = self._create_medicine_section( meds_frame, bup, hydro, gaba, prop, quet ) vars_dict.update(med_vars) row += 1 # Dose tracking section dose_frame = ttk.LabelFrame(parent, text="Dose Tracking", padding="15") dose_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15)) dose_frame.grid_columnconfigure(0, weight=1) dose_vars = self._create_dose_tracking(dose_frame, dose_data) vars_dict.update(dose_vars) row += 1 # Notes section notes_frame = ttk.LabelFrame(parent, text="Notes", padding="15") notes_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20)) notes_frame.grid_columnconfigure(0, weight=1) vars_dict["note"] = tk.StringVar(value=str(note)) note_text = tk.Text( notes_frame, height=4, wrap=tk.WORD, font=("TkDefaultFont", 10) ) note_text.grid(row=0, column=0, sticky="ew") note_text.insert(1.0, str(note)) vars_dict["note_text"] = note_text # Add scrollbar for notes note_scroll = ttk.Scrollbar( notes_frame, orient="vertical", command=note_text.yview ) note_scroll.grid(row=0, column=1, sticky="ns") note_text.configure(yscrollcommand=note_scroll.set) return vars_dict def _create_symptom_scale( self, parent: ttk.Frame, row: int, label: str, key: str, value: int, vars_dict: dict[str, Any], ) -> None: """Create a symptom scale with visual feedback.""" # Ensure value is properly converted try: value = int(float(value)) if value not in ["", None] else 0 except (ValueError, TypeError): value = 0 vars_dict[key] = tk.IntVar(value=value) # Label ttk.Label(parent, text=f"{label}:", font=("TkDefaultFont", 10, "bold")).grid( row=row, column=0, sticky="w", pady=8 ) # Scale container scale_container = ttk.Frame(parent) scale_container.grid(row=row, column=1, sticky="ew", padx=(20, 0), pady=8) scale_container.grid_columnconfigure(0, weight=1) # Scale with value labels scale_frame = ttk.Frame(scale_container) scale_frame.grid(row=0, column=0, sticky="ew") scale_frame.grid_columnconfigure(1, weight=1) # Current value display value_label = ttk.Label( scale_frame, text=str(value), font=("TkDefaultFont", 12, "bold"), foreground="#2E86AB", width=3, ) value_label.grid(row=0, column=0, padx=(0, 10)) # Scale widget scale = ttk.Scale( scale_frame, from_=0, to=10, variable=vars_dict[key], orient=tk.HORIZONTAL, length=300, ) scale.grid(row=0, column=1, sticky="ew") # Scale labels (0, 5, 10) labels_frame = ttk.Frame(scale_container) labels_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0)) ttk.Label(labels_frame, text="0", font=("TkDefaultFont", 8)).grid( row=0, column=0, sticky="w" ) labels_frame.grid_columnconfigure(1, weight=1) ttk.Label(labels_frame, text="5", font=("TkDefaultFont", 8)).grid( row=0, column=1 ) ttk.Label(labels_frame, text="10", font=("TkDefaultFont", 8)).grid( row=0, column=2, sticky="e" ) # Update label when scale changes def update_value_label(event=None): current_val = vars_dict[key].get() value_label.configure(text=str(current_val)) # Change color based on value if current_val <= 3: value_label.configure(foreground="#28A745") # Green for low/good elif current_val <= 6: value_label.configure(foreground="#FFC107") # Yellow for medium else: value_label.configure(foreground="#DC3545") # Red for high/bad scale.bind("", update_value_label) scale.bind("", update_value_label) scale.bind("", update_value_label) update_value_label() # Set initial color def _create_medicine_section( self, parent: ttk.Frame, bup: int, hydro: int, gaba: int, prop: int, quet: int ) -> dict[str, tk.IntVar]: """Create medicine checkboxes with organized layout.""" vars_dict = {} # Create a grid layout for medicines medicines = [ ("bupropion", bup, "Bupropion", "150/300 mg", "#E8F4FD"), ("hydroxyzine", hydro, "Hydroxyzine", "25 mg", "#FFF2E8"), ("gabapentin", gaba, "Gabapentin", "100 mg", "#F0F8E8"), ("propranolol", prop, "Propranolol", "10 mg", "#FCE8F3"), ("quetiapine", quet, "Quetiapine", "25 mg", "#E8F0FF"), ] # Create medicine cards in a 2-column layout for i, (key, value, name, dose, _bg_color) in enumerate(medicines): row = i // 2 col = i % 2 # Medicine card frame med_card = ttk.Frame(parent, relief="solid", borderwidth=1) med_card.grid(row=row, column=col, sticky="ew", padx=5, pady=5) parent.grid_columnconfigure(col, weight=1) vars_dict[key] = tk.IntVar(value=int(value)) # Checkbox with medicine name check_frame = ttk.Frame(med_card) check_frame.pack(fill="x", padx=10, pady=8) checkbox = ttk.Checkbutton( check_frame, text=f"{name} ({dose})", variable=vars_dict[key], style="Medicine.TCheckbutton", ) checkbox.pack(anchor="w") return vars_dict def _create_dose_tracking( self, parent: ttk.Frame, dose_data: dict[str, str] ) -> dict[str, Any]: """Create dose tracking interface.""" vars_dict = {} # Create notebook for organized dose tracking notebook = ttk.Notebook(parent) notebook.pack(fill="both", expand=True) medicines = [ ("bupropion", "Bupropion"), ("hydroxyzine", "Hydroxyzine"), ("gabapentin", "Gabapentin"), ("propranolol", "Propranolol"), ("quetiapine", "Quetiapine"), ] for med_key, med_name in medicines: # Create tab for each medicine tab_frame = ttk.Frame(notebook) notebook.add(tab_frame, text=med_name) # Configure tab layout tab_frame.grid_columnconfigure(0, weight=1) # Quick dose entry section entry_frame = ttk.LabelFrame(tab_frame, text="Add New Dose", padding="10") entry_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5) entry_frame.grid_columnconfigure(1, weight=1) ttk.Label(entry_frame, text="Dose amount:").grid( row=0, column=0, sticky="w" ) dose_entry_var = tk.StringVar() vars_dict[f"{med_key}_entry_var"] = dose_entry_var dose_entry = ttk.Entry(entry_frame, textvariable=dose_entry_var, width=15) dose_entry.grid(row=0, column=1, sticky="w", padx=(10, 10)) # Quick dose buttons quick_frame = ttk.Frame(entry_frame) quick_frame.grid(row=0, column=2, sticky="w") # Common dose amounts (customize per medicine) quick_doses = self._get_quick_doses(med_key) for i, dose in enumerate(quick_doses): ttk.Button( quick_frame, text=dose, width=8, command=lambda d=dose, var=dose_entry_var: var.set(d), ).grid(row=0, column=i, padx=2) # Take dose button def create_take_dose_command(med_name, entry_var, med_key): def take_dose(): self._take_dose(med_name, entry_var, med_key, vars_dict) return take_dose take_button = ttk.Button( entry_frame, text=f"Take {med_name}", style="Accent.TButton", command=create_take_dose_command(med_name, dose_entry_var, med_key), ) take_button.grid(row=1, column=0, columnspan=3, pady=(10, 0), sticky="ew") # Dose history section history_frame = ttk.LabelFrame( tab_frame, text="Today's Doses", padding="10" ) history_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=5) history_frame.grid_columnconfigure(0, weight=1) # Dose history display with fixed height to prevent excessive expansion dose_text = tk.Text( history_frame, height=4, # Reduced height to fit better in scrollable window wrap=tk.WORD, font=("Consolas", 10), state="normal", # Start enabled ) dose_text.grid(row=0, column=0, sticky="ew") # Store raw dose string in a variable doses_str = dose_data.get(med_key, "") dose_str_var = tk.StringVar(value=doses_str) vars_dict[f"{med_key}_doses_str"] = dose_str_var # Populate with existing doses self._populate_dose_history(dose_text, dose_str_var.get()) vars_dict[f"{med_key}_doses_text"] = dose_text # Scrollbar for dose history dose_scroll = ttk.Scrollbar( history_frame, orient="vertical", command=dose_text.yview ) dose_scroll.grid(row=0, column=1, sticky="ns") dose_text.configure(yscrollcommand=dose_scroll.set) return vars_dict def _get_quick_doses(self, medicine_key: str) -> list[str]: """Get common dose amounts for quick selection.""" dose_map = { "bupropion": ["150", "300"], "hydroxyzine": ["25", "50"], "gabapentin": ["100", "300", "600"], "propranolol": ["10", "20", "40"], "quetiapine": ["25", "50", "100"], } return dose_map.get(medicine_key, ["25", "50"]) def _populate_dose_history(self, text_widget: tk.Text, doses_str: str) -> None: """Populate dose history text widget with formatted dose data.""" text_widget.configure(state="normal") text_widget.delete(1.0, tk.END) if not doses_str or str(doses_str) == "nan": text_widget.insert(1.0, "No doses recorded today") # Keep text widget enabled for editing return doses_str = str(doses_str) formatted_doses = [] for dose_entry in doses_str.split("|"): if ":" in dose_entry: timestamp, dose = dose_entry.split(":", 1) try: dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") time_str = dt.strftime("%I:%M %p") formatted_doses.append(f"• {time_str} - {dose}") except ValueError: # Handle cases where the timestamp might be malformed formatted_doses.append(f"• {dose_entry}") if formatted_doses: text_widget.insert(1.0, "\n".join(formatted_doses)) else: text_widget.insert(1.0, "No doses recorded today") # Always keep text widget enabled for user editing def _take_dose( self, med_name: str, entry_var: tk.StringVar, med_key: str, vars_dict: dict[str, Any], ) -> None: """Handle taking a dose with feedback and state management.""" dose = entry_var.get().strip() # Get the dose text widget - this is what the save function reads from dose_text_widget = vars_dict.get(f"{med_key}_doses_text") if not dose_text_widget: self.logger.error(f"Dose text widget not found for {med_key}") return # Find the parent edit window parent_window = dose_text_widget.winfo_toplevel() if not dose: messagebox.showerror( "Error", f"Please enter a dose amount for {med_name}", parent=parent_window, ) return # Get current time and timestamp now = datetime.now() time_str = now.strftime("%I:%M %p") # Ensure text widget is enabled dose_text_widget.configure(state="normal") # Get current content from the text widget current_content = dose_text_widget.get(1.0, tk.END).strip() self.logger.debug(f"Current content before adding dose: '{current_content}'") # Create new dose entry in the display format new_dose_line = f"• {time_str} - {dose}" self.logger.debug(f"New dose line: '{new_dose_line}'") # Add the new dose to the text widget if current_content == "No doses recorded today" or not current_content: dose_text_widget.delete(1.0, tk.END) dose_text_widget.insert(1.0, new_dose_line) self.logger.debug("Added first dose") else: # Append to existing content with proper formatting updated_content = current_content + f"\n{new_dose_line}" self.logger.debug(f"Updated content: '{updated_content}'") dose_text_widget.delete(1.0, tk.END) dose_text_widget.insert(1.0, updated_content) self.logger.debug("Added subsequent dose") # Verify what's actually in the widget after insertion final_content = dose_text_widget.get(1.0, tk.END).strip() self.logger.debug(f"Final content in widget: '{final_content}'") # Clear entry field entry_var.set("") # Success feedback messagebox.showinfo( "Dose Recorded", f"{med_name} dose of {dose} recorded at {time_str}", parent=parent_window, ) def _add_edit_buttons( self, parent: ttk.Frame, vars_dict: dict[str, Any], callbacks: dict[str, Callable], edit_win: tk.Toplevel, ) -> None: """Add action buttons to edit window.""" button_frame = ttk.Frame(parent) button_frame.grid(row=999, column=0, sticky="ew", pady=(20, 0)) button_frame.grid_columnconfigure((0, 1, 2), weight=1) # Save button def save_with_data(): self.logger.debug("=== SAVE FUNCTION CALLED ===") # Get note text from Text widget note_text_widget = vars_dict.get("note_text") note_content = "" if note_text_widget: note_content = note_text_widget.get(1.0, tk.END).strip() # Extract dose data from the editable text widgets dose_data = {} medicine_list = [ "bupropion", "hydroxyzine", "gabapentin", "propranolol", "quetiapine", ] for medicine in medicine_list: dose_text_key = f"{medicine}_doses_text" self.logger.debug(f"Processing {medicine}...") if dose_text_key in vars_dict and isinstance( vars_dict[dose_text_key], tk.Text ): raw_text = vars_dict[dose_text_key].get(1.0, tk.END).strip() self.logger.debug(f"Raw text for {medicine}: '{raw_text}'") parsed_dose = self._parse_dose_history_for_saving( raw_text, vars_dict["date"].get() ) dose_data[medicine] = parsed_dose self.logger.debug(f"Parsed dose for {medicine}: '{parsed_dose}'") else: self.logger.debug(f"No text widget found for {medicine}") dose_data[medicine] = "" self.logger.debug(f"Final dose_data: {dose_data}") callbacks["save"]( edit_win, vars_dict["date"].get(), vars_dict["depression"].get(), vars_dict["anxiety"].get(), vars_dict["sleep"].get(), vars_dict["appetite"].get(), vars_dict["bupropion"].get(), vars_dict["hydroxyzine"].get(), vars_dict["gabapentin"].get(), vars_dict["propranolol"].get(), vars_dict["quetiapine"].get(), note_content, dose_data, ) save_btn = ttk.Button( button_frame, text="💾 Save Changes", style="Accent.TButton", command=save_with_data, ) save_btn.grid(row=0, column=0, sticky="ew", padx=(0, 5)) # Cancel button cancel_btn = ttk.Button( button_frame, text="❌ Cancel", command=edit_win.destroy ) cancel_btn.grid(row=0, column=1, sticky="ew", padx=5) # Delete button delete_btn = ttk.Button( button_frame, text="🗑️ Delete Entry", style="Danger.TButton", command=lambda: callbacks["delete"](edit_win), ) delete_btn.grid(row=0, column=2, sticky="ew", padx=(5, 0)) def _parse_dose_history_for_saving(self, text: str, date_str: str) -> str: """ Parse the user-edited dose history back into the storable format, supporting add/delete/edit. """ self.logger.debug("=== PARSING DOSE HISTORY ===") self.logger.debug(f"Input text: '{text}'") self.logger.debug(f"Date string: '{date_str}'") if not text or "No doses recorded" in text: self.logger.debug("No doses to parse, returning empty string") return "" lines = text.strip().split("\n") self.logger.debug(f"Split into {len(lines)} lines: {lines}") dose_entries = [] for line_num, line in enumerate(lines): line = line.strip() self.logger.debug(f"Processing line {line_num}: '{line}'") if not line or line.lower().startswith("no doses recorded"): self.logger.debug("Empty or placeholder line, skipping") continue # Handle bullet point format: "• HH:MM AM/PM - dose" if line.startswith("•") and " - " in line: try: content = line.lstrip("• ").strip() self.logger.debug(f"Bullet point content: '{content}'") time_part, dose_part = content.split(" - ", 1) self.logger.debug( f"Time part: '{time_part}', Dose part: '{dose_part}'" ) # Try parsing as 12-hour (with AM/PM) try: time_obj = datetime.strptime(time_part.strip(), "%I:%M %p") except ValueError: # Try 24-hour format fallback time_obj = datetime.strptime(time_part.strip(), "%H:%M") entry_date = datetime.strptime(date_str, "%m/%d/%Y") full_timestamp = entry_date.replace( hour=time_obj.hour, minute=time_obj.minute, second=0, microsecond=0, ) timestamp_str = full_timestamp.strftime("%Y-%m-%d %H:%M:%S") dose_entry = f"{timestamp_str}:{dose_part.strip()}" dose_entries.append(dose_entry) self.logger.debug(f"Added dose entry: '{dose_entry}'") except Exception as e: self.logger.warning( f"Could not parse dose line: '{line}'. Error: {e}" ) continue # Handle simple format: "HH:MM dose" or "HH:MM: dose" elif ":" in line and not line.startswith("•"): try: # Try to parse as "HH:MM dose" or "HH:MM: dose" if " " in line: time_part, dose_part = line.split(" ", 1) time_part = time_part.rstrip(":") # Try 24-hour format first try: time_obj = datetime.strptime(time_part, "%H:%M") except ValueError: # Try 12-hour format time_obj = datetime.strptime(time_part, "%I:%M") entry_date = datetime.strptime(date_str, "%m/%d/%Y") full_timestamp = entry_date.replace( hour=time_obj.hour, minute=time_obj.minute, second=0, microsecond=0, ) timestamp_str = full_timestamp.strftime("%Y-%m-%d %H:%M:%S") dose_entries.append(f"{timestamp_str}:{dose_part.strip()}") self.logger.debug( "Added simple dose entry: '%s:%s'", timestamp_str, dose_part.strip(), ) except Exception as e: self.logger.warning( f"Could not parse simple dose line: '{line}'. Error: {e}" ) continue # If user just types a dose (no time), store as-is with no timestamp elif line: self.logger.debug(f"Line with no time, storing as-is: '{line}'") dose_entries.append(line) result = "|".join(dose_entries) self.logger.debug(f"Final parsed result: '{result}'") return result def _bind_mousewheel_to_widget_tree( self, widget: tk.Widget, canvas: tk.Canvas ) -> None: """Recursively bind mouse wheel events to all widgets in the tree.""" def on_mousewheel(event): # Check if canvas is scrollable before scrolling if canvas.cget("scrollregion"): canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") def on_mousewheel_linux_up(event): if canvas.cget("scrollregion"): canvas.yview_scroll(-1, "units") def on_mousewheel_linux_down(event): if canvas.cget("scrollregion"): canvas.yview_scroll(1, "units") # Bind to the widget itself try: widget.bind("", on_mousewheel) widget.bind("", on_mousewheel_linux_up) widget.bind("", on_mousewheel_linux_down) except tk.TclError: # Some widgets might not support binding pass # Recursively bind to all children try: for child in widget.winfo_children(): # Skip widgets that have their own scrolling behavior or are problematic skip_types = (tk.Text, tk.Listbox, tk.Canvas, ttk.Notebook) if not isinstance(child, skip_types): self._bind_mousewheel_to_widget_tree(child, canvas) elif isinstance(child, ttk.Notebook): # For notebooks, bind to their tab frames for tab_id in child.tabs(): tab_widget = child.nametowidget(tab_id) self._bind_mousewheel_to_widget_tree(tab_widget, canvas) except tk.TclError: # Handle potential errors when accessing children pass