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
+51
View File
@@ -0,0 +1,51 @@
name: Build and Push Docker Image
run-name: ${{ gitea.actor }} is building and pushing TheChart Docker image 🚀
on:
push:
branches:
- main
- master
tags:
- "v*"
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: gitea-http.taildb3494.ts.net
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: gitea-http.taildb3494.ts.net/will/thechart
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
type=sha,format=short
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache
cache-to: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache,mode=max
+1
View File
@@ -7,3 +7,4 @@ __pycache__/
*.spec
*.log
logs/
.vscode/
+60 -11
View File
@@ -1,10 +1,13 @@
# Use a Python Alpine base image
FROM python:3.13-alpine
# Set the working directory in the container
ARG TARGET=thechart
ARG ICON=chart-671.png
ARG UID=1000
ARG GUID=1000
USER root
WORKDIR /app
# Install necessary system dependencies
RUN apk add --no-cache \
build-base \
libffi-dev \
@@ -15,17 +18,63 @@ RUN apk add --no-cache \
xorg-server \
xauth \
xvfb \
&& pip install --upgrade pip
font-terminus font-inconsolata font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
&& pip install --upgrade pip && pip install pyinstaller python-dotenv
# Copy the requirements file and install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY src/ .
# RUN export uid=$UID gid=$GUID
RUN mkdir -p /home/docker_user
RUN echo "docker_user:x:${UID}:${GUID}:docker_user,,,:/home/docker_user:/bin/bash" >> /etc/passwd
RUN echo "docker_user:x:${UID}:" >> /etc/group
RUN mkdir -p /etc/sudoers.d && echo "docker_user ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/docker_user
RUN chmod 0440 /etc/sudoers.d/docker_user
RUN chown -R ${UID}:${GUID} /home/docker_user
# Expose the port your application listens on (if applicable)
# EXPOSE 8000
COPY --chown=${UID}:${GUID} --chmod=765 ./src/ ./src/
COPY --chown=${UID}:${GUID} --chmod=765 ./.env .
COPY --chown=${UID}:${GUID} --chmod=765 ./thechart_data.csv .
COPY --chown=${UID}:${GUID} --chmod=765 ./${ICON} .
# Define the command to run your application
CMD ["python", "main.py"]
ARG HOME=/home/docker_user
ENV HOME=/home/docker_user
# Make sure the icon file and data file are in the right location
RUN cp -f ./${ICON} ./src/ && cp -f ./thechart_data.csv ./.env ./src/
# Debug the environment
RUN echo "Icon file is: ${ICON}"
RUN ls -la ./src/
# Use a shell command with variable expansion to make sure variables are properly substituted
RUN sh -c "pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon=./src/chart-671.png --add-data='.env:.' --add-data='./src/chart-671.png:.' --add-data='./src/thechart_data.csv:.' --distpath ${HOME}/${TARGET} ./src/main.py"
RUN chown -R ${UID}:${GUID} /home/docker_user/
RUN chmod -R 777 /home/docker_user/${TARGET}
# Set environment variables for X11 forwarding
ENV DISPLAY=:0
ENV XAUTHORITY=/tmp/.docker.xauth
ENV QT_X11_NO_MITSHM=1
# Create a startup script to handle X11 setup and application launch
# Create a proper entrypoint script with correct line endings
RUN echo '#!/bin/sh' > /home/docker_user/entrypoint.sh && \
echo 'set -e' >> /home/docker_user/entrypoint.sh && \
echo 'echo "Starting entrypoint script..."' >> /home/docker_user/entrypoint.sh && \
echo 'echo "DISPLAY=$DISPLAY"' >> /home/docker_user/entrypoint.sh && \
echo 'echo "XAUTHORITY=$XAUTHORITY"' >> /home/docker_user/entrypoint.sh && \
echo 'echo "HOME=$HOME"' >> /home/docker_user/entrypoint.sh && \
echo 'mkdir -p $(dirname $XAUTHORITY)' >> /home/docker_user/entrypoint.sh && \
echo 'touch $XAUTHORITY || echo "Warning: Could not create $XAUTHORITY file"' >> /home/docker_user/entrypoint.sh && \
echo 'xauth nlist $DISPLAY | sed -e "s/^..../ffff/" | xauth -f $XAUTHORITY nmerge - || echo "Warning: X auth setup failed"' >> /home/docker_user/entrypoint.sh && \
echo "echo \"Starting application: \$HOME/${TARGET}/${TARGET}\"" >> /home/docker_user/entrypoint.sh && \
echo "exec \$HOME/${TARGET}/${TARGET}" >> /home/docker_user/entrypoint.sh && \
chmod +x /home/docker_user/entrypoint.sh
USER docker_user
# Set the entrypoint to our startup script through shell
ENTRYPOINT ["/bin/sh"]
CMD ["-c", "/home/docker_user/entrypoint.sh"]
+1 -1
View File
@@ -13,7 +13,7 @@ setup-env:
build:
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE} --push .
install:
pyinstaller --name ${TARGET} --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data=".env:." src/main.py
pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data=".env:." --add-data='./src/chart-671.png:.' --add-data='./src/thechart_data.csv:.' src/main.py
cp -f ./dist/${TARGET} ${ROOT}/Applications/
cp -f ./deploy/${TARGET}.desktop ${ROOT}/.local/share/applications/
desktop-file-validate ${ROOT}/.local/share/applications/${TARGET}.desktop
+1 -1
View File
@@ -19,11 +19,11 @@ pytz = "==2025.2"
six = "==1.17.0"
tzdata = "==2025.2"
colorlog = "*"
dotenv = "*"
[dev-packages]
pre-commit = "*"
pyinstaller = "*"
dotenv = "*"
[requires]
python_version = "3.13"
Generated
+22 -22
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "61b1a4d194b2257d4bef97d275fe5d947559a2346020473a0c45cb49940608ab"
"sha256": "3f2c36deb68c43ab2082c8b3f7f37b33345ad3693c498101d5b1d1edf6ff3223"
},
"pipfile-spec": 6,
"requires": {
@@ -98,6 +98,13 @@
"markers": "python_version >= '3.8'",
"version": "==0.12.1"
},
"dotenv": {
"hashes": [
"sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"
],
"index": "pypi",
"version": "==0.9.9"
},
"fonttools": {
"hashes": [
"sha256:0162a6a37b0ca70d8505311d541e291cd6cab54d1a986ae3d2686c56c0581e8f",
@@ -522,6 +529,14 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
"version": "==2.9.0.post0"
},
"python-dotenv": {
"hashes": [
"sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc",
"sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"
],
"markers": "python_version >= '3.9'",
"version": "==1.1.1"
},
"pytz": {
"hashes": [
"sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3",
@@ -572,13 +587,6 @@
],
"version": "==0.4.0"
},
"dotenv": {
"hashes": [
"sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"
],
"index": "pypi",
"version": "==0.9.9"
},
"filelock": {
"hashes": [
"sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2",
@@ -650,19 +658,11 @@
},
"pyinstaller-hooks-contrib": {
"hashes": [
"sha256:06779d024f7d60dd75b05520923bba16b17df5f64073434b23e570ffb71094dc",
"sha256:223ae773733fb7a0ee9cb5e817480998a90a6c7a9c3d2b7b580d2dfa2b325751"
"sha256:b0c19dbe9a8428665d2c5c4538eaa16b683579aabd7f2ecd31f41b116c4e4e57",
"sha256:ba4fecc94eb761de2015cd8b1e674354e8a1f13ba65047602912f34c20fb510b"
],
"markers": "python_version >= '3.8'",
"version": "==2025.6"
},
"python-dotenv": {
"hashes": [
"sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc",
"sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"
],
"markers": "python_version >= '3.9'",
"version": "==1.1.1"
"version": "==2025.7"
},
"pyyaml": {
"hashes": [
@@ -733,11 +733,11 @@
},
"virtualenv": {
"hashes": [
"sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11",
"sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"
"sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56",
"sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0"
],
"markers": "python_version >= '3.8'",
"version": "==20.31.2"
"version": "==20.32.0"
}
}
}
+1 -1
View File
@@ -5,4 +5,4 @@ Name=Thechart
Exec=sh -c "/home/will/Applications/thechart /home/will/Documents/thechart_data.csv"
Icon=/home/will/Code/thechart/chart-671.png
Categories=Utility;
StartupWMClass=tk # Crucial for Dock icon persistence
StartupWMClass=Tk # Crucial for Dock icon persistence
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/bash
CONTAINER_ENGINE="docker" # podman | docker
VERSION="v1.0.0"
REGISTRY="gitea-http.taildb3494.ts.net/will/thechart"
if [ "$CONTAINER_ENGINE" == "podman" ];
then
buildah build \
-t $REGISTRY:$VERSION \
--platform linux/amd64,linux/arm64/v8 \
--no-cache .
else
DOCKER_BUILDKIT=1 \
docker buildx build \
--platform linux/amd64,linux/arm64/v8 \
-t $REGISTRY:$VERSION \
--no-cache \
--push .
fi
+4 -2
View File
@@ -7,6 +7,8 @@ services:
working_dir: '/app'
tty: true
volumes:
- ${SRC_PATH}:/app
- /tmp/.X11-unix:/tmp/.X11-unix
- ${XAUTHORITY:-~/.Xauthority}:/tmp/.docker.xauth:rw
environment:
- DISPLAY=${DISPLAY_IP}:0.0
- DISPLAY=${DISPLAY:-:0}
- XAUTHORITY=/tmp/.docker.xauth
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/bash
# Check for .env file and create if it doesn't exist
if [ ! -f .env ]; then
echo "Creating .env file..."
touch .env
fi
# Allow local X server connections
xhost +local:
# Set environment variables
export DISPLAY=":0" # Default to local display
# Try to get IP address if hostname -I is available
if command -v hostname >/dev/null 2>&1; then
if hostname --help 2>&1 | grep -q -- "-I"; then
IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "")
if [ -n "$IP" ]; then
export DISPLAY="$IP:0"
fi
fi
fi
export SRC_PATH=$(pwd)
export IMAGE="thechart:latest"
export XAUTHORITY=$HOME/.Xauthority
echo "Building and running the container..."
echo "Using DISPLAY=$DISPLAY"
echo "Using SRC_PATH=$SRC_PATH"
echo "Using XAUTHORITY=$XAUTHORITY"
# Check if debug mode is requested
if [ "$1" = "debug" ]; then
echo "Running in debug mode - will open shell instead of running app"
docker-compose build
docker run --rm -it \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v ${XAUTHORITY:-$HOME/.Xauthority}:/tmp/.docker.xauth:rw \
-e DISPLAY=${DISPLAY:-:0} \
-e XAUTHORITY=/tmp/.docker.xauth \
${IMAGE} /bin/sh
else
# First run with debug to see the container's internal state
echo "First entering container shell for debugging..."
docker run --rm -it \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v ${XAUTHORITY:-$HOME/.Xauthority}:/tmp/.docker.xauth:rw \
-e DISPLAY=${DISPLAY:-:0} \
-e XAUTHORITY=/tmp/.docker.xauth \
${IMAGE} /bin/sh
# Then continue with normal operation if needed
echo "Now running the container with docker-compose..."
docker-compose up --build
fi
# Disallow local X server connections when done
xhost -local:
+120
View File
@@ -0,0 +1,120 @@
import os
import csv
import logging
from typing import List, Union
import pandas as pd
class DataManager:
"""Handle all data operations for the application."""
def __init__(self, filename: str, logger: logging.Logger) -> None:
self.filename: str = filename
self.logger: logging.Logger = logger
self.initialize_csv()
def initialize_csv(self) -> None:
"""Create CSV file with headers if it doesn't exist."""
if not os.path.exists(self.filename):
with open(self.filename, mode="w", newline="") as file:
writer = csv.writer(file)
writer.writerow(
[
"date",
"depression",
"anxiety",
"sleep",
"appetite",
"bupropion",
"hydroxyzine",
"gabapentin",
"propranolol",
"note",
]
)
def load_data(self) -> pd.DataFrame:
"""Load data from CSV file."""
if (
not os.path.exists(self.filename)
or os.path.getsize(self.filename) == 0
):
self.logger.warning(
"CSV file is empty or doesn't exist. No data to load."
)
return pd.DataFrame()
try:
df: pd.DataFrame = pd.read_csv(
self.filename,
dtype={
"depression": int,
"anxiety": int,
"sleep": int,
"appetite": int,
"bupropion": int,
"hydroxyzine": int,
"gabapentin": int,
"propranolol": int,
"note": str,
"date": str,
},
).fillna("")
return df.sort_values(by="date").reset_index(drop=True)
except pd.errors.EmptyDataError:
self.logger.warning("CSV file is empty. No data to load.")
return pd.DataFrame()
except Exception as e:
self.logger.error(f"Error loading data: {str(e)}")
return pd.DataFrame()
def add_entry(self, entry_data: List[Union[str, int]]) -> bool:
"""Add a new entry to the CSV file."""
try:
with open(self.filename, mode="a", newline="") as file:
writer = csv.writer(file)
writer.writerow(entry_data)
return True
except Exception as e:
self.logger.error(f"Error adding entry: {str(e)}")
return False
def update_entry(self, date: str, values: List[Union[str, int]]) -> bool:
"""Update an existing entry identified by date."""
try:
df: pd.DataFrame = self.load_data()
# Find the row to update using date as a unique identifier
df.loc[
df["date"] == date,
[
"date",
"depression",
"anxiety",
"sleep",
"appetite",
"bupropion",
"hydroxyzine",
"gabapentin",
"propranolol",
"note",
],
] = values
df.to_csv(self.filename, index=False)
return True
except Exception as e:
self.logger.error(f"Error updating entry: {str(e)}")
return False
def delete_entry(self, date: str) -> bool:
"""Delete an entry identified by date."""
try:
df: pd.DataFrame = self.load_data()
# Remove the row with the matching date
df = df[df["date"] != date]
# Write the updated dataframe back to the CSV
df.to_csv(self.filename, index=False)
return True
except Exception as e:
self.logger.error(f"Error deleting entry: {str(e)}")
return False
+81
View File
@@ -0,0 +1,81 @@
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.axes import Axes
from tkinter import ttk
class GraphManager:
"""Handle all graph-related operations for the application."""
def __init__(self, parent_frame: ttk.LabelFrame) -> None:
self.parent_frame: ttk.LabelFrame = parent_frame
# Configure graph frame to expand
self.parent_frame.grid_rowconfigure(0, weight=1)
self.parent_frame.grid_columnconfigure(0, weight=1)
# Initialize matplotlib figure and canvas
self.fig: matplotlib.figure.Figure
self.ax: Axes
self.fig, self.ax = plt.subplots()
self.canvas: FigureCanvasTkAgg = FigureCanvasTkAgg(
figure=self.fig, master=self.parent_frame
)
self.canvas.get_tk_widget().pack(fill="both", expand=True)
def update_graph(self, df: pd.DataFrame) -> None:
"""Update the graph with new data."""
self.ax.clear()
if not df.empty:
# Convert dates and sort
df = df.copy() # Create a copy to avoid modifying the original
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values(by="date")
df.set_index(keys="date", inplace=True)
# Plot data series
self._plot_series(
df, "depression", "Depression (0:good, 10:bad)", "o", "-"
)
self._plot_series(
df, "anxiety", "Anxiety (0:good, 10:bad)", "o", "-"
)
self._plot_series(
df, "sleep", "Sleep (0:bad, 10:good)", "o", "dashed"
)
self._plot_series(
df, "appetite", "Appetite (0:bad, 10:good)", "o", "dashed"
)
# Configure graph appearance
self.ax.legend()
self.ax.set_title("Medication Effects Over Time")
self.ax.set_xlabel("Date")
self.ax.set_ylabel("Rating (0-10)")
self.fig.autofmt_xdate()
# Redraw the canvas
self.canvas.draw()
def _plot_series(
self,
df: pd.DataFrame,
column: str,
label: str,
marker: str,
linestyle: str,
) -> None:
"""Helper method to plot a data series."""
self.ax.plot(
df.index,
df[column],
marker=marker,
linestyle=linestyle,
label=label,
)
def close(self) -> None:
"""Clean up resources."""
plt.close(self.fig)
+1 -10
View File
@@ -4,8 +4,6 @@ from constants import LOG_PATH, LOG_CLEAR, LOG_LEVEL
if not os.path.exists(LOG_PATH):
os.mkdir(LOG_PATH)
else:
pass
log_files = (
f"{LOG_PATH}/app.log",
@@ -13,10 +11,7 @@ log_files = (
f"{LOG_PATH}/app.error.log",
)
if LOG_LEVEL == "DEBUG":
testing_mode = True
else:
testing_mode = False
testing_mode = LOG_LEVEL == "DEBUG"
logger = init_logger(__name__, testing_mode=testing_mode)
@@ -26,10 +21,6 @@ if LOG_CLEAR == "True":
if os.path.exists(log_file):
with open(log_file, "r+") as t:
t.truncate(0)
else:
pass
except Exception as e:
logger.error(e)
raise
else:
pass
+179 -530
View File
@@ -1,399 +1,134 @@
import csv
import logging
import os
import tkinter as tk
from tkinter import messagebox, ttk
import matplotlib.pyplot as plt
import pandas as pd
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import sys
from PIL import Image, ImageTk
import tkinter as tk
from tkinter import messagebox
from typing import Dict, List, Tuple, Any, Callable, Union
import pandas as pd
from init import logger
from constants import LOG_LEVEL
from data_manager import DataManager
from graph_manager import GraphManager
from ui_manager import UIManager
class MedTrackerApp:
def __init__(self, root):
self.root = root
def __init__(self, root: tk.Tk) -> None:
self.root: tk.Tk = root
self.root.resizable(True, True)
self.root.title("Thechart - medication tracker")
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
# self.root.iconbitmap("app_icon.ico")
# screen_width = self.root.winfo_screenwidth()
# screen_height = self.root.winfo_screenheight()
# self.root.geometry(f"{screen_width}x{screen_height}")
# self.root.configure(background='gold')
# self.root.lift()
# self.root.attributes("-topmost", True)
# self.root.geometry("800x600")
# Set up data file
self.filename: str = "thechart_data.csv"
if len(sys.argv) > 1:
script_name = sys.argv[0]
first_argument = sys.argv[1]
first_argument: str = sys.argv[1]
if LOG_LEVEL == "DEBUG":
logger.debug(f"Script name: {script_name}")
logger.debug(f"Script name: {sys.argv[0]}")
logger.debug(f"First argument: {first_argument}")
if os.path.exists(first_argument):
self.filename = first_argument
logger.info(f"Using data file: {first_argument}")
else:
logger.warning(
f"Data file {first_argument} does not exist."
f" Using default file: {self.filename}"
f"Data file {first_argument} does not exist. Using default file: {self.filename}"
)
self.make_icon(
img="/home/will/Code/thechart/chart-671.png",
logger=logger,
# Initialize managers
self.ui_manager: UIManager = UIManager(root, logger)
self.data_manager: DataManager = DataManager(self.filename, logger)
# Set up application icon
icon_path: str = "chart-671.png"
if not os.path.exists(icon_path) and os.path.exists("./chart-671.png"):
icon_path = "./chart-671.png"
self.ui_manager.setup_icon(img_path=icon_path)
# Set up the main application UI
self._setup_main_ui()
def _setup_main_ui(self) -> None:
"""Set up the main UI components."""
import tkinter.ttk as ttk
# --- Main Frame ---
main_frame: ttk.Frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky="nsew")
# Configure root window grid
self.root.grid_rowconfigure(0, weight=1)
self.root.grid_columnconfigure(0, weight=1)
# Configure main frame grid for scaling
for i in range(2):
main_frame.grid_rowconfigure(i, weight=1 if i == 1 else 0)
main_frame.grid_columnconfigure(i, weight=3 if i == 1 else 1)
logger.debug("Main frame and root grid configured for scaling.")
# --- Create Graph Frame ---
graph_frame: ttk.Frame = self.ui_manager.create_graph_frame(main_frame)
self.graph_manager: GraphManager = GraphManager(graph_frame)
# --- Create Input Frame ---
input_ui: Dict[str, Any] = self.ui_manager.create_input_frame(
main_frame
)
self.input_frame: ttk.Frame = input_ui["frame"]
self.symptom_vars: Dict[str, tk.IntVar] = input_ui["symptom_vars"]
self.medicine_vars: Dict[str, List[Union[tk.IntVar, ttk.Spinbox]]] = (
input_ui["medicine_vars"]
)
self.note_var: tk.StringVar = input_ui["note_var"]
self.date_var: tk.StringVar = input_ui["date_var"]
# Add buttons to input frame
self.ui_manager.add_buttons(
self.input_frame,
[
{
"text": "Add Entry",
"command": self.add_entry,
"fill": "both",
"expand": True,
},
{"text": "Quit", "command": self.on_closing},
],
)
self.filename = "thechart_data.csv"
self.initialize_csv()
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0)
# --- Input Frame ---
input_frame = ttk.LabelFrame(main_frame, text="New Entry")
input_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
ttk.Label(input_frame, text="Depression (0-10):").grid(
row=0, column=0, sticky="w", padx=5, pady=2
# --- Create Table Frame ---
table_ui: Dict[str, Any] = self.ui_manager.create_table_frame(
main_frame
)
self.depression_var = tk.IntVar()
ttk.Scale(
input_frame,
from_=0,
to=10,
orient=tk.HORIZONTAL,
variable=self.depression_var,
).grid(row=0, column=1, sticky="ew")
ttk.Label(input_frame, text="Anxiety (0-10):").grid(
row=1, column=0, sticky="w", padx=5, pady=2
)
self.anxiety_var = tk.IntVar()
ttk.Scale(
input_frame,
from_=0,
to=10,
orient=tk.HORIZONTAL,
variable=self.anxiety_var,
).grid(row=1, column=1, sticky="ew")
ttk.Label(input_frame, text="Sleep Quality (0-10):").grid(
row=2, column=0, sticky="w", padx=5, pady=2
)
self.sleep_var = tk.IntVar()
ttk.Scale(
input_frame,
from_=0,
to=10,
orient=tk.HORIZONTAL,
variable=self.sleep_var,
).grid(row=2, column=1, sticky="ew")
ttk.Label(input_frame, text="Appetite (0-10):").grid(
row=3, column=0, sticky="w", padx=5, pady=2
)
self.appetite_var = tk.IntVar()
ttk.Scale(
input_frame,
from_=0,
to=10,
orient=tk.HORIZONTAL,
variable=self.appetite_var,
).grid(row=3, column=1, sticky="ew")
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")
self.bupropion_var = tk.IntVar(value=0)
self.hydroxyzine_var = tk.IntVar(value=0)
self.gabapentin_var = tk.IntVar(value=0)
self.propranolol_var = tk.IntVar(value=0)
ttk.Checkbutton(
medicine_frame,
text="Bupropion 150mg",
variable=self.bupropion_var,
name="bupropion_check",
command=lambda: self.toggle_checkbox(obj_name="bupropion_check"),
).grid(row=0, column=0, sticky="w", padx=5, pady=2)
ttk.Checkbutton(
medicine_frame,
text="Hydroxyzine 25mg",
variable=self.hydroxyzine_var,
name="hydroxyzine_check",
command=lambda: self.toggle_checkbox(obj_name="hydroxyzine_check"),
).grid(row=1, column=0, sticky="w", padx=5, pady=2)
ttk.Checkbutton(
medicine_frame,
text="Gabapentin 100mg",
variable=self.gabapentin_var,
name="gabapentin_check",
command=lambda: self.toggle_checkbox(obj_name="gabapentin_check"),
).grid(row=2, column=0, sticky="w", padx=5, pady=2)
ttk.Checkbutton(
medicine_frame,
text="Propranolol 10mg",
name="propranolol_check",
variable=self.propranolol_var,
command=lambda: self.toggle_checkbox(obj_name="propranolol_check"),
).grid(row=3, column=0, sticky="w", padx=5, pady=2)
ttk.Label(input_frame, text="Note:").grid(
row=5, column=0, sticky="w", padx=5, pady=2
)
self.note_var = tk.StringVar()
ttk.Entry(input_frame, textvariable=self.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
)
self.date_var = tk.StringVar()
ttk.Entry(
input_frame, textvariable=self.date_var, justify="center"
).grid(row=6, column=1, sticky="ew", padx=5, pady=2)
button_frame = ttk.Frame(input_frame)
button_frame.grid(row=7, column=0, columnspan=2, pady=10)
ttk.Button(
button_frame, text="Add Entry", command=self.add_entry
).pack(side="left", padx=5, fill="both", expand=True)
ttk.Button(button_frame, text="Quit", command=self.on_closing).pack(
side="left", padx=5
)
# --- Table Frame ---
table_frame = ttk.LabelFrame(
main_frame, text="Log (Double-click to edit)"
)
table_frame.grid(row=1, column=1, padx=10, pady=10, sticky="nsew")
self.tree = ttk.Treeview(
table_frame,
columns=(
"Date",
"Depression",
"Anxiety",
"Sleep",
"Appetite",
"Bupropion",
"Hydroxyzine",
"Gabapentin",
"Propranolol",
"Note",
),
show="headings",
)
self.tree.heading("Date", text="Date")
self.tree.heading("Depression", text="Depression")
self.tree.heading("Anxiety", text="Anxiety")
self.tree.heading("Sleep", text="Sleep")
self.tree.heading("Appetite", text="Appetite")
self.tree.heading("Bupropion", text="Bupropion 150mg")
self.tree.heading("Hydroxyzine", text="Hydroxyzine 25mg")
self.tree.heading("Gabapentin", text="Gabapentin 100mg")
self.tree.heading("Propranolol", text="Propranolol 10mg")
self.tree.heading("Note", text="Note")
self.tree.column("Date", width=80, anchor="center")
self.tree.column("Depression", width=80, anchor="center")
self.tree.column("Anxiety", width=80, anchor="center")
self.tree.column("Sleep", width=80, anchor="center")
self.tree.column("Appetite", width=80, anchor="center")
self.tree.column("Bupropion", width=120, anchor="center")
self.tree.column("Hydroxyzine", width=120, anchor="center")
self.tree.column("Gabapentin", width=120, anchor="center")
self.tree.column("Propranolol", width=120, anchor="center")
self.tree.column("Note", width=300, anchor="w")
# --- Bind double-click event ---
self.tree: ttk.Treeview = table_ui["tree"]
self.tree.bind("<Double-1>", self.on_double_click)
self.tree.pack(side="left", fill="both", expand=True)
scrollbar = ttk.Scrollbar(
table_frame, orient="vertical", command=self.tree.yview
)
self.tree.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side="right", fill="y")
# --- Graph Frame ---
graph_frame = ttk.LabelFrame(main_frame, text="Evolution")
graph_frame.grid(
row=0, column=0, columnspan=2, padx=10, pady=10, sticky="nsew"
)
self.fig, self.ax = plt.subplots()
self.canvas = FigureCanvasTkAgg(figure=self.fig, master=graph_frame)
self.canvas.get_tk_widget().pack(fill="both", expand=True)
# Load data
self.load_data()
def toggle_checkbox(obj_name: str) -> None:
if ttk.Checkbutton.nametowidget(name=obj_name).get():
ttk.Checkbutton.nametowidget(name=obj_name).set(False)
else:
ttk.Checkbutton.nametowidget(name=obj_name).set(True)
def on_double_click(self, event: any) -> None:
def on_double_click(self, event: tk.Event) -> None:
"""Handle double-click event to edit an entry."""
logger.debug("Double-click event triggered on treeview.")
if len(self.tree.get_children()) > 0:
item_id = self.tree.selection()[0]
item_values = self.tree.item(item_id, "values")
self.create_edit_window(item_id, item_values)
logger.debug(f"Editing item_id={item_id}, values={item_values}")
self._create_edit_window(item_id, item_values)
def create_edit_window(self, item_id: str, values: tuple) -> None:
def _create_edit_window(
self, item_id: str, values: Tuple[str, ...]
) -> None:
"""Create a new Toplevel window for editing an entry."""
edit_win = tk.Toplevel(master=self.root)
edit_win.title("Edit Entry")
# Define callbacks for edit window buttons
callbacks: Dict[str, Callable] = {
"save": self._save_edit,
"delete": lambda win: self._delete_entry(win, item_id),
}
# Unpack values
date, dep, anx, slp, app, bup, hydro, gaba, prop, note = values
# Create edit window using UI manager
_: tk.Toplevel = self.ui_manager.create_edit_window(values, callbacks)
# Create variables for the widgets
date_var = tk.StringVar(value=str(date))
dep_var = tk.IntVar(value=int(dep))
anx_var = tk.IntVar(value=int(anx))
slp_var = tk.IntVar(value=int(slp))
app_var = tk.IntVar(value=int(app))
bup_var = tk.IntVar(value=int(bup))
hydro_var = tk.IntVar(value=int(hydro))
gaba_var = tk.IntVar(value=int(gaba))
prop_var = tk.IntVar(value=int(prop))
note_var = tk.StringVar(value=str(note))
# Create form widgets
ttk.Label(edit_win, text="Depression:").grid(
row=1, column=0, sticky="w", padx=5, pady=2
)
ttk.Scale(
edit_win, from_=0, to=10, variable=dep_var, orient=tk.HORIZONTAL
).grid(row=1, column=1, sticky="ew")
ttk.Label(edit_win, text="Anxiety:").grid(
row=2, column=0, sticky="w", padx=5, pady=2
)
ttk.Scale(
edit_win, from_=0, to=10, variable=anx_var, orient=tk.HORIZONTAL
).grid(row=2, column=1, sticky="ew")
ttk.Label(edit_win, text="Sleep:").grid(
row=3, column=0, sticky="w", padx=5, pady=2
)
ttk.Scale(
edit_win, from_=0, to=10, variable=slp_var, orient=tk.HORIZONTAL
).grid(row=3, column=1, sticky="ew")
ttk.Label(edit_win, text="Appetite:").grid(
row=4, column=0, sticky="w", padx=5, pady=2
)
ttk.Scale(
edit_win, from_=0, to=10, variable=app_var, orient=tk.HORIZONTAL
).grid(row=4, column=1, sticky="ew")
ttk.Label(edit_win, text="Treatment:").grid(
row=5, column=0, sticky="w", padx=5, pady=2
)
medicine_frame = ttk.LabelFrame(edit_win, text="Medicine")
medicine_frame.grid(row=5, column=1, padx=0, pady=10, sticky="nsew")
ttk.Checkbutton(
medicine_frame,
text="Bupropion 150mg",
name="bupropion_check",
variable=bup_var,
command=lambda: self.toggle_checkbox(obj_name="bupropion_check"),
).grid(row=0, column=0, sticky="w", padx=5, pady=2)
ttk.Checkbutton(
medicine_frame,
text="Hydroxyzine 25mg",
name="hydroxyzine_check",
variable=hydro_var,
command=lambda: self.toggle_checkbox(obj_name="hydroxyzine_check"),
).grid(row=1, column=0, sticky="w", padx=5, pady=2)
ttk.Checkbutton(
medicine_frame,
text="Gabapentin 100mg",
name="gabapentin_check",
variable=gaba_var,
command=lambda: self.toggle_checkbox(obj_name="gabapentin_check"),
).grid(row=2, column=0, sticky="w", padx=5, pady=2)
ttk.Checkbutton(
medicine_frame,
text="Propranolol 10mg",
name="propranolol_check",
variable=prop_var,
command=lambda: self.toggle_checkbox(obj_name="propranolol_check"),
).grid(row=3, column=0, sticky="w", padx=5, pady=2)
ttk.Label(edit_win, text="Note:").grid(
row=6, column=0, sticky="w", padx=5, pady=2
)
ttk.Entry(edit_win, textvariable=note_var).grid(
row=6, column=1, sticky="ew"
)
ttk.Label(edit_win, text="Date:").grid(
row=7, column=0, sticky="w", padx=5, pady=2
)
ttk.Entry(edit_win, textvariable=date_var).grid(
row=7, column=1, sticky="ew"
)
# Save and Cancel buttons
save_btn = ttk.Button(
edit_win,
text="Save",
command=lambda: self.save_edit(
edit_win,
date_var.get(),
dep_var.get(),
anx_var.get(),
slp_var.get(),
app_var.get(),
bup_var.get(),
hydro_var.get(),
gaba_var.get(),
prop_var.get(),
note_var.get(),
),
)
save_btn.grid(row=8, column=0, padx=5, pady=10)
cancel_btn = ttk.Button(
edit_win, text="Cancel", command=edit_win.destroy
)
cancel_btn.grid(row=8, column=1, padx=5, pady=10)
delete_btn = ttk.Button(
edit_win,
text="Delete",
command=lambda: self.delete_entry(edit_win, item_id),
)
delete_btn.grid(row=8, column=2, padx=5, pady=10)
def save_edit(
def _save_edit(
self,
edit_win: tk.Toplevel,
date: str,
@@ -407,207 +142,121 @@ class MedTrackerApp:
prop: int,
note: str,
) -> None:
"""
Save the edited data to the CSV file.
"""
df = pd.read_csv(self.filename)
# Find the row to update using the date as a unique identifier
df.loc[
df["date"] == date,
[
"date",
"depression",
"anxiety",
"sleep",
"appetite",
"bupropion",
"hydroxyzine",
"gabapentin",
"propranolol",
"note",
],
] = [date, dep, anx, slp, app, bup, hydro, gaba, prop, note]
# Write the updated dataframe back to the CSV
df.to_csv(self.filename, index=False)
"""Save the edited data to the CSV file."""
values: List[Union[str, int]] = [
date,
dep,
anx,
slp,
app,
bup,
hydro,
gaba,
prop,
note,
]
edit_win.destroy()
messagebox.showinfo(
"Success", "Entry updated successfully!", parent=self.root
)
self.clear_entries()
self.load_data()
if self.data_manager.update_entry(date, values):
edit_win.destroy()
messagebox.showinfo(
"Success", "Entry updated successfully!", parent=self.root
)
self._clear_entries()
self.load_data()
else:
messagebox.showerror(
"Error", "Failed to save changes", parent=edit_win
)
def on_closing(self) -> None:
if messagebox.askokcancel(
"Quit", "Do you want to quit the application?", parent=self.root
):
plt.close(self.fig)
self.graph_manager.close()
self.root.destroy()
def initialize_csv(self) -> None:
if not os.path.exists(self.filename):
with open(self.filename, mode="w", newline="") as file:
writer = csv.writer(file)
writer.writerow(
[
"date",
"depression",
"anxiety",
"sleep",
"appetite",
"bupropion",
"hydroxyzine",
"gabapentin",
"propranolol",
"note",
]
)
def add_entry(self) -> None:
with open(self.filename, mode="a", newline="") as file:
writer = csv.writer(file)
writer.writerow(
[
self.date_var.get(),
self.depression_var.get(),
self.anxiety_var.get(),
self.sleep_var.get(),
self.appetite_var.get(),
self.bupropion_var.get(),
self.hydroxyzine_var.get(),
self.gabapentin_var.get(),
self.propranolol_var.get(),
self.note_var.get(),
]
"""Add a new entry to the CSV file."""
entry: List[Union[str, int]] = [
self.date_var.get(),
self.symptom_vars["depression"].get(),
self.symptom_vars["anxiety"].get(),
self.symptom_vars["sleep"].get(),
self.symptom_vars["appetite"].get(),
self.medicine_vars["bupropion"][0].get(),
self.medicine_vars["hydroxyzine"][0].get(),
self.medicine_vars["gabapentin"][0].get(),
self.medicine_vars["propranolol"][0].get(),
self.note_var.get(),
]
logger.debug(f"Adding entry: {entry}")
if self.data_manager.add_entry(entry):
messagebox.showinfo(
"Success", "Entry added successfully!", parent=self.root
)
self._clear_entries()
self.load_data()
else:
messagebox.showerror(
"Error", "Failed to add entry", parent=self.root
)
messagebox.showinfo(
"Success", "Entry added successfully!", parent=self.root
)
self.clear_entries()
self.load_data()
def delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None:
"""
Delete the selected entry from the CSV file.
"""
def _delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None:
"""Delete the selected entry from the CSV file."""
logger.debug(f"Delete requested for item_id={item_id}")
if messagebox.askyesno(
"Delete Entry",
"Are you sure you want to delete this entry?",
parent=edit_win,
):
df = pd.read_csv(self.filename)
# Get the date of the entry to delete
date = self.tree.item(item_id, "values")[0]
# Remove the row with the matching date
df = df[df["date"] != date]
# Write the updated dataframe back to the CSV
df.to_csv(self.filename, index=False)
date: str = self.tree.item(item_id, "values")[0]
logger.debug(f"Deleting entry with date={date}")
edit_win.destroy()
messagebox.showinfo(
"Success", "Entry deleted successfully!", parent=edit_win
)
self.load_data()
if self.data_manager.delete_entry(date):
edit_win.destroy()
messagebox.showinfo(
"Success", "Entry deleted successfully!", parent=edit_win
)
self.load_data()
else:
messagebox.showerror(
"Error", "Failed to delete entry", parent=edit_win
)
def clear_entries(self) -> None:
def _clear_entries(self) -> None:
"""Clear all input fields."""
logger.debug("Clearing input fields.")
self.date_var.set("")
self.depression_var.set(0)
self.anxiety_var.set(0)
self.sleep_var.set(0)
self.appetite_var.set(0)
self.bupropion_var.set(False)
self.hydroxyzine_var.set(False)
self.gabapentin_var.set(False)
self.propranolol_var.set(False)
for key in self.symptom_vars:
self.symptom_vars[key].set(0)
for key in self.medicine_vars:
self.medicine_vars[key][0].set(0)
self.note_var.set("")
def load_data(self) -> None:
"""Load data from the CSV file into the table and graph."""
logger.debug("Loading data from CSV.")
# Clear existing data in the treeview
for i in self.tree.get_children():
self.tree.delete(i)
if (
os.path.exists(self.filename)
and os.path.getsize(self.filename) > 0
):
try:
df = pd.read_csv(
self.filename,
dtype={
"depression": int,
"anxiety": int,
"sleep": int,
"appetite": int,
"bupropion": int,
"hydroxyzine": int,
"gabapentin": int,
"propranolol": int,
"note": str,
"date": str,
},
).fillna("")
df = df.sort_values(by="date").reset_index(drop=True)
for index, row in df.iterrows():
self.tree.insert(parent="", index="end", values=list(row))
self.update_graph(df)
except pd.errors.EmptyDataError:
self.update_graph(pd.DataFrame())
# Load data from the CSV file
df: pd.DataFrame = self.data_manager.load_data()
def update_graph(self, df: pd.DataFrame) -> None:
self.ax.clear()
# Update the treeview with the data
if not df.empty:
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values(by="date")
df.set_index(keys="date", inplace=True)
self.ax.plot(
df.index,
df["depression"],
marker="o",
linestyle="-",
label="Depression (0:good, 10:bad)",
)
self.ax.plot(
df.index,
df["anxiety"],
marker="o",
linestyle="-",
label="Anxiety (0:good, 10:bad)",
)
self.ax.plot(
df.index,
df["sleep"],
marker="o",
linestyle="dashed",
label="Sleep (0:bad, 10:good)",
)
self.ax.plot(
df.index,
df["appetite"],
marker="o",
linestyle="dashed",
label="Appetite (0:bad, 10:good)",
)
self.ax.legend()
self.ax.set_title("Medication Effects Over Time")
self.ax.set_xlabel("Date")
self.ax.set_ylabel("Rating (0-10)")
self.fig.autofmt_xdate()
self.canvas.draw()
for index, row in df.iterrows():
self.tree.insert(parent="", index="end", values=list(row))
logger.debug(f"Loaded {len(df)} entries into treeview.")
def make_icon(self, img: str, logger: logging.Logger) -> None:
try:
icon_image = Image.open(img)
icon_image = icon_image.resize(
size=(32, 32), resample=Image.Resampling.NEAREST
)
icon_photo = ImageTk.PhotoImage(image=icon_image)
self.root.iconphoto(True, icon_photo)
self.root.wm_iconphoto(True, icon_photo)
except FileNotFoundError:
logger.warning("Icon file not found.")
# Update the graph
self.graph_manager.update_graph(df)
if __name__ == "__main__":
root = tk.Tk()
app = MedTrackerApp(root)
root: tk.Tk = tk.Tk()
app: MedTrackerApp = MedTrackerApp(root)
root.mainloop()
+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)