Refactor MedTrackerApp and UI components for improved structure and readability

- Simplified initialization logic in init.py
- Consolidated testing_mode assignment
- Removed unnecessary else statements
- Created UIManager class to handle UI-related tasks
- Modularized input frame creation, table frame creation, and graph frame creation
- Enhanced edit window creation with better organization and error handling
- Updated data management methods to improve clarity and maintainability
- Improved logging for better debugging and tracking of application flow
This commit is contained in:
William Valentin
2025-07-23 16:10:22 -07:00
parent 4ba4b1b7c5
commit 2142db7093
15 changed files with 1063 additions and 578 deletions
+462
View File
@@ -0,0 +1,462 @@
import os
import logging
import sys
import tkinter as tk
from tkinter import ttk
from typing import Dict, List, Tuple, Any, Callable, Union
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_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
if not os.path.exists(img_path):
# Check if we're in PyInstaller bundle
if 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."""
input_frame: ttk.LabelFrame = ttk.LabelFrame(
parent_frame, text="New Entry"
)
input_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
input_frame.grid_columnconfigure(1, weight=1)
# 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 checkboxes
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_vars: Dict[str, Tuple[tk.IntVar, str]] = {
"bupropion": (tk.IntVar(value=0), "Bupropion 150mg"),
"hydroxyzine": (tk.IntVar(value=0), "Hydroxyzine 25mg"),
"gabapentin": (tk.IntVar(value=0), "Gabapentin 100mg"),
"propranolol": (tk.IntVar(value=0), "Propranolol 10mg"),
}
for idx, (name, (var, text)) in enumerate(medicine_vars.items()):
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
)
# Return all UI elements and variables
return {
"frame": input_frame,
"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",
"Note",
]
tree: ttk.Treeview = ttk.Treeview(
table_frame, columns=columns, show="headings"
)
col_labels: List[str] = [
"Date",
"Depression",
"Anxiety",
"Sleep",
"Appetite",
"Bupropion 150mg",
"Hydroxyzine 25mg",
"Gabapentin 100mg",
"Propranolol 10mg",
"Note",
]
for col, label in zip(columns, col_labels):
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"),
("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_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."""
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(400, 300)
# Configure grid columns to expand properly
edit_win.grid_columnconfigure(1, weight=1)
# Unpack values
date, dep, anx, slp, app, bup, hydro, gaba, prop, note = values
# Create variables and fields
vars_dict = self._create_edit_fields(
edit_win, date, dep, anx, slp, app
)
# Medicine checkboxes
current_row = 6 # After the 5 fields (date, dep, anx, slp, app)
med_vars = self._create_medicine_checkboxes(
edit_win, current_row, bup, hydro, gaba, prop
)
vars_dict.update(med_vars)
# Note field
current_row += 1
vars_dict["note"] = tk.StringVar(value=str(note))
ttk.Label(edit_win, text="Note:").grid(
row=current_row, column=0, sticky="w", padx=5, pady=2
)
ttk.Entry(edit_win, textvariable=vars_dict["note"]).grid(
row=current_row, column=1, sticky="ew", padx=5, pady=2
)
# Buttons
current_row += 1
self._add_edit_window_buttons(
edit_win, current_row, vars_dict, callbacks
)
# Make window modal
edit_win.update_idletasks()
edit_win.focus_set()
edit_win.grab_set()
return edit_win
def _create_edit_fields(
self,
parent: tk.Toplevel,
date: str,
dep: int,
anx: int,
slp: int,
app: int,
) -> Dict[str, Union[tk.StringVar, tk.IntVar]]:
"""Create fields for editing entry values."""
vars_dict: Dict[str, Union[tk.StringVar, tk.IntVar]] = {}
# Ensure values are converted to appropriate types
try:
app = int(app) if app != "" else 0
except (ValueError, TypeError):
self.logger.warning(
f"Invalid appetite value: {app}, defaulting to 0"
)
app = 0
value_map = {
"date": date,
"depression": dep,
"anxiety": anx,
"sleep": slp,
"appetite": app,
}
fields = [
("Date", tk.StringVar, "date"),
("Depression (0-10)", tk.IntVar, "depression"),
("Anxiety (0-10)", tk.IntVar, "anxiety"),
("Sleep (0-10)", tk.IntVar, "sleep"),
("Appetite (0-10)", tk.IntVar, "appetite"),
]
for idx, (label, var_type, key) in enumerate(fields):
try:
value = value_map[key]
if var_type == tk.IntVar:
try:
value = int(float(value))
except (ValueError, TypeError):
value = 0
self.logger.warning(
f"Failed to convert {key} value: {value}, defaulting to 0"
)
else:
value = str(value)
except (ValueError, TypeError, KeyError):
value = 0 if var_type == tk.IntVar else ""
self.logger.warning(
f"Missing or invalid value for {key}, defaulting to {value}"
)
vars_dict[key] = var_type(value=value)
ttk.Label(parent, text=f"{label}:").grid(
row=idx + 1, column=0, sticky="w", padx=5, pady=2
)
if var_type == tk.IntVar:
self._create_scale_with_label(
parent, idx + 1, vars_dict[key], value
)
else:
ttk.Entry(parent, textvariable=vars_dict[key]).grid(
row=idx + 1, column=1, sticky="ew"
)
return vars_dict
def _create_scale_with_label(
self, parent: tk.Toplevel, row: int, var: tk.IntVar, value: int
) -> None:
"""Create a scale with a value label."""
scale_frame: ttk.Frame = ttk.Frame(parent)
scale_frame.grid(row=row, column=1, sticky="ew", padx=5, pady=2)
scale_frame.grid_columnconfigure(0, weight=1)
scale = ttk.Scale(
scale_frame, from_=0, to=10, variable=var, orient=tk.HORIZONTAL
)
scale.grid(row=0, column=0, sticky="ew", padx=5)
# Add a value label to show the current value
value_label = ttk.Label(scale_frame, width=3)
value_label.grid(row=0, column=1, padx=(5, 0))
# Update label when scale value changes
def update_label(event=None):
value_label.configure(text=str(var.get()))
scale.bind("<Motion>", update_label)
scale.bind("<ButtonRelease-1>", update_label)
update_label() # Set initial value
scale.set(value) # Explicitly set scale value
def _create_medicine_checkboxes(
self,
parent: tk.Toplevel,
row: int,
bup: int,
hydro: int,
gaba: int,
prop: int,
) -> Dict[str, tk.IntVar]:
"""Create medicine checkboxes in the edit window."""
ttk.Label(parent, text="Treatment:").grid(
row=row, column=0, sticky="w", padx=5, pady=2
)
medicine_frame: ttk.LabelFrame = ttk.LabelFrame(
parent, text="Medicine"
)
medicine_frame.grid(row=row, column=1, padx=0, pady=10, sticky="nsew")
medicine_vars: Dict[str, Tuple[int, str]] = {
"bupropion": (bup, "Bupropion 150mg"),
"hydroxyzine": (hydro, "Hydroxyzine 25mg"),
"gabapentin": (gaba, "Gabapentin 100mg"),
"propranolol": (prop, "Propranolol 10mg"),
}
vars_dict: Dict[str, tk.IntVar] = {}
for idx, (key, (value, label)) in enumerate(medicine_vars.items()):
vars_dict[key] = tk.IntVar(value=int(value))
ttk.Checkbutton(
medicine_frame, text=label, variable=vars_dict[key]
).grid(row=idx, column=0, sticky="w", padx=5, pady=2)
return vars_dict
def _add_edit_window_buttons(
self,
parent: tk.Toplevel,
row: int,
vars_dict: Dict[str, Any],
callbacks: Dict[str, Callable],
) -> None:
"""Add buttons to the edit window."""
button_frame: ttk.Frame = ttk.Frame(parent)
button_frame.grid(row=row, column=0, columnspan=2, pady=10)
# Save button
ttk.Button(
button_frame,
text="Save",
command=lambda: callbacks["save"](
parent,
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["note"].get(),
),
).pack(side="left", padx=5)
# Cancel button
ttk.Button(button_frame, text="Cancel", command=parent.destroy).pack(
side="left", padx=5
)
# Delete button
ttk.Button(
button_frame,
text="Delete",
command=lambda: callbacks["delete"](parent),
).pack(side="left", padx=5)