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

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
.gitignore vendored
View File

@@ -7,3 +7,4 @@ __pycache__/
*.spec *.spec
*.log *.log
logs/ logs/
.vscode/

View File

@@ -1,10 +1,13 @@
# Use a Python Alpine base image
FROM python:3.13-alpine 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 WORKDIR /app
# Install necessary system dependencies
RUN apk add --no-cache \ RUN apk add --no-cache \
build-base \ build-base \
libffi-dev \ libffi-dev \
@@ -15,17 +18,63 @@ RUN apk add --no-cache \
xorg-server \ xorg-server \
xauth \ xauth \
xvfb \ 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 . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code # RUN export uid=$UID gid=$GUID
COPY src/ . 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) COPY --chown=${UID}:${GUID} --chmod=765 ./src/ ./src/
# EXPOSE 8000 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 ARG HOME=/home/docker_user
CMD ["python", "main.py"] 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"]

View File

@@ -13,7 +13,7 @@ setup-env:
build: build:
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE} --push . docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE} --push .
install: 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 ./dist/${TARGET} ${ROOT}/Applications/
cp -f ./deploy/${TARGET}.desktop ${ROOT}/.local/share/applications/ cp -f ./deploy/${TARGET}.desktop ${ROOT}/.local/share/applications/
desktop-file-validate ${ROOT}/.local/share/applications/${TARGET}.desktop desktop-file-validate ${ROOT}/.local/share/applications/${TARGET}.desktop

View File

@@ -19,11 +19,11 @@ pytz = "==2025.2"
six = "==1.17.0" six = "==1.17.0"
tzdata = "==2025.2" tzdata = "==2025.2"
colorlog = "*" colorlog = "*"
dotenv = "*"
[dev-packages] [dev-packages]
pre-commit = "*" pre-commit = "*"
pyinstaller = "*" pyinstaller = "*"
dotenv = "*"
[requires] [requires]
python_version = "3.13" python_version = "3.13"

44
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "61b1a4d194b2257d4bef97d275fe5d947559a2346020473a0c45cb49940608ab" "sha256": "3f2c36deb68c43ab2082c8b3f7f37b33345ad3693c498101d5b1d1edf6ff3223"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -98,6 +98,13 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==0.12.1" "version": "==0.12.1"
}, },
"dotenv": {
"hashes": [
"sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"
],
"index": "pypi",
"version": "==0.9.9"
},
"fonttools": { "fonttools": {
"hashes": [ "hashes": [
"sha256:0162a6a37b0ca70d8505311d541e291cd6cab54d1a986ae3d2686c56c0581e8f", "sha256:0162a6a37b0ca70d8505311d541e291cd6cab54d1a986ae3d2686c56c0581e8f",
@@ -522,6 +529,14 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
"version": "==2.9.0.post0" "version": "==2.9.0.post0"
}, },
"python-dotenv": {
"hashes": [
"sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc",
"sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"
],
"markers": "python_version >= '3.9'",
"version": "==1.1.1"
},
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3",
@@ -572,13 +587,6 @@
], ],
"version": "==0.4.0" "version": "==0.4.0"
}, },
"dotenv": {
"hashes": [
"sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"
],
"index": "pypi",
"version": "==0.9.9"
},
"filelock": { "filelock": {
"hashes": [ "hashes": [
"sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2",
@@ -650,19 +658,11 @@
}, },
"pyinstaller-hooks-contrib": { "pyinstaller-hooks-contrib": {
"hashes": [ "hashes": [
"sha256:06779d024f7d60dd75b05520923bba16b17df5f64073434b23e570ffb71094dc", "sha256:b0c19dbe9a8428665d2c5c4538eaa16b683579aabd7f2ecd31f41b116c4e4e57",
"sha256:223ae773733fb7a0ee9cb5e817480998a90a6c7a9c3d2b7b580d2dfa2b325751" "sha256:ba4fecc94eb761de2015cd8b1e674354e8a1f13ba65047602912f34c20fb510b"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2025.6" "version": "==2025.7"
},
"python-dotenv": {
"hashes": [
"sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc",
"sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"
],
"markers": "python_version >= '3.9'",
"version": "==1.1.1"
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
@@ -733,11 +733,11 @@
}, },
"virtualenv": { "virtualenv": {
"hashes": [ "hashes": [
"sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56",
"sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af" "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==20.31.2" "version": "==20.32.0"
} }
} }
} }

View File

@@ -5,4 +5,4 @@ Name=Thechart
Exec=sh -c "/home/will/Applications/thechart /home/will/Documents/thechart_data.csv" Exec=sh -c "/home/will/Applications/thechart /home/will/Documents/thechart_data.csv"
Icon=/home/will/Code/thechart/chart-671.png Icon=/home/will/Code/thechart/chart-671.png
Categories=Utility; Categories=Utility;
StartupWMClass=tk # Crucial for Dock icon persistence StartupWMClass=Tk # Crucial for Dock icon persistence

20
docker-build.sh Executable file
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

View File

@@ -7,6 +7,8 @@ services:
working_dir: '/app' working_dir: '/app'
tty: true tty: true
volumes: volumes:
- ${SRC_PATH}:/app - /tmp/.X11-unix:/tmp/.X11-unix
- ${XAUTHORITY:-~/.Xauthority}:/tmp/.docker.xauth:rw
environment: environment:
- DISPLAY=${DISPLAY_IP}:0.0 - DISPLAY=${DISPLAY:-:0}
- XAUTHORITY=/tmp/.docker.xauth

59
run-container.sh Executable file
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
src/data_manager.py Normal file
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
src/graph_manager.py Normal file
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)

View File

@@ -4,8 +4,6 @@ from constants import LOG_PATH, LOG_CLEAR, LOG_LEVEL
if not os.path.exists(LOG_PATH): if not os.path.exists(LOG_PATH):
os.mkdir(LOG_PATH) os.mkdir(LOG_PATH)
else:
pass
log_files = ( log_files = (
f"{LOG_PATH}/app.log", f"{LOG_PATH}/app.log",
@@ -13,10 +11,7 @@ log_files = (
f"{LOG_PATH}/app.error.log", f"{LOG_PATH}/app.error.log",
) )
if LOG_LEVEL == "DEBUG": testing_mode = LOG_LEVEL == "DEBUG"
testing_mode = True
else:
testing_mode = False
logger = init_logger(__name__, testing_mode=testing_mode) logger = init_logger(__name__, testing_mode=testing_mode)
@@ -26,10 +21,6 @@ if LOG_CLEAR == "True":
if os.path.exists(log_file): if os.path.exists(log_file):
with open(log_file, "r+") as t: with open(log_file, "r+") as t:
t.truncate(0) t.truncate(0)
else:
pass
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
raise raise
else:
pass

View File

@@ -1,399 +1,134 @@
import csv
import logging
import os 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 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 init import logger
from constants import LOG_LEVEL from constants import LOG_LEVEL
from data_manager import DataManager
from graph_manager import GraphManager
from ui_manager import UIManager
class MedTrackerApp: class MedTrackerApp:
def __init__(self, root): def __init__(self, root: tk.Tk) -> None:
self.root: tk.Tk = root
self.root = root
self.root.resizable(True, True) self.root.resizable(True, True)
self.root.title("Thechart - medication tracker") self.root.title("Thechart - medication tracker")
self.root.protocol("WM_DELETE_WINDOW", self.on_closing) 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: if len(sys.argv) > 1:
script_name = sys.argv[0] first_argument: str = sys.argv[1]
first_argument = sys.argv[1]
if LOG_LEVEL == "DEBUG": 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}") logger.debug(f"First argument: {first_argument}")
if os.path.exists(first_argument): if os.path.exists(first_argument):
self.filename = first_argument self.filename = first_argument
logger.info(f"Using data file: {first_argument}") logger.info(f"Using data file: {first_argument}")
else: else:
logger.warning( logger.warning(
f"Data file {first_argument} does not exist." f"Data file {first_argument} does not exist. Using default file: {self.filename}"
f" Using default file: {self.filename}"
) )
self.make_icon( # Initialize managers
img="/home/will/Code/thechart/chart-671.png", self.ui_manager: UIManager = UIManager(root, logger)
logger=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" # --- Create Table Frame ---
self.initialize_csv() table_ui: Dict[str, Any] = self.ui_manager.create_table_frame(
main_frame
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
) )
self.depression_var = tk.IntVar() self.tree: ttk.Treeview = table_ui["tree"]
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.bind("<Double-1>", self.on_double_click) 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() self.load_data()
def toggle_checkbox(obj_name: str) -> None: def on_double_click(self, event: tk.Event) -> 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:
"""Handle double-click event to edit an entry.""" """Handle double-click event to edit an entry."""
logger.debug("Double-click event triggered on treeview.")
if len(self.tree.get_children()) > 0: if len(self.tree.get_children()) > 0:
item_id = self.tree.selection()[0] item_id = self.tree.selection()[0]
item_values = self.tree.item(item_id, "values") 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.""" """Create a new Toplevel window for editing an entry."""
edit_win = tk.Toplevel(master=self.root) # Define callbacks for edit window buttons
edit_win.title("Edit Entry") callbacks: Dict[str, Callable] = {
"save": self._save_edit,
"delete": lambda win: self._delete_entry(win, item_id),
}
# Unpack values # Create edit window using UI manager
date, dep, anx, slp, app, bup, hydro, gaba, prop, note = values _: tk.Toplevel = self.ui_manager.create_edit_window(values, callbacks)
# Create variables for the widgets def _save_edit(
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(
self, self,
edit_win: tk.Toplevel, edit_win: tk.Toplevel,
date: str, date: str,
@@ -407,207 +142,121 @@ class MedTrackerApp:
prop: int, prop: int,
note: str, note: str,
) -> None: ) -> None:
""" """Save the edited data to the CSV file."""
Save the edited data to the CSV file. values: List[Union[str, int]] = [
""" date,
df = pd.read_csv(self.filename) dep,
# Find the row to update using the date as a unique identifier anx,
df.loc[ slp,
df["date"] == date, app,
[ bup,
"date", hydro,
"depression", gaba,
"anxiety", prop,
"sleep", note,
"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)
edit_win.destroy() if self.data_manager.update_entry(date, values):
messagebox.showinfo( edit_win.destroy()
"Success", "Entry updated successfully!", parent=self.root messagebox.showinfo(
) "Success", "Entry updated successfully!", parent=self.root
self.clear_entries() )
self.load_data() self._clear_entries()
self.load_data()
else:
messagebox.showerror(
"Error", "Failed to save changes", parent=edit_win
)
def on_closing(self) -> None: def on_closing(self) -> None:
if messagebox.askokcancel( if messagebox.askokcancel(
"Quit", "Do you want to quit the application?", parent=self.root "Quit", "Do you want to quit the application?", parent=self.root
): ):
plt.close(self.fig) self.graph_manager.close()
self.root.destroy() 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: def add_entry(self) -> None:
with open(self.filename, mode="a", newline="") as file: """Add a new entry to the CSV file."""
writer = csv.writer(file) entry: List[Union[str, int]] = [
writer.writerow( self.date_var.get(),
[ self.symptom_vars["depression"].get(),
self.date_var.get(), self.symptom_vars["anxiety"].get(),
self.depression_var.get(), self.symptom_vars["sleep"].get(),
self.anxiety_var.get(), self.symptom_vars["appetite"].get(),
self.sleep_var.get(), self.medicine_vars["bupropion"][0].get(),
self.appetite_var.get(), self.medicine_vars["hydroxyzine"][0].get(),
self.bupropion_var.get(), self.medicine_vars["gabapentin"][0].get(),
self.hydroxyzine_var.get(), self.medicine_vars["propranolol"][0].get(),
self.gabapentin_var.get(), self.note_var.get(),
self.propranolol_var.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( def _delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None:
"Success", "Entry added successfully!", parent=self.root """Delete the selected entry from the CSV file."""
) logger.debug(f"Delete requested for item_id={item_id}")
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.
"""
if messagebox.askyesno( if messagebox.askyesno(
"Delete Entry", "Delete Entry",
"Are you sure you want to delete this entry?", "Are you sure you want to delete this entry?",
parent=edit_win, parent=edit_win,
): ):
df = pd.read_csv(self.filename)
# Get the date of the entry to delete # Get the date of the entry to delete
date = self.tree.item(item_id, "values")[0] date: str = self.tree.item(item_id, "values")[0]
# Remove the row with the matching date logger.debug(f"Deleting entry with date={date}")
df = df[df["date"] != date]
# Write the updated dataframe back to the CSV
df.to_csv(self.filename, index=False)
edit_win.destroy() if self.data_manager.delete_entry(date):
messagebox.showinfo( edit_win.destroy()
"Success", "Entry deleted successfully!", parent=edit_win messagebox.showinfo(
) "Success", "Entry deleted successfully!", parent=edit_win
self.load_data() )
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.date_var.set("")
self.depression_var.set(0) for key in self.symptom_vars:
self.anxiety_var.set(0) self.symptom_vars[key].set(0)
self.sleep_var.set(0) for key in self.medicine_vars:
self.appetite_var.set(0) self.medicine_vars[key][0].set(0)
self.bupropion_var.set(False)
self.hydroxyzine_var.set(False)
self.gabapentin_var.set(False)
self.propranolol_var.set(False)
self.note_var.set("") self.note_var.set("")
def load_data(self) -> None: 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(): for i in self.tree.get_children():
self.tree.delete(i) self.tree.delete(i)
if ( # Load data from the CSV file
os.path.exists(self.filename) df: pd.DataFrame = self.data_manager.load_data()
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())
def update_graph(self, df: pd.DataFrame) -> None: # Update the treeview with the data
self.ax.clear()
if not df.empty: if not df.empty:
df["date"] = pd.to_datetime(df["date"]) for index, row in df.iterrows():
df = df.sort_values(by="date") self.tree.insert(parent="", index="end", values=list(row))
df.set_index(keys="date", inplace=True) logger.debug(f"Loaded {len(df)} entries into treeview.")
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()
def make_icon(self, img: str, logger: logging.Logger) -> None: # Update the graph
try: self.graph_manager.update_graph(df)
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.")
if __name__ == "__main__": if __name__ == "__main__":
root = tk.Tk() root: tk.Tk = tk.Tk()
app = MedTrackerApp(root) app: MedTrackerApp = MedTrackerApp(root)
root.mainloop() root.mainloop()

462
src/ui_manager.py Normal file
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)