Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3c88c63d2 | |||
| 86606d56b6 | |||
| 9790f2730a | |||
| fdcc210fc4 | |||
| b7a22524d7 | |||
| 156dcd1651 | |||
| 1d310dd081 | |||
| abd1fa33cf | |||
| 03ef9e761a | |||
| ca1f8c976d | |||
| 7392709a27 | |||
| 623050478a | |||
| 41d91d9c30 | |||
| 14d9943665 | |||
| 13a4826415 |
+2
-1
@@ -47,7 +47,7 @@ htmlcov/
|
|||||||
.pylint.d/
|
.pylint.d/
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
#.vscode/
|
.vscode/
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
.idea/
|
.idea/
|
||||||
@@ -81,3 +81,4 @@ Thumbs.db
|
|||||||
.Trashes
|
.Trashes
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
integration_test_exports/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
TARGET=thechart
|
TARGET=thechart
|
||||||
VERSION=1.6.1
|
VERSION=1.9.5
|
||||||
ROOT=/home/will
|
ROOT=/home/will
|
||||||
ICON=chart-671.png
|
ICON=chart-671.png
|
||||||
SHELL=fish
|
SHELL=fish
|
||||||
@@ -85,7 +85,7 @@ install: ## Set up the development environment
|
|||||||
@echo "To run tests: make test"
|
@echo "To run tests: make test"
|
||||||
build: ## Build the Docker image
|
build: ## Build the Docker image
|
||||||
@echo "Building the Docker image..."
|
@echo "Building the Docker image..."
|
||||||
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE} --push .
|
docker buildx build --platform linux/amd64 -t ${IMAGE} --push .
|
||||||
deploy: ## Deploy the application as a standalone executable
|
deploy: ## Deploy the application as a standalone executable
|
||||||
@echo "Deploying the application..."
|
@echo "Deploying the application..."
|
||||||
pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data="./.env:." --add-data='./chart-671.png:.' --add-data='./thechart_data.csv:.' --log-level=DEBUG src/main.py
|
pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data="./.env:." --add-data='./chart-671.png:.' --add-data='./thechart_data.csv:.' --log-level=DEBUG src/main.py
|
||||||
@@ -121,21 +121,6 @@ test-watch: ## Run tests in watch mode
|
|||||||
test-debug: ## Run tests with debug output
|
test-debug: ## Run tests with debug output
|
||||||
@echo "Running tests with debug output..."
|
@echo "Running tests with debug output..."
|
||||||
.venv/bin/python -m pytest tests/ -v -s --tb=long --cov=src
|
.venv/bin/python -m pytest tests/ -v -s --tb=long --cov=src
|
||||||
test-dose-tracking: ## Test the dose tracking functionality
|
|
||||||
@echo "Testing dose tracking functionality..."
|
|
||||||
.venv/bin/python scripts/test_dose_tracking.py
|
|
||||||
test-scrollable-input: ## Test the scrollable input frame UI
|
|
||||||
@echo "Testing scrollable input frame..."
|
|
||||||
.venv/bin/python scripts/test_scrollable_input.py
|
|
||||||
test-edit-functionality: ## Test the enhanced edit functionality
|
|
||||||
@echo "Testing edit functionality..."
|
|
||||||
.venv/bin/python scripts/test_edit_functionality.py
|
|
||||||
test-edit-window: $(VENV_ACTIVATE) ## Test edit window functionality (save and delete)
|
|
||||||
@echo "Running edit window functionality test..."
|
|
||||||
$(PYTHON) scripts/test_edit_window_functionality.py
|
|
||||||
test-dose-editing: $(VENV_ACTIVATE) ## Test dose editing functionality in edit window
|
|
||||||
@echo "Running dose editing functionality test..."
|
|
||||||
$(PYTHON) scripts/test_dose_editing_functionality.py
|
|
||||||
lint: ## Run the linter
|
lint: ## Run the linter
|
||||||
@echo "Running the linter..."
|
@echo "Running the linter..."
|
||||||
docker-compose exec ${TARGET} pipenv run pre-commit run --all-files
|
docker-compose exec ${TARGET} pipenv run pre-commit run --all-files
|
||||||
@@ -157,4 +142,4 @@ commit-emergency: ## Emergency commit (bypasses pre-commit hooks) - USE SPARINGL
|
|||||||
@read -p "Enter commit message: " msg; \
|
@read -p "Enter commit message: " msg; \
|
||||||
git add . && git commit --no-verify -m "$$msg"
|
git add . && git commit --no-verify -m "$$msg"
|
||||||
@echo "✅ Emergency commit completed. Please run tests manually when possible."
|
@echo "✅ Emergency commit completed. Please run tests manually when possible."
|
||||||
.PHONY: install clean reinstall check-env build attach deploy run start stop test lint format shell requirements commit-emergency test-dose-tracking test-scrollable-input test-edit-functionality test-edit-window test-dose-editing migrate-csv help
|
.PHONY: install clean reinstall check-env build attach deploy run start stop test lint format shell requirements commit-emergency help
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# TheChart
|
# TheChart
|
||||||
Advanced medication tracking application for monitoring treatment progress and symptom evolution.
|
Modern medication tracking application with advanced UI/UX for monitoring treatment progress and symptom evolution.
|
||||||
|
|
||||||
## Quick Start
|
## 🚀 Quick Start
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
make install
|
make install
|
||||||
@@ -14,10 +14,22 @@ make test
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 📚 Documentation
|
## 📚 Documentation
|
||||||
- **[Features Guide](docs/FEATURES.md)** - Complete feature documentation
|
- **[Features Guide](docs/FEATURES.md)** - Complete feature documentation with UI/UX improvements
|
||||||
|
- **[Keyboard Shortcuts](docs/KEYBOARD_SHORTCUTS.md)** - Comprehensive keyboard shortcuts for efficiency
|
||||||
|
- **[Export System](docs/EXPORT_SYSTEM.md)** - Data export functionality (JSON, XML, PDF)
|
||||||
- **[Development Guide](docs/DEVELOPMENT.md)** - Testing, development, and architecture
|
- **[Development Guide](docs/DEVELOPMENT.md)** - Testing, development, and architecture
|
||||||
- **[Changelog](docs/CHANGELOG.md)** - Version history and feature evolution
|
- **[Changelog](docs/CHANGELOG.md)** - Version history and recent UI improvements
|
||||||
- **[Quick Reference](#quick-reference)** - Common commands and shortcuts
|
- **[Documentation Index](docs/README.md)** - Complete documentation navigation guide
|
||||||
|
|
||||||
|
> 💡 **Quick Start**: New users should start with this README, then explore the [Features Guide](docs/FEATURES.md) for detailed functionality. The [Documentation Index](docs/README.md) provides comprehensive navigation.
|
||||||
|
|
||||||
|
## ✨ Recent Major Updates (v1.9.5)
|
||||||
|
- **🎨 Modern UI/UX**: Professional themes with ttkthemes integration
|
||||||
|
- **⌨️ Keyboard Shortcuts**: Comprehensive shortcut system for all operations
|
||||||
|
- **💡 Smart Tooltips**: Context-sensitive help throughout the application
|
||||||
|
- **🎭 8 Professional Themes**: Arc, Equilux, Adapta, Yaru, Ubuntu, Plastik, Breeze, Elegance
|
||||||
|
- **⚙️ Settings System**: Advanced configuration with theme persistence
|
||||||
|
- **📊 Enhanced Tables**: Improved selection highlighting and alternating row colors
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
@@ -226,6 +238,13 @@ On first run, the application will:
|
|||||||
- **Backward Compatibility**: Seamless upgrades without data loss
|
- **Backward Compatibility**: Seamless upgrades without data loss
|
||||||
- **Dynamic Columns**: Adapts to new medicines and pathologies
|
- **Dynamic Columns**: Adapts to new medicines and pathologies
|
||||||
|
|
||||||
|
### 📋 Data Export System
|
||||||
|
- **Multiple Formats**: Export to JSON, XML, and PDF formats
|
||||||
|
- **Comprehensive Reports**: PDF exports with optional graph visualization
|
||||||
|
- **Metadata Inclusion**: Export includes date ranges, pathologies, and medicines
|
||||||
|
- **User-Friendly Interface**: Easy access through File menu with format selection
|
||||||
|
- **Data Portability**: Structured exports for analysis or backup purposes
|
||||||
|
|
||||||
For complete feature documentation, see **[docs/FEATURES.md](docs/FEATURES.md)**.
|
For complete feature documentation, see **[docs/FEATURES.md](docs/FEATURES.md)**.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
@@ -475,6 +494,30 @@ thechart_data.csv # User data (created on first run)
|
|||||||
- **`pyproject.toml`**: Project configuration and dependencies
|
- **`pyproject.toml`**: Project configuration and dependencies
|
||||||
- **`uv.lock`**: Dependency lock file
|
- **`uv.lock`**: Dependency lock file
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
```bash
|
||||||
|
# File Operations
|
||||||
|
Ctrl+S # Save/Add new entry
|
||||||
|
Ctrl+Q # Quit application
|
||||||
|
Ctrl+E # Export data
|
||||||
|
|
||||||
|
# Data Management
|
||||||
|
Ctrl+N # Clear entries
|
||||||
|
Ctrl+R / F5 # Refresh data
|
||||||
|
|
||||||
|
# Window Management
|
||||||
|
Ctrl+M # Manage medicines
|
||||||
|
Ctrl+P # Manage pathologies
|
||||||
|
|
||||||
|
# Table Operations
|
||||||
|
Delete # Delete selected entry
|
||||||
|
Escape # Clear selection
|
||||||
|
Double-click # Edit entry
|
||||||
|
|
||||||
|
# Help
|
||||||
|
F1 # Show keyboard shortcuts help
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Why uv?
|
## Why uv?
|
||||||
|
|||||||
+3
-3
@@ -1,19 +1,19 @@
|
|||||||
#!/usr/bin/bash
|
#!/usr/bin/bash
|
||||||
|
|
||||||
CONTAINER_ENGINE="docker" # podman | docker
|
CONTAINER_ENGINE="docker" # podman | docker
|
||||||
VERSION="v1.0.0"
|
VERSION="v1.7.5"
|
||||||
REGISTRY="gitea-http.taildb3494.ts.net/will/thechart"
|
REGISTRY="gitea-http.taildb3494.ts.net/will/thechart"
|
||||||
|
|
||||||
if [ "$CONTAINER_ENGINE" == "podman" ];
|
if [ "$CONTAINER_ENGINE" == "podman" ];
|
||||||
then
|
then
|
||||||
buildah build \
|
buildah build \
|
||||||
-t $REGISTRY:$VERSION \
|
-t $REGISTRY:$VERSION \
|
||||||
--platform linux/amd64,linux/arm64/v8 \
|
--platform linux/amd64 \
|
||||||
--no-cache .
|
--no-cache .
|
||||||
else
|
else
|
||||||
DOCKER_BUILDKIT=1 \
|
DOCKER_BUILDKIT=1 \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64,linux/arm64/v8 \
|
--platform linux/amd64 \
|
||||||
-t $REGISTRY:$VERSION \
|
-t $REGISTRY:$VERSION \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
--push .
|
--push .
|
||||||
|
|||||||
@@ -5,6 +5,75 @@ All notable changes to TheChart project are documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.9.5] - 2025-08-05
|
||||||
|
|
||||||
|
### 🎨 Major UI/UX Overhaul
|
||||||
|
- **Added**: Professional theme system with ttkthemes integration
|
||||||
|
- **Added**: 8 curated themes (Arc, Equilux, Adapta, Yaru, Ubuntu, Plastik, Breeze, Elegance)
|
||||||
|
- **Added**: Dynamic theme switching without restart
|
||||||
|
- **Added**: Theme persistence between sessions
|
||||||
|
- **Added**: Comprehensive settings window with tabbed interface
|
||||||
|
- **Added**: Smart tooltip system with context-sensitive help
|
||||||
|
- **Improved**: Table selection highlighting and alternating row colors
|
||||||
|
- **Improved**: Modern styling for all UI components (buttons, frames, forms)
|
||||||
|
- **Improved**: Professional card-style layouts and enhanced spacing
|
||||||
|
|
||||||
|
### ⚙️ Settings and Configuration System
|
||||||
|
- **Added**: Advanced settings window (accessible via F2)
|
||||||
|
- **Added**: Theme selection with live preview
|
||||||
|
- **Added**: UI preferences and customization options
|
||||||
|
- **Added**: About dialog with detailed application information
|
||||||
|
- **Added**: Settings persistence across application restarts
|
||||||
|
|
||||||
|
### 💡 Enhanced User Experience
|
||||||
|
- **Added**: Intelligent tooltips for all interactive elements
|
||||||
|
- **Added**: Specialized help for pathology scales and medicine options
|
||||||
|
- **Added**: Non-intrusive tooltip timing (500-800ms delay)
|
||||||
|
- **Added**: Quick theme switching via menu bar
|
||||||
|
- **Improved**: Visual hierarchy with better typography and spacing
|
||||||
|
- **Improved**: Professional color schemes across all themes
|
||||||
|
|
||||||
|
### 🏗️ Technical Architecture Improvements
|
||||||
|
- **Added**: Modular theme manager with dependency injection
|
||||||
|
- **Added**: Tooltip management system
|
||||||
|
- **Added**: Enhanced UI manager with theme integration
|
||||||
|
- **Improved**: Code organization with separate concerns
|
||||||
|
- **Improved**: Error handling with graceful theme fallbacks
|
||||||
|
|
||||||
|
## [1.7.0] - 2025-08-05
|
||||||
|
|
||||||
|
### ⌨️ Keyboard Shortcuts System
|
||||||
|
- **Added**: Comprehensive keyboard shortcuts for improved productivity
|
||||||
|
- **Added**: File operations shortcuts (Ctrl+S, Ctrl+Q, Ctrl+E)
|
||||||
|
- **Added**: Data management shortcuts (Ctrl+N, Ctrl+R, F5)
|
||||||
|
- **Added**: Window management shortcuts (Ctrl+M, Ctrl+P)
|
||||||
|
- **Added**: Table operation shortcuts (Delete, Escape)
|
||||||
|
- **Added**: Help system shortcut (F1)
|
||||||
|
- **Added**: Menu integration showing shortcuts next to menu items
|
||||||
|
- **Added**: Button labels updated to show primary shortcuts
|
||||||
|
- **Added**: In-app help dialog accessible via F1
|
||||||
|
- **Added**: Status bar feedback for all keyboard operations
|
||||||
|
- **Improved**: Button text shows shortcuts (e.g., "Add Entry (Ctrl+S)")
|
||||||
|
- **Improved**: Case-insensitive shortcuts (Ctrl+S and Ctrl+Shift+S both work)
|
||||||
|
|
||||||
|
#### Keyboard Shortcuts Added:
|
||||||
|
- **Ctrl+S**: Save/Add new entry
|
||||||
|
- **Ctrl+Q**: Quit application (with confirmation)
|
||||||
|
- **Ctrl+E**: Export data
|
||||||
|
- **Ctrl+N**: Clear entries
|
||||||
|
- **Ctrl+R / F5**: Refresh data
|
||||||
|
- **Ctrl+M**: Manage medicines
|
||||||
|
- **Ctrl+P**: Manage pathologies
|
||||||
|
- **Delete**: Delete selected entry (with confirmation)
|
||||||
|
- **Escape**: Clear selection
|
||||||
|
- **F1**: Show keyboard shortcuts help
|
||||||
|
|
||||||
|
### 📚 Documentation Updates
|
||||||
|
- **Updated**: FEATURES.md with keyboard shortcuts section
|
||||||
|
- **Added**: KEYBOARD_SHORTCUTS.md with comprehensive shortcut reference
|
||||||
|
- **Updated**: In-app help system with shortcut information
|
||||||
|
- **Updated**: About dialog with keyboard shortcut mention
|
||||||
|
|
||||||
## [1.6.1] - 2025-07-31
|
## [1.6.1] - 2025-07-31
|
||||||
|
|
||||||
### 📚 Documentation Overhaul
|
### 📚 Documentation Overhaul
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Documentation Consolidation Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This document summarizes the documentation consolidation and updates performed to improve the TheChart project documentation structure.
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. Documentation Structure Consolidation
|
||||||
|
- **Removed**: `docs/UI_IMPROVEMENTS.md` (redundant file)
|
||||||
|
- **Consolidated**: UI/UX improvements documentation into `docs/FEATURES.md`
|
||||||
|
- **Enhanced**: Main `README.md` with recent updates section
|
||||||
|
- **Updated**: `docs/README.md` (documentation index) with comprehensive navigation
|
||||||
|
|
||||||
|
### 2. Content Integration
|
||||||
|
|
||||||
|
#### FEATURES.md Enhancements
|
||||||
|
- **Added**: Modern UI/UX System section (new in v1.9.5)
|
||||||
|
- **Added**: Professional Theme Engine documentation
|
||||||
|
- **Added**: Comprehensive Keyboard Shortcuts section
|
||||||
|
- **Added**: Settings and Theme Management documentation
|
||||||
|
- **Added**: Smart Tooltip System documentation
|
||||||
|
- **Added**: Enhanced Technical Architecture section
|
||||||
|
- **Added**: UI/UX Technical Implementation section
|
||||||
|
|
||||||
|
#### CHANGELOG.md Updates
|
||||||
|
- **Added**: Version 1.9.5 with comprehensive UI/UX overhaul documentation
|
||||||
|
- **Added**: Settings and Configuration System section
|
||||||
|
- **Added**: Enhanced User Experience section
|
||||||
|
- **Added**: Technical Architecture Improvements section
|
||||||
|
|
||||||
|
#### README.md Improvements
|
||||||
|
- **Updated**: Title and description to emphasize modern UI/UX
|
||||||
|
- **Added**: Recent Major Updates section highlighting v1.9.5 improvements
|
||||||
|
- **Added**: Quick start guidance for new users
|
||||||
|
- **Updated**: Documentation links with better descriptions
|
||||||
|
- **Added**: Documentation navigation guide reference
|
||||||
|
|
||||||
|
### 3. Cross-Reference Updates
|
||||||
|
- **Updated**: All internal links to reflect consolidated structure
|
||||||
|
- **Enhanced**: Documentation index with comprehensive navigation
|
||||||
|
- **Added**: Task-based navigation in docs/README.md
|
||||||
|
- **Improved**: User type-based documentation guidance
|
||||||
|
|
||||||
|
## Current Documentation Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── README.md # Documentation index and navigation guide
|
||||||
|
├── FEATURES.md # Complete feature documentation (includes UI/UX)
|
||||||
|
├── KEYBOARD_SHORTCUTS.md # Comprehensive shortcut reference
|
||||||
|
├── EXPORT_SYSTEM.md # Data export functionality
|
||||||
|
├── DEVELOPMENT.md # Development setup and testing
|
||||||
|
├── CHANGELOG.md # Version history and improvements
|
||||||
|
└── DOCUMENTATION_SUMMARY.md # This summary (new)
|
||||||
|
|
||||||
|
README.md # Main project README with quick start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation Highlights
|
||||||
|
|
||||||
|
### For End Users
|
||||||
|
1. **Modern UI/UX**: Complete documentation of the new theme system
|
||||||
|
2. **Keyboard Efficiency**: Comprehensive shortcut system documentation
|
||||||
|
3. **Feature Guidance**: Consolidated feature documentation with examples
|
||||||
|
4. **Quick Navigation**: Task-based and user-type-based navigation
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
1. **Technical Architecture**: Enhanced architecture documentation
|
||||||
|
2. **UI/UX Implementation**: Technical details of theme system
|
||||||
|
3. **Code Organization**: Clear separation of concerns documentation
|
||||||
|
4. **Development Workflow**: Comprehensive development guide
|
||||||
|
|
||||||
|
## Quality Improvements
|
||||||
|
|
||||||
|
### Content Quality
|
||||||
|
- **Comprehensive Coverage**: All major features and improvements documented
|
||||||
|
- **Clear Structure**: Hierarchical organization with clear headings
|
||||||
|
- **Practical Examples**: Code snippets and usage examples maintained
|
||||||
|
- **Cross-References**: Better linking between related sections
|
||||||
|
|
||||||
|
### User Experience
|
||||||
|
- **Progressive Disclosure**: Information organized by user expertise level
|
||||||
|
- **Task-Oriented**: Documentation organized around user tasks
|
||||||
|
- **Quick Access**: Multiple entry points and navigation paths
|
||||||
|
- **Searchable**: Clear headings and consistent formatting
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
- **Reduced Redundancy**: Eliminated duplicate information
|
||||||
|
- **Single Source of Truth**: Consolidated information reduces maintenance burden
|
||||||
|
- **Version Alignment**: Documentation synchronized with current codebase
|
||||||
|
- **Future-Proof**: Structure supports easy updates and additions
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Recommended Maintenance
|
||||||
|
1. **Keep Features Updated**: Update FEATURES.md as new UI/UX improvements are added
|
||||||
|
2. **Maintain Changelog**: Continue detailed changelog entries for version tracking
|
||||||
|
3. **Review Navigation**: Periodically review docs/README.md navigation for completeness
|
||||||
|
4. **User Feedback**: Collect user feedback on documentation effectiveness
|
||||||
|
|
||||||
|
### Future Enhancements
|
||||||
|
1. **Screenshots**: Consider adding screenshots of the new UI themes
|
||||||
|
2. **Video Guides**: Potential for video demonstrations of key features
|
||||||
|
3. **API Documentation**: If public APIs develop, consider separate API docs
|
||||||
|
4. **Internationalization**: Structure supports future translation efforts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Documentation consolidation completed**: All major UI/UX improvements are now properly documented and easily discoverable through the improved navigation structure.
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
# TheChart Export System Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The TheChart application now includes a comprehensive data export system that allows users to export their medication tracking data and visualizations to multiple formats:
|
||||||
|
|
||||||
|
- **JSON** - Structured data format with metadata
|
||||||
|
- **XML** - Hierarchical data format
|
||||||
|
- **PDF** - Formatted report with optional graph visualization
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Export Formats
|
||||||
|
|
||||||
|
#### JSON Export
|
||||||
|
- Exports all CSV data to structured JSON format
|
||||||
|
- Includes metadata about the export (date, total entries, date range)
|
||||||
|
- Lists all pathologies and medicines being tracked
|
||||||
|
- Data is exported as an array of entry objects
|
||||||
|
|
||||||
|
#### XML Export
|
||||||
|
- Exports data to hierarchical XML format
|
||||||
|
- Includes comprehensive metadata section
|
||||||
|
- All entries are properly structured with XML tags
|
||||||
|
- Column names are sanitized for valid XML element names
|
||||||
|
|
||||||
|
#### PDF Export
|
||||||
|
- Creates a formatted report document
|
||||||
|
- Includes export metadata and summary information
|
||||||
|
- Optional graph visualization inclusion
|
||||||
|
- Data table with all entries
|
||||||
|
- Proper pagination and styling
|
||||||
|
- Notes are truncated for better table formatting
|
||||||
|
|
||||||
|
### User Interface
|
||||||
|
|
||||||
|
The export functionality is accessible through:
|
||||||
|
1. **File Menu** - "Export Data..." option in the main menu bar
|
||||||
|
2. **Export Window** - Modal dialog with export options
|
||||||
|
3. **Format Selection** - Radio buttons for JSON, XML, or PDF
|
||||||
|
4. **Graph Option** - Checkbox to include graph in PDF exports
|
||||||
|
5. **File Dialog** - Standard save dialog for choosing export location
|
||||||
|
|
||||||
|
### Export Manager Architecture
|
||||||
|
|
||||||
|
The export system consists of three main components:
|
||||||
|
|
||||||
|
#### ExportManager Class (`src/export_manager.py`)
|
||||||
|
- Core export functionality
|
||||||
|
- Handles data transformation and file generation
|
||||||
|
- Integrates with existing data and graph managers
|
||||||
|
- Supports all three export formats
|
||||||
|
|
||||||
|
#### ExportWindow Class (`src/export_window.py`)
|
||||||
|
- GUI interface for export operations
|
||||||
|
- Modal dialog with export options
|
||||||
|
- File save dialog integration
|
||||||
|
- Progress feedback and error handling
|
||||||
|
|
||||||
|
#### Integration in MedTrackerApp (`src/main.py`)
|
||||||
|
- Export manager initialization
|
||||||
|
- Menu integration
|
||||||
|
- Seamless integration with existing managers
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
### Dependencies Added
|
||||||
|
- `reportlab` - PDF generation library
|
||||||
|
- `lxml` - XML processing (added for future enhancements)
|
||||||
|
- `charset-normalizer` - Character encoding support
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
1. User selects export format and options
|
||||||
|
2. ExportManager loads data from DataManager
|
||||||
|
3. Data is transformed according to selected format
|
||||||
|
4. Graph image is optionally generated for PDF
|
||||||
|
5. Output file is created and saved
|
||||||
|
6. User receives success/failure feedback
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Graceful handling of missing data
|
||||||
|
- File system error management
|
||||||
|
- User-friendly error messages
|
||||||
|
- Logging of export operations
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Export Process
|
||||||
|
1. Open TheChart application
|
||||||
|
2. Go to File → Export Data...
|
||||||
|
3. Select desired format (JSON/XML/PDF)
|
||||||
|
4. For PDF: choose whether to include graph
|
||||||
|
5. Click "Export..." button
|
||||||
|
6. Choose save location and filename
|
||||||
|
7. Confirm successful export
|
||||||
|
|
||||||
|
### Export File Examples
|
||||||
|
|
||||||
|
#### JSON Structure
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"export_date": "2025-08-02T09:03:22.580489",
|
||||||
|
"total_entries": 32,
|
||||||
|
"date_range": {
|
||||||
|
"start": "07/02/2025",
|
||||||
|
"end": "08/02/2025"
|
||||||
|
},
|
||||||
|
"pathologies": ["depression", "anxiety", "sleep", "appetite"],
|
||||||
|
"medicines": ["bupropion", "hydroxyzine", "gabapentin", "propranolol", "quetiapine"]
|
||||||
|
},
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"date": "07/02/2025",
|
||||||
|
"depression": 8,
|
||||||
|
"anxiety": 5,
|
||||||
|
"sleep": 3,
|
||||||
|
"appetite": 1,
|
||||||
|
"bupropion": 0,
|
||||||
|
"bupropion_doses": "",
|
||||||
|
"note": "Starting medication tracking"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### XML Structure
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<thechart_data>
|
||||||
|
<metadata>
|
||||||
|
<export_date>2025-08-02T09:03:22.613013</export_date>
|
||||||
|
<total_entries>32</total_entries>
|
||||||
|
<date_range>
|
||||||
|
<start>07/02/2025</start>
|
||||||
|
<end>08/02/2025</end>
|
||||||
|
</date_range>
|
||||||
|
</metadata>
|
||||||
|
<entries>
|
||||||
|
<entry>
|
||||||
|
<date>07/02/2025</date>
|
||||||
|
<depression>8</depression>
|
||||||
|
<anxiety>5</anxiety>
|
||||||
|
<note>Starting medication tracking</note>
|
||||||
|
</entry>
|
||||||
|
</entries>
|
||||||
|
</thechart_data>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Automated Tests
|
||||||
|
- Export functionality is tested through `simple_export_test.py`
|
||||||
|
- Creates sample exports in all three formats
|
||||||
|
- Validates file creation and basic content structure
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
- GUI testing available through `test_export_gui.py`
|
||||||
|
- Opens export window for interactive testing
|
||||||
|
- Allows testing of all user interface components
|
||||||
|
|
||||||
|
### Test Files Location
|
||||||
|
Exported test files are created in the `test_exports/` directory:
|
||||||
|
- `export.json` - JSON format export
|
||||||
|
- `export.xml` - XML format export
|
||||||
|
- `export.csv` - CSV format copy
|
||||||
|
- `test_export.pdf` - PDF format with graph
|
||||||
|
|
||||||
|
## File Locations
|
||||||
|
|
||||||
|
### Source Files
|
||||||
|
- `src/export_manager.py` - Core export functionality
|
||||||
|
- `src/export_window.py` - GUI export interface
|
||||||
|
|
||||||
|
### Test Files
|
||||||
|
- `simple_export_test.py` - Basic export functionality test
|
||||||
|
- `test_export_gui.py` - GUI testing interface
|
||||||
|
- `scripts/test_export_functionality.py` - Comprehensive export tests
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Added to `requirements.txt` and managed by `uv`
|
||||||
|
- PDF generation requires `reportlab`
|
||||||
|
- XML processing enhanced with `lxml`
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential improvements for the export system:
|
||||||
|
1. **Additional Formats** - Excel, CSV with formatting
|
||||||
|
2. **Export Filtering** - Date range selection, specific pathologies/medicines
|
||||||
|
3. **Batch Exports** - Multiple formats at once
|
||||||
|
4. **Email Integration** - Direct email export
|
||||||
|
5. **Cloud Storage** - Export to cloud services
|
||||||
|
6. **Export Scheduling** - Automated periodic exports
|
||||||
|
7. **Advanced PDF Styling** - Charts, graphs, custom layouts
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
1. **No Data to Export** - Ensure CSV file has entries before exporting
|
||||||
|
2. **PDF Generation Fails** - Check ReportLab installation and permissions
|
||||||
|
3. **File Save Errors** - Verify write permissions to selected directory
|
||||||
|
4. **Large File Exports** - PDF exports may take longer for large datasets
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
- Check application logs for detailed error messages
|
||||||
|
- Export operations are logged with DEBUG level information
|
||||||
|
- File system errors are captured and reported to user
|
||||||
|
|
||||||
|
## Integration Notes
|
||||||
|
|
||||||
|
The export system integrates seamlessly with existing TheChart functionality:
|
||||||
|
- Uses same data validation and loading mechanisms
|
||||||
|
- Respects existing pathology and medicine configurations
|
||||||
|
- Maintains data integrity and formatting consistency
|
||||||
|
- Follows existing logging and error handling patterns
|
||||||
+144
-16
@@ -1,7 +1,49 @@
|
|||||||
# TheChart - Features Documentation
|
# TheChart - Features Documentation
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
TheChart is a comprehensive medication tracking application that allows users to monitor medication intake, symptom tracking, and visualize treatment progress over time.
|
TheChart is a comprehensive medication tracking application with a modern, professional UI that allows users to monitor medication intake, track symptoms, and visualize treatment progress over time.
|
||||||
|
|
||||||
|
## 🎨 Modern UI/UX System (New in v1.9.5)
|
||||||
|
|
||||||
|
### Professional Theme Engine
|
||||||
|
TheChart features a sophisticated theme system powered by ttkthemes, offering 8 carefully curated professional themes.
|
||||||
|
|
||||||
|
#### Available Themes:
|
||||||
|
- **Arc**: Modern flat design with subtle shadows
|
||||||
|
- **Equilux**: Dark theme with excellent contrast
|
||||||
|
- **Adapta**: Clean, minimalist design
|
||||||
|
- **Yaru**: Ubuntu-inspired modern interface
|
||||||
|
- **Ubuntu**: Official Ubuntu styling
|
||||||
|
- **Plastik**: Classic professional appearance
|
||||||
|
- **Breeze**: KDE-inspired clean design
|
||||||
|
- **Elegance**: Sophisticated dark theme
|
||||||
|
|
||||||
|
#### UI Enhancements:
|
||||||
|
- **Modern Styling**: Card-style frames, enhanced buttons, professional form controls
|
||||||
|
- **Smart Tooltips**: Context-sensitive help for all interactive elements
|
||||||
|
- **Improved Tables**: Better selection highlighting and alternating row colors
|
||||||
|
- **Settings System**: Comprehensive preferences with theme persistence
|
||||||
|
- **Responsive Design**: Automatic layout adjustments and scaling
|
||||||
|
|
||||||
|
### ⌨️ Comprehensive Keyboard Shortcuts
|
||||||
|
Professional keyboard shortcut system for efficient navigation and operation.
|
||||||
|
|
||||||
|
#### File Operations:
|
||||||
|
- **Ctrl+S**: Save/Add new entry
|
||||||
|
- **Ctrl+Q**: Quit application (with confirmation)
|
||||||
|
- **Ctrl+E**: Export data
|
||||||
|
|
||||||
|
#### Data Management:
|
||||||
|
- **Ctrl+N**: Clear entries
|
||||||
|
- **Ctrl+R / F5**: Refresh data
|
||||||
|
- **Delete**: Delete selected entry
|
||||||
|
- **Escape**: Clear selection
|
||||||
|
|
||||||
|
#### Window Management:
|
||||||
|
- **Ctrl+M**: Manage medicines
|
||||||
|
- **Ctrl+P**: Manage pathologies
|
||||||
|
- **F1**: Show keyboard shortcuts help
|
||||||
|
- **F2**: Open settings window
|
||||||
|
|
||||||
## Core Features
|
## Core Features
|
||||||
|
|
||||||
@@ -37,6 +79,36 @@ Each medicine includes:
|
|||||||
2. **Manual Configuration**: Edit `medicines.json` directly
|
2. **Manual Configuration**: Edit `medicines.json` directly
|
||||||
3. **Programmatically**: Use the MedicineManager API
|
3. **Programmatically**: Use the MedicineManager API
|
||||||
|
|
||||||
|
### ⚙️ Settings and Theme Management
|
||||||
|
Advanced configuration system allowing users to customize their experience.
|
||||||
|
|
||||||
|
#### Settings Window (F2):
|
||||||
|
- **Theme Selection**: Choose from 8 professional themes with live preview
|
||||||
|
- **UI Preferences**: Font scaling, window behavior options
|
||||||
|
- **About Information**: Detailed application and version information
|
||||||
|
- **Tabbed Interface**: Organized settings categories for easy navigation
|
||||||
|
|
||||||
|
#### Theme Features:
|
||||||
|
- **Real-time Switching**: No restart required for theme changes
|
||||||
|
- **Persistence**: Selected theme remembered between sessions
|
||||||
|
- **Quick Access**: Theme menu for instant switching
|
||||||
|
- **Fallback Handling**: Graceful handling if themes fail to load
|
||||||
|
|
||||||
|
### 💡 Smart Tooltip System
|
||||||
|
Context-sensitive help system providing guidance throughout the application.
|
||||||
|
|
||||||
|
#### Tooltip Types:
|
||||||
|
- **Pathology Scales**: Usage guidance for symptom tracking
|
||||||
|
- **Medicine Checkboxes**: Medication information and dosage details
|
||||||
|
- **Action Buttons**: Functionality description with keyboard shortcuts
|
||||||
|
- **Form Controls**: Input guidance and format requirements
|
||||||
|
|
||||||
|
#### Features:
|
||||||
|
- **Delayed Display**: Non-intrusive timing (500-800ms delay)
|
||||||
|
- **Theme-aware Styling**: Tooltips match selected theme
|
||||||
|
- **Smart Positioning**: Automatic placement to avoid screen edges
|
||||||
|
- **Rich Content**: Multi-line descriptions with formatting
|
||||||
|
|
||||||
### 💊 Advanced Dose Tracking
|
### 💊 Advanced Dose Tracking
|
||||||
Comprehensive dose tracking system that records exact timestamps and dosages throughout the day.
|
Comprehensive dose tracking system that records exact timestamps and dosages throughout the day.
|
||||||
|
|
||||||
@@ -159,26 +231,82 @@ Professional testing infrastructure with high code coverage.
|
|||||||
- **Real-time Updates**: Immediate feedback and data updates
|
- **Real-time Updates**: Immediate feedback and data updates
|
||||||
- **Error Handling**: Comprehensive error messages and recovery options
|
- **Error Handling**: Comprehensive error messages and recovery options
|
||||||
|
|
||||||
|
### ⌨️ Keyboard Shortcuts
|
||||||
|
Comprehensive keyboard shortcuts for efficient navigation and data entry.
|
||||||
|
|
||||||
|
#### File Operations:
|
||||||
|
- **Ctrl+S**: Save/Add new entry - Quickly save current entry data
|
||||||
|
- **Ctrl+Q**: Quit application - Exit with confirmation dialog
|
||||||
|
- **Ctrl+E**: Export data - Open export dialog window
|
||||||
|
|
||||||
|
#### Data Management:
|
||||||
|
- **Ctrl+N**: Clear entries - Clear all input fields for new entry
|
||||||
|
- **Ctrl+R / F5**: Refresh data - Reload data from CSV and update displays
|
||||||
|
|
||||||
|
#### Window Management:
|
||||||
|
- **Ctrl+M**: Manage medicines - Open medicine management window
|
||||||
|
- **Ctrl+P**: Manage pathologies - Open pathology management window
|
||||||
|
|
||||||
|
#### Table Operations:
|
||||||
|
- **Delete**: Delete selected entry - Remove selected table entry with confirmation
|
||||||
|
- **Escape**: Clear selection - Clear current table selection
|
||||||
|
- **Double-click**: Edit entry - Open edit dialog for selected entry
|
||||||
|
|
||||||
|
#### Help System:
|
||||||
|
- **F1**: Show keyboard shortcuts - Display help dialog with all shortcuts
|
||||||
|
|
||||||
|
#### Integration Features:
|
||||||
|
- **Menu Display**: All shortcuts shown in menu bar next to items
|
||||||
|
- **Button Labels**: Primary buttons show their keyboard shortcuts
|
||||||
|
- **Case Insensitive**: Both Ctrl+S and Ctrl+Shift+S work
|
||||||
|
- **Focus Management**: Shortcuts work when main window has focus
|
||||||
|
- **Status Feedback**: All operations provide status bar feedback
|
||||||
|
|
||||||
## Technical Architecture
|
## Technical Architecture
|
||||||
|
|
||||||
### 🏗️ Modular Design
|
### � Modern UI Architecture
|
||||||
- **MedicineManager**: Core medicine CRUD operations
|
- **ThemeManager**: Centralized theme management with dynamic switching
|
||||||
- **PathologyManager**: Symptom and pathology management
|
- **TooltipManager**: Smart tooltip system with context-sensitive help
|
||||||
- **GraphManager**: All graph-related operations and visualizations
|
- **UIManager**: Enhanced UI component creation with theme integration
|
||||||
- **UIManager**: User interface creation and management
|
- **SettingsWindow**: Advanced configuration interface with persistence
|
||||||
- **DataManager**: CSV operations and data persistence
|
|
||||||
|
|
||||||
### 🔧 Configuration Management
|
### 🏗️ Core Application Design
|
||||||
- **JSON-based Configuration**: `medicines.json` and `pathologies.json`
|
- **MedicineManager**: Core medicine CRUD operations with JSON persistence
|
||||||
- **Dynamic Loading**: Runtime configuration updates
|
- **PathologyManager**: Symptom and pathology management system
|
||||||
- **Validation**: Input validation and error handling
|
- **GraphManager**: Professional graph rendering with matplotlib integration
|
||||||
- **Backward Compatibility**: Seamless updates and migrations
|
- **DataManager**: Robust CSV operations and data persistence with validation
|
||||||
|
|
||||||
### 📈 Data Processing
|
### 🔧 Configuration and Data Management
|
||||||
|
- **JSON-based Configuration**: `medicines.json` and `pathologies.json` for easy management
|
||||||
|
- **Dynamic Loading**: Runtime configuration updates without restarts
|
||||||
|
- **Data Validation**: Comprehensive input validation and error handling
|
||||||
|
- **Backward Compatibility**: Seamless updates and migrations across versions
|
||||||
|
|
||||||
|
### 📈 Advanced Data Processing
|
||||||
- **Pandas Integration**: Efficient data manipulation and analysis
|
- **Pandas Integration**: Efficient data manipulation and analysis
|
||||||
- **Matplotlib Visualization**: Professional graph rendering
|
- **Real-time Calculations**: Dynamic dose totals, averages, and statistics
|
||||||
- **Robust Parsing**: Handles various data formats and edge cases
|
- **Robust Parsing**: Handles various data formats and edge cases gracefully
|
||||||
- **Real-time Calculations**: Dynamic dose totals and averages
|
- **Performance Optimization**: Efficient batch operations and caching
|
||||||
|
|
||||||
|
## UI/UX Technical Implementation
|
||||||
|
|
||||||
|
### 🎭 Theme System Architecture
|
||||||
|
- **Multiple Theme Support**: 8 curated professional themes
|
||||||
|
- **Dynamic Style Application**: Real-time theme switching without restart
|
||||||
|
- **Color Extraction**: Automatic color scheme detection and application
|
||||||
|
- **Fallback Mechanisms**: Graceful handling when themes fail to load
|
||||||
|
|
||||||
|
### 💡 Enhanced User Experience
|
||||||
|
- **Smart Tooltips**: Context-sensitive help with delayed, non-intrusive display
|
||||||
|
- **Modern Styling**: Card-style frames, enhanced buttons, professional form controls
|
||||||
|
- **Improved Tables**: Better selection highlighting and alternating row colors
|
||||||
|
- **Responsive Design**: Automatic layout adjustments and proper scaling
|
||||||
|
|
||||||
|
### ⚙️ Settings and Persistence
|
||||||
|
- **Configuration Management**: Theme and preference persistence across sessions
|
||||||
|
- **Tabbed Settings Interface**: Organized categories for easy navigation
|
||||||
|
- **Live Preview**: Real-time theme preview in settings
|
||||||
|
- **Error Recovery**: Robust handling of corrupted settings with defaults
|
||||||
|
|
||||||
## Deployment and Distribution
|
## Deployment and Distribution
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Keyboard Shortcuts
|
||||||
|
|
||||||
|
TheChart application supports comprehensive keyboard shortcuts for improved productivity and efficient navigation.
|
||||||
|
|
||||||
|
## File Operations
|
||||||
|
- **Ctrl+S**: Save/Add new entry - Saves the current entry data to the database
|
||||||
|
- **Ctrl+Q**: Quit application - Exits the application (with confirmation dialog)
|
||||||
|
- **Ctrl+E**: Export data - Opens the export dialog window
|
||||||
|
|
||||||
|
## Data Management
|
||||||
|
- **Ctrl+N**: Clear entries - Clears all input fields to start a new entry
|
||||||
|
- **Ctrl+R** or **F5**: Refresh data - Reloads data from the CSV file and updates the display
|
||||||
|
|
||||||
|
## Window Management
|
||||||
|
- **Ctrl+M**: Manage medicines - Opens the medicine management window
|
||||||
|
- **Ctrl+P**: Manage pathologies - Opens the pathology management window
|
||||||
|
|
||||||
|
## Table Operations
|
||||||
|
- **Delete**: Delete selected entry - Deletes the currently selected entry in the table (with confirmation)
|
||||||
|
- **Escape**: Clear selection - Clears the current selection in the table
|
||||||
|
- **Double-click**: Edit entry - Opens the edit dialog for the selected entry
|
||||||
|
|
||||||
|
## Help
|
||||||
|
- **F1**: Show keyboard shortcuts help - Displays a dialog with all available keyboard shortcuts
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Menu Integration
|
||||||
|
All keyboard shortcuts are displayed in the menu bar next to their corresponding menu items for easy reference.
|
||||||
|
|
||||||
|
### Button Labels
|
||||||
|
Primary action buttons show their keyboard shortcuts in the button text (e.g., "Add Entry (Ctrl+S)").
|
||||||
|
|
||||||
|
### Case Sensitivity
|
||||||
|
- Shortcuts are case-insensitive
|
||||||
|
- Both `Ctrl+S` and `Ctrl+Shift+S` work
|
||||||
|
- Uppercase and lowercase variants are supported
|
||||||
|
|
||||||
|
### Focus Requirements
|
||||||
|
- Keyboard shortcuts work when the main window has focus
|
||||||
|
- Focus is automatically set to the main window on startup
|
||||||
|
- Shortcuts work across all tabs and interface elements
|
||||||
|
|
||||||
|
### Feedback System
|
||||||
|
- All operations provide feedback through the status bar
|
||||||
|
- Success and error messages are displayed
|
||||||
|
- Confirmation dialogs are shown for destructive operations (quit, delete)
|
||||||
|
|
||||||
|
## Usage Tips
|
||||||
|
|
||||||
|
### Quick Workflow
|
||||||
|
1. **Ctrl+N** - Clear fields for new entry
|
||||||
|
2. Enter data in the form
|
||||||
|
3. **Ctrl+S** - Save the entry
|
||||||
|
4. **F5** - Refresh to see updated data
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
- Use **Ctrl+M** and **Ctrl+P** to quickly access management windows
|
||||||
|
- Use **Delete** to remove unwanted entries from the table
|
||||||
|
- Use **Escape** to clear selections when needed
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
- Press **F1** anytime to see the keyboard shortcuts help dialog
|
||||||
|
- All shortcuts are also visible in the menu bar
|
||||||
|
- Button tooltips show additional keyboard shortcut information
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
- Keyboard shortcuts provide full application functionality without mouse use
|
||||||
|
- All critical operations have keyboard equivalents
|
||||||
|
- Shortcuts follow standard application conventions (Ctrl+S for save, Ctrl+Q for quit)
|
||||||
|
- Help system is easily accessible via F1
|
||||||
+46
-19
@@ -1,16 +1,27 @@
|
|||||||
# TheChart Documentation
|
# TheChart Documentation
|
||||||
|
|
||||||
Welcome to TheChart documentation! This guide will help you navigate the available documentation.
|
Welcome to TheChart documentation! This guide will help you navigate the available documentation for the modern medication tracking application.
|
||||||
|
|
||||||
## 📖 Documentation Index
|
## 📖 Documentation Index
|
||||||
|
|
||||||
### For Users
|
### For Users
|
||||||
- **[README.md](../README.md)** - Quick start guide and installation
|
- **[README.md](../README.md)** - Quick start guide and installation
|
||||||
- **[Features Guide](FEATURES.md)** - Complete feature documentation
|
- **[Features Guide](FEATURES.md)** - Complete feature documentation including new UI/UX improvements
|
||||||
|
- Modern Theme System (8 Professional Themes)
|
||||||
|
- Advanced Keyboard Shortcuts
|
||||||
|
- Smart Tooltip System
|
||||||
- Modular Medicine System
|
- Modular Medicine System
|
||||||
- Advanced Dose Tracking
|
- Advanced Dose Tracking
|
||||||
- Graph Visualizations
|
- Graph Visualizations
|
||||||
- Data Management
|
- Data Management
|
||||||
|
- **[Keyboard Shortcuts](KEYBOARD_SHORTCUTS.md)** - Comprehensive shortcut reference
|
||||||
|
- File operations shortcuts (Ctrl+S, Ctrl+Q, Ctrl+E)
|
||||||
|
- Data management shortcuts (Ctrl+N, Ctrl+R, F5)
|
||||||
|
- Navigation shortcuts (Ctrl+M, Ctrl+P, F1, F2)
|
||||||
|
- **[Export System](EXPORT_SYSTEM.md)** - Data export functionality and formats
|
||||||
|
- JSON, XML, and PDF export options
|
||||||
|
- Graph visualization inclusion
|
||||||
|
- Export manager architecture
|
||||||
|
|
||||||
### For Developers
|
### For Developers
|
||||||
- **[Development Guide](DEVELOPMENT.md)** - Development setup and testing
|
- **[Development Guide](DEVELOPMENT.md)** - Development setup and testing
|
||||||
@@ -21,53 +32,69 @@ Welcome to TheChart documentation! This guide will help you navigate the availab
|
|||||||
|
|
||||||
### Project History
|
### Project History
|
||||||
- **[Changelog](CHANGELOG.md)** - Version history and feature evolution
|
- **[Changelog](CHANGELOG.md)** - Version history and feature evolution
|
||||||
- Recent updates and improvements
|
- Recent UI/UX overhaul (v1.9.5)
|
||||||
- Migration notes
|
- Keyboard shortcuts system (v1.7.0)
|
||||||
- Future roadmap
|
- Medicine and dose tracking improvements
|
||||||
|
- Migration notes and future roadmap
|
||||||
|
|
||||||
## 🚀 Quick Navigation
|
## 🚀 Quick Navigation
|
||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
1. **Installation**: See [README.md - Installation](../README.md#installation)
|
1. **Installation**: See [README.md - Installation](../README.md#installation)
|
||||||
2. **First Run**: See [README.md - Running the Application](../README.md#running-the-application)
|
2. **First Run**: See [README.md - Running the Application](../README.md#running-the-application)
|
||||||
3. **Key Features**: See [FEATURES.md](FEATURES.md)
|
3. **UI/UX Features**: See [FEATURES.md - Modern UI/UX System](FEATURES.md#-modern-uiux-system-new-in-v195)
|
||||||
|
|
||||||
|
### Using the Application
|
||||||
|
1. **Theme Selection**: See [FEATURES.md - Settings and Theme Management](FEATURES.md#️-settings-and-theme-management)
|
||||||
|
2. **Keyboard Shortcuts**: See [KEYBOARD_SHORTCUTS.md](KEYBOARD_SHORTCUTS.md)
|
||||||
|
3. **Medicine Management**: See [FEATURES.md - Modular Medicine System](FEATURES.md#-modular-medicine-system)
|
||||||
|
4. **Dose Tracking**: See [FEATURES.md - Advanced Dose Tracking](FEATURES.md#-advanced-dose-tracking)
|
||||||
|
5. **Data Export**: See [EXPORT_SYSTEM.md](EXPORT_SYSTEM.md)
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
1. **Setup**: See [DEVELOPMENT.md - Development Environment Setup](DEVELOPMENT.md#development-environment-setup)
|
1. **Setup**: See [DEVELOPMENT.md - Development Environment Setup](DEVELOPMENT.md#development-environment-setup)
|
||||||
2. **Testing**: See [DEVELOPMENT.md - Testing Framework](DEVELOPMENT.md#testing-framework)
|
2. **Testing**: See [DEVELOPMENT.md - Testing Framework](DEVELOPMENT.md#testing-framework)
|
||||||
3. **Contributing**: See [DEVELOPMENT.md - Development Workflow](DEVELOPMENT.md#development-workflow)
|
3. **Architecture**: See [FEATURES.md - Technical Architecture](FEATURES.md#technical-architecture)
|
||||||
|
4. **Contributing**: See [DEVELOPMENT.md - Development Workflow](DEVELOPMENT.md#development-workflow)
|
||||||
|
|
||||||
### Advanced Usage
|
## 📋 What's New in Documentation
|
||||||
1. **Medicine Management**: See [FEATURES.md - Modular Medicine System](FEATURES.md#-modular-medicine-system)
|
|
||||||
2. **Dose Tracking**: See [FEATURES.md - Advanced Dose Tracking](FEATURES.md#-advanced-dose-tracking)
|
|
||||||
3. **Visualizations**: See [FEATURES.md - Enhanced Graph Visualization](FEATURES.md#-enhanced-graph-visualization)
|
|
||||||
|
|
||||||
## 📋 Documentation Standards
|
### Recent Updates (v1.9.5)
|
||||||
|
- **Consolidated Structure**: Merged UI improvements into main features documentation
|
||||||
|
- **Enhanced Features Guide**: Added comprehensive UI/UX documentation
|
||||||
|
- **Updated Changelog**: Detailed UI/UX overhaul documentation
|
||||||
|
- **Improved Navigation**: Better cross-referencing between documents
|
||||||
|
|
||||||
All documentation follows these principles:
|
### Documentation Highlights
|
||||||
- **Clear Structure**: Hierarchical organization with clear headings
|
- **Professional UI/UX**: Complete documentation of the new theme system
|
||||||
- **Practical Examples**: Code snippets and usage examples
|
- **Keyboard Efficiency**: Comprehensive shortcut system documentation
|
||||||
- **Up-to-date**: Synchronized with current codebase
|
- **Developer-Friendly**: Enhanced development and testing documentation
|
||||||
- **Comprehensive**: Covers all major features and workflows
|
- **User-Focused**: Clear separation of user vs developer documentation
|
||||||
- **Cross-referenced**: Links between related sections
|
|
||||||
|
|
||||||
## 🔍 Finding Information
|
## 🔍 Finding Information
|
||||||
|
|
||||||
### By Topic
|
### By Topic
|
||||||
- **Installation & Setup** → [README.md](../README.md)
|
- **Installation & Setup** → [README.md](../README.md)
|
||||||
|
- **UI/UX and Themes** → [FEATURES.md - Modern UI/UX System](FEATURES.md#-modern-uiux-system-new-in-v195)
|
||||||
- **Feature Usage** → [FEATURES.md](FEATURES.md)
|
- **Feature Usage** → [FEATURES.md](FEATURES.md)
|
||||||
|
- **Keyboard Shortcuts** → [KEYBOARD_SHORTCUTS.md](KEYBOARD_SHORTCUTS.md)
|
||||||
|
- **Data Export** → [EXPORT_SYSTEM.md](EXPORT_SYSTEM.md)
|
||||||
- **Development** → [DEVELOPMENT.md](DEVELOPMENT.md)
|
- **Development** → [DEVELOPMENT.md](DEVELOPMENT.md)
|
||||||
- **Version History** → [CHANGELOG.md](CHANGELOG.md)
|
- **Version History** → [CHANGELOG.md](CHANGELOG.md)
|
||||||
|
|
||||||
### By User Type
|
### By User Type
|
||||||
- **End Users** → Start with [README.md](../README.md), then [FEATURES.md](FEATURES.md)
|
- **End Users** → Start with [README.md](../README.md), then [FEATURES.md](FEATURES.md)
|
||||||
- **Developers** → [DEVELOPMENT.md](DEVELOPMENT.md) and [CHANGELOG.md](CHANGELOG.md)
|
- **Power Users** → [KEYBOARD_SHORTCUTS.md](KEYBOARD_SHORTCUTS.md) and [EXPORT_SYSTEM.md](EXPORT_SYSTEM.md)
|
||||||
|
- **Developers** → [DEVELOPMENT.md](DEVELOPMENT.md) and [FEATURES.md - Technical Architecture](FEATURES.md#technical-architecture)
|
||||||
- **Contributors** → All documentation, especially [DEVELOPMENT.md](DEVELOPMENT.md)
|
- **Contributors** → All documentation, especially [DEVELOPMENT.md](DEVELOPMENT.md)
|
||||||
|
|
||||||
### By Task
|
### By Task
|
||||||
- **Install TheChart** → [README.md - Installation](../README.md#installation)
|
- **Install TheChart** → [README.md - Installation](../README.md#installation)
|
||||||
|
- **Change Theme** → [FEATURES.md - Settings and Theme Management](FEATURES.md#️-settings-and-theme-management)
|
||||||
|
- **Learn Shortcuts** → [KEYBOARD_SHORTCUTS.md](KEYBOARD_SHORTCUTS.md)
|
||||||
- **Add New Medicine** → [FEATURES.md - Modular Medicine System](FEATURES.md#-modular-medicine-system)
|
- **Add New Medicine** → [FEATURES.md - Modular Medicine System](FEATURES.md#-modular-medicine-system)
|
||||||
- **Track Doses** → [FEATURES.md - Advanced Dose Tracking](FEATURES.md#-advanced-dose-tracking)
|
- **Track Doses** → [FEATURES.md - Advanced Dose Tracking](FEATURES.md#-advanced-dose-tracking)
|
||||||
|
- **Export Data** → [EXPORT_SYSTEM.md](EXPORT_SYSTEM.md)
|
||||||
- **Run Tests** → [DEVELOPMENT.md - Testing Framework](DEVELOPMENT.md#testing-framework)
|
- **Run Tests** → [DEVELOPMENT.md - Testing Framework](DEVELOPMENT.md#testing-framework)
|
||||||
- **Deploy Application** → [README.md - Deployment](../README.md#deployment)
|
- **Deploy Application** → [README.md - Deployment](../README.md#deployment)
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -1,15 +1,18 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "thechart"
|
name = "thechart"
|
||||||
version = "1.6.1"
|
version = "1.9.5"
|
||||||
description = "Chart to monitor your medication intake over time."
|
description = "Chart to monitor your medication intake over time."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"colorlog>=6.9.0",
|
"colorlog>=6.9.0",
|
||||||
"dotenv>=0.9.9",
|
"dotenv>=0.9.9",
|
||||||
|
"lxml>=6.0.0",
|
||||||
"matplotlib>=3.10.3",
|
"matplotlib>=3.10.3",
|
||||||
"pandas>=2.3.1",
|
"pandas>=2.3.1",
|
||||||
|
"reportlab>=4.4.3",
|
||||||
"tk>=0.1.0",
|
"tk>=0.1.0",
|
||||||
|
"ttkthemes>=3.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ matplotlib
|
|||||||
pandas
|
pandas
|
||||||
dotenv
|
dotenv
|
||||||
colorlog
|
colorlog
|
||||||
|
ttkthemes
|
||||||
|
|||||||
+5
-1
@@ -24,7 +24,9 @@ packaging==25.0
|
|||||||
pandas==2.3.1
|
pandas==2.3.1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
pillow==11.3.0
|
pillow==11.3.0
|
||||||
# via matplotlib
|
# via
|
||||||
|
# matplotlib
|
||||||
|
# ttkthemes
|
||||||
pyparsing==3.2.3
|
pyparsing==3.2.3
|
||||||
# via matplotlib
|
# via matplotlib
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
@@ -39,5 +41,7 @@ six==1.17.0
|
|||||||
# via python-dateutil
|
# via python-dateutil
|
||||||
tk==0.1.0
|
tk==0.1.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
ttkthemes==3.2.2
|
||||||
|
# via -r requirements.in
|
||||||
tzdata==2025.2
|
tzdata==2025.2
|
||||||
# via pandas
|
# via pandas
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# TheChart Scripts Directory
|
||||||
|
|
||||||
|
This directory contains testing and utility scripts for TheChart application.
|
||||||
|
|
||||||
|
## Scripts Overview
|
||||||
|
|
||||||
|
### Testing Scripts
|
||||||
|
|
||||||
|
#### `run_tests.py`
|
||||||
|
Main test runner for the application.
|
||||||
|
```bash
|
||||||
|
cd /home/will/Code/thechart
|
||||||
|
.venv/bin/python scripts/run_tests.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `integration_test.py`
|
||||||
|
Comprehensive integration test for the export system.
|
||||||
|
- Tests all export formats (JSON, XML, PDF)
|
||||||
|
- Validates data integrity and file creation
|
||||||
|
- No GUI dependencies - safe for automated testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/will/Code/thechart
|
||||||
|
.venv/bin/python scripts/integration_test.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feature Testing Scripts
|
||||||
|
|
||||||
|
#### `test_note_saving.py`
|
||||||
|
Tests note saving and retrieval functionality.
|
||||||
|
- Validates note persistence in CSV files
|
||||||
|
- Tests special characters and formatting
|
||||||
|
|
||||||
|
#### `test_update_entry.py`
|
||||||
|
Tests entry update functionality.
|
||||||
|
- Validates data modification operations
|
||||||
|
- Tests date validation and duplicate handling
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
All scripts should be run from the project root directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/will/Code/thechart
|
||||||
|
.venv/bin/python scripts/<script_name>.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Data
|
||||||
|
|
||||||
|
- Integration tests create temporary export files in `integration_test_exports/` (auto-cleaned)
|
||||||
|
- Test scripts use the main `thechart_data.csv` file unless specified otherwise
|
||||||
|
- No test data is committed to the repository
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
When adding new scripts:
|
||||||
|
1. Place them in this directory
|
||||||
|
2. Use the standard shebang: `#!/usr/bin/env python3`
|
||||||
|
3. Add proper docstrings and error handling
|
||||||
|
4. Update this README with script documentation
|
||||||
|
5. Follow the project's linting and formatting standards
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Integration test for TheChart export system
|
||||||
|
Tests the complete export workflow without GUI dependencies
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add src to path
|
||||||
|
sys.path.insert(0, "src")
|
||||||
|
|
||||||
|
from data_manager import DataManager
|
||||||
|
from export_manager import ExportManager
|
||||||
|
from init import logger
|
||||||
|
from medicine_manager import MedicineManager
|
||||||
|
from pathology_manager import PathologyManager
|
||||||
|
|
||||||
|
|
||||||
|
class MockGraphManager:
|
||||||
|
"""Mock graph manager for testing."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.fig = None
|
||||||
|
|
||||||
|
|
||||||
|
def test_integration():
|
||||||
|
"""Test complete export system integration."""
|
||||||
|
print("TheChart Export System Integration Test")
|
||||||
|
print("=" * 45)
|
||||||
|
|
||||||
|
# 1. Initialize all managers
|
||||||
|
print("\n1. Initializing managers...")
|
||||||
|
try:
|
||||||
|
medicine_manager = MedicineManager(logger=logger)
|
||||||
|
pathology_manager = PathologyManager(logger=logger)
|
||||||
|
data_manager = DataManager(
|
||||||
|
"thechart_data.csv", logger, medicine_manager, pathology_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock graph manager (no GUI dependencies)
|
||||||
|
graph_manager = MockGraphManager()
|
||||||
|
|
||||||
|
export_manager = ExportManager(
|
||||||
|
data_manager, graph_manager, medicine_manager, pathology_manager, logger
|
||||||
|
)
|
||||||
|
print(" ✓ All managers initialized successfully")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Manager initialization failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2. Check data availability
|
||||||
|
print("\n2. Checking data availability...")
|
||||||
|
try:
|
||||||
|
export_info = export_manager.get_export_info()
|
||||||
|
print(f" Total entries: {export_info['total_entries']}")
|
||||||
|
print(f" Has data: {export_info['has_data']}")
|
||||||
|
|
||||||
|
if not export_info["has_data"]:
|
||||||
|
print(" ✗ No data available for export")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(
|
||||||
|
f" Date range: {export_info['date_range']['start']} "
|
||||||
|
f"to {export_info['date_range']['end']}"
|
||||||
|
)
|
||||||
|
print(f" Pathologies: {len(export_info['pathologies'])}")
|
||||||
|
print(f" Medicines: {len(export_info['medicines'])}")
|
||||||
|
print(" ✓ Data is available for export")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Data check failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 3. Test all export formats
|
||||||
|
export_dir = Path("integration_test_exports")
|
||||||
|
export_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
formats_to_test = [
|
||||||
|
("JSON", "integration_test.json", export_manager.export_data_to_json),
|
||||||
|
("XML", "integration_test.xml", export_manager.export_data_to_xml),
|
||||||
|
(
|
||||||
|
"PDF",
|
||||||
|
"integration_test.pdf",
|
||||||
|
lambda path: export_manager.export_to_pdf(path, include_graph=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for format_name, filename, export_func in formats_to_test:
|
||||||
|
print(f"\n3.{len(results) + 1}. Testing {format_name} export...")
|
||||||
|
try:
|
||||||
|
file_path = export_dir / filename
|
||||||
|
success = export_func(str(file_path))
|
||||||
|
|
||||||
|
if success and file_path.exists():
|
||||||
|
file_size = file_path.stat().st_size
|
||||||
|
print(
|
||||||
|
f" ✓ {format_name} export successful: {filename} "
|
||||||
|
f"({file_size} bytes)"
|
||||||
|
)
|
||||||
|
results.append(True)
|
||||||
|
else:
|
||||||
|
print(f" ✗ {format_name} export failed")
|
||||||
|
results.append(False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ {format_name} export error: {e}")
|
||||||
|
results.append(False)
|
||||||
|
|
||||||
|
# 4. Summary
|
||||||
|
print("\n4. Test Summary")
|
||||||
|
print(f" Total tests: {len(results)}")
|
||||||
|
print(f" Passed: {sum(results)}")
|
||||||
|
print(f" Failed: {len(results) - sum(results)}")
|
||||||
|
|
||||||
|
if all(results):
|
||||||
|
print(" ✓ All export formats working correctly!")
|
||||||
|
print(f" Check '{export_dir}' directory for exported files.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(" ✗ Some export formats failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = test_integration()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for keyboard shortcuts functionality.
|
||||||
|
This script tests that the keyboard shortcuts are properly bound.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
# Add the src directory to the path so we can import the main module
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||||
|
|
||||||
|
from main import MedTrackerApp
|
||||||
|
|
||||||
|
|
||||||
|
def test_keyboard_shortcuts():
|
||||||
|
"""Test that keyboard shortcuts are properly bound."""
|
||||||
|
print("Testing keyboard shortcuts...")
|
||||||
|
|
||||||
|
# Create a test window
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw() # Hide the window for testing
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create the app instance
|
||||||
|
app = MedTrackerApp(root)
|
||||||
|
|
||||||
|
# Test that the shortcuts are bound
|
||||||
|
expected_shortcuts = [
|
||||||
|
"<Control-s>",
|
||||||
|
"<Control-S>",
|
||||||
|
"<Control-q>",
|
||||||
|
"<Control-Q>",
|
||||||
|
"<Control-e>",
|
||||||
|
"<Control-E>",
|
||||||
|
"<Control-n>",
|
||||||
|
"<Control-N>",
|
||||||
|
"<Control-r>",
|
||||||
|
"<Control-R>",
|
||||||
|
"<F5>",
|
||||||
|
"<Control-m>",
|
||||||
|
"<Control-M>",
|
||||||
|
"<Control-p>",
|
||||||
|
"<Control-P>",
|
||||||
|
"<Delete>",
|
||||||
|
"<Escape>",
|
||||||
|
"<F1>",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check if shortcuts are bound
|
||||||
|
bound_shortcuts = []
|
||||||
|
for shortcut in expected_shortcuts:
|
||||||
|
if root.bind(shortcut):
|
||||||
|
bound_shortcuts.append(shortcut)
|
||||||
|
|
||||||
|
print(f"Successfully bound {len(bound_shortcuts)} keyboard shortcuts:")
|
||||||
|
for shortcut in bound_shortcuts:
|
||||||
|
print(f" ✓ {shortcut}")
|
||||||
|
|
||||||
|
# Test that methods exist
|
||||||
|
methods_to_test = [
|
||||||
|
"add_new_entry",
|
||||||
|
"handle_window_closing",
|
||||||
|
"_open_export_window",
|
||||||
|
"_clear_entries",
|
||||||
|
"refresh_data_display",
|
||||||
|
"_open_medicine_manager",
|
||||||
|
"_open_pathology_manager",
|
||||||
|
"_delete_selected_entry",
|
||||||
|
"_clear_selection",
|
||||||
|
"_show_keyboard_shortcuts",
|
||||||
|
]
|
||||||
|
|
||||||
|
for method_name in methods_to_test:
|
||||||
|
if hasattr(app, method_name):
|
||||||
|
print(f" ✓ Method {method_name} exists")
|
||||||
|
else:
|
||||||
|
print(f" ✗ Method {method_name} missing")
|
||||||
|
|
||||||
|
print("\n✅ Keyboard shortcuts test completed successfully!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error during testing: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
root.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = test_keyboard_shortcuts()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to verify note field saving functionality
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# Add src directory to path to import modules
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||||
|
|
||||||
|
from data_manager import DataManager
|
||||||
|
from medicine_manager import MedicineManager
|
||||||
|
from pathology_manager import PathologyManager
|
||||||
|
|
||||||
|
|
||||||
|
def test_note_saving():
|
||||||
|
"""Test note saving functionality by checking current data"""
|
||||||
|
print("Testing note saving functionality...")
|
||||||
|
|
||||||
|
# Initialize logger
|
||||||
|
logger = logging.getLogger("test")
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# Initialize managers
|
||||||
|
medicine_manager = MedicineManager("medicines.json")
|
||||||
|
pathology_manager = PathologyManager("pathologies.json")
|
||||||
|
data_manager = DataManager(
|
||||||
|
"thechart_data.csv", logger, medicine_manager, pathology_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load current data
|
||||||
|
df = data_manager.load_data()
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
print("No data found in CSV file")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(df)} entries in the data file")
|
||||||
|
|
||||||
|
# Check if we have any entries with notes
|
||||||
|
entries_with_notes = df[df["note"].notna() & (df["note"] != "")].copy()
|
||||||
|
|
||||||
|
print(f"Entries with notes: {len(entries_with_notes)}")
|
||||||
|
|
||||||
|
if len(entries_with_notes) > 0:
|
||||||
|
print("\nEntries with notes:")
|
||||||
|
for _, row in entries_with_notes.iterrows():
|
||||||
|
note_preview = (
|
||||||
|
row["note"][:50] + "..." if len(str(row["note"])) > 50 else row["note"]
|
||||||
|
)
|
||||||
|
print(f" Date: {row['date']}, Note: {note_preview}")
|
||||||
|
|
||||||
|
# Show the most recent entry
|
||||||
|
if len(df) > 0:
|
||||||
|
latest_entry = df.iloc[-1]
|
||||||
|
print("\nMost recent entry:")
|
||||||
|
print(f" Date: {latest_entry['date']}")
|
||||||
|
print(f" Note: '{latest_entry['note']}'")
|
||||||
|
print(f" Note length: {len(str(latest_entry['note']))}")
|
||||||
|
is_empty = pd.isna(latest_entry["note"]) or latest_entry["note"] == ""
|
||||||
|
print(f" Note is empty/null: {is_empty}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_note_saving()
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test the update_entry functionality with notes
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add src directory to path to import modules
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||||
|
|
||||||
|
from data_manager import DataManager
|
||||||
|
from medicine_manager import MedicineManager
|
||||||
|
from pathology_manager import PathologyManager
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_entry_with_note():
|
||||||
|
"""Test updating an entry with a note"""
|
||||||
|
print("Testing update_entry functionality with notes...")
|
||||||
|
|
||||||
|
# Initialize logger
|
||||||
|
logger = logging.getLogger("test")
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Add console handler to see debug output
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setLevel(logging.DEBUG)
|
||||||
|
formatter = logging.Formatter("%(levelname)s - %(message)s")
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
# Initialize managers
|
||||||
|
medicine_manager = MedicineManager("medicines.json")
|
||||||
|
pathology_manager = PathologyManager("pathologies.json")
|
||||||
|
data_manager = DataManager(
|
||||||
|
"thechart_data.csv", logger, medicine_manager, pathology_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load current data
|
||||||
|
df = data_manager.load_data()
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
print("No data found in CSV file")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(df)} entries in the data file")
|
||||||
|
|
||||||
|
# Find the most recent entry to test with
|
||||||
|
latest_entry = df.iloc[-1].copy()
|
||||||
|
original_date = latest_entry["date"]
|
||||||
|
|
||||||
|
print(f"Testing with entry: {original_date}")
|
||||||
|
print(f"Current note: '{latest_entry['note']}'")
|
||||||
|
|
||||||
|
# Create test values - keep everything the same but change the note
|
||||||
|
test_note = "This is a test note to verify saving functionality!"
|
||||||
|
|
||||||
|
# Build values list (same format as the UI would send)
|
||||||
|
values = [original_date] # date
|
||||||
|
|
||||||
|
# Add pathology values
|
||||||
|
pathology_keys = pathology_manager.get_pathology_keys()
|
||||||
|
for key in pathology_keys:
|
||||||
|
values.append(latest_entry.get(key, 0))
|
||||||
|
|
||||||
|
# Add medicine values and doses
|
||||||
|
medicine_keys = medicine_manager.get_medicine_keys()
|
||||||
|
for key in medicine_keys:
|
||||||
|
values.append(latest_entry.get(key, 0)) # medicine checkbox
|
||||||
|
values.append(latest_entry.get(f"{key}_doses", "")) # medicine doses
|
||||||
|
|
||||||
|
# Add the test note
|
||||||
|
values.append(test_note)
|
||||||
|
|
||||||
|
print(f"Values to save: {values}")
|
||||||
|
print(f"Note in values: '{values[-1]}'")
|
||||||
|
|
||||||
|
# Test the update
|
||||||
|
success = data_manager.update_entry(original_date, values)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("Update successful!")
|
||||||
|
|
||||||
|
# Reload and verify
|
||||||
|
df_after = data_manager.load_data()
|
||||||
|
updated_entry = df_after[df_after["date"] == original_date].iloc[0]
|
||||||
|
|
||||||
|
print(f"Note after update: '{updated_entry['note']}'")
|
||||||
|
print(f"Note correctly saved: {updated_entry['note'] == test_note}")
|
||||||
|
|
||||||
|
# Reset the note back to original
|
||||||
|
values[-1] = latest_entry["note"]
|
||||||
|
data_manager.update_entry(original_date, values)
|
||||||
|
print("Reverted note back to original")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Update failed!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_update_entry_with_note()
|
||||||
+161
-55
@@ -9,7 +9,7 @@ from pathology_manager import PathologyManager
|
|||||||
|
|
||||||
|
|
||||||
class DataManager:
|
class DataManager:
|
||||||
"""Handle all data operations for the application."""
|
"""Handle all data operations for the application with performance optimizations."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -22,10 +22,21 @@ class DataManager:
|
|||||||
self.logger: logging.Logger = logger
|
self.logger: logging.Logger = logger
|
||||||
self.medicine_manager = medicine_manager
|
self.medicine_manager = medicine_manager
|
||||||
self.pathology_manager = pathology_manager
|
self.pathology_manager = pathology_manager
|
||||||
|
|
||||||
|
# Cache for loaded data to avoid repeated file I/O
|
||||||
|
self._data_cache: pd.DataFrame | None = None
|
||||||
|
self._cache_timestamp: float = 0
|
||||||
|
self._headers_cache: tuple[str, ...] | None = None
|
||||||
|
self._dtype_cache: dict[str, type] | None = None
|
||||||
|
|
||||||
self._initialize_csv_file()
|
self._initialize_csv_file()
|
||||||
|
|
||||||
def _get_csv_headers(self) -> list[str]:
|
def _get_csv_headers(self) -> tuple[str, ...]:
|
||||||
"""Get CSV headers based on current pathology and medicine configuration."""
|
"""Get CSV headers based on current pathology and medicine configuration.
|
||||||
|
Cached to avoid repeated computation."""
|
||||||
|
if self._headers_cache is not None:
|
||||||
|
return self._headers_cache
|
||||||
|
|
||||||
# Start with date
|
# Start with date
|
||||||
headers = ["date"]
|
headers = ["date"]
|
||||||
|
|
||||||
@@ -37,7 +48,9 @@ class DataManager:
|
|||||||
for medicine_key in self.medicine_manager.get_medicine_keys():
|
for medicine_key in self.medicine_manager.get_medicine_keys():
|
||||||
headers.extend([medicine_key, f"{medicine_key}_doses"])
|
headers.extend([medicine_key, f"{medicine_key}_doses"])
|
||||||
|
|
||||||
return headers + ["note"]
|
result = tuple(headers + ["note"])
|
||||||
|
self._headers_cache = result
|
||||||
|
return result
|
||||||
|
|
||||||
def _initialize_csv_file(self) -> None:
|
def _initialize_csv_file(self) -> None:
|
||||||
"""Create CSV file with headers if it doesn't exist or is empty."""
|
"""Create CSV file with headers if it doesn't exist or is empty."""
|
||||||
@@ -46,27 +59,74 @@ class DataManager:
|
|||||||
writer = csv.writer(file)
|
writer = csv.writer(file)
|
||||||
writer.writerow(self._get_csv_headers())
|
writer.writerow(self._get_csv_headers())
|
||||||
|
|
||||||
|
def _invalidate_cache(self) -> None:
|
||||||
|
"""Invalidate the data cache when data changes."""
|
||||||
|
self._data_cache = None
|
||||||
|
self._cache_timestamp = 0
|
||||||
|
|
||||||
|
def _should_reload_data(self) -> bool:
|
||||||
|
"""Check if data should be reloaded based on file modification time."""
|
||||||
|
if self._data_cache is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_mtime = os.path.getmtime(self.filename)
|
||||||
|
return file_mtime > self._cache_timestamp
|
||||||
|
except OSError:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _get_dtype_dict(self) -> dict[str, type]:
|
||||||
|
"""Get pandas dtype dictionary for efficient reading.
|
||||||
|
Cached to avoid recreation."""
|
||||||
|
if self._dtype_cache is not None:
|
||||||
|
return self._dtype_cache
|
||||||
|
|
||||||
|
dtype_dict = {"date": str, "note": str}
|
||||||
|
|
||||||
|
# Add pathology types
|
||||||
|
for pathology_key in self.pathology_manager.get_pathology_keys():
|
||||||
|
dtype_dict[pathology_key] = int
|
||||||
|
|
||||||
|
# Add medicine types
|
||||||
|
for medicine_key in self.medicine_manager.get_medicine_keys():
|
||||||
|
dtype_dict[medicine_key] = int
|
||||||
|
dtype_dict[f"{medicine_key}_doses"] = str
|
||||||
|
|
||||||
|
self._dtype_cache = dtype_dict
|
||||||
|
return dtype_dict
|
||||||
|
|
||||||
def load_data(self) -> pd.DataFrame:
|
def load_data(self) -> pd.DataFrame:
|
||||||
"""Load data from CSV file."""
|
"""Load data from CSV file with caching for better performance."""
|
||||||
if not os.path.exists(self.filename) or os.path.getsize(self.filename) == 0:
|
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.")
|
self.logger.warning("CSV file is empty or doesn't exist. No data to load.")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
# Use cached data if available and file hasn't changed
|
||||||
|
if not self._should_reload_data():
|
||||||
|
return self._data_cache.copy()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Build dtype dictionary dynamically
|
# Use pre-built dtype dictionary for faster parsing
|
||||||
dtype_dict = {"date": str, "note": str}
|
dtype_dict = self._get_dtype_dict()
|
||||||
|
|
||||||
# Add pathology types
|
# Read with optimized settings
|
||||||
for pathology_key in self.pathology_manager.get_pathology_keys():
|
df: pd.DataFrame = pd.read_csv(
|
||||||
dtype_dict[pathology_key] = int
|
self.filename,
|
||||||
|
dtype=dtype_dict,
|
||||||
|
na_filter=False, # Don't convert to NaN, keep as empty strings
|
||||||
|
engine="c", # Use faster C engine
|
||||||
|
)
|
||||||
|
|
||||||
# Add medicine types
|
# Sort only if needed (check if already sorted)
|
||||||
for medicine_key in self.medicine_manager.get_medicine_keys():
|
if len(df) > 1 and not df["date"].is_monotonic_increasing:
|
||||||
dtype_dict[medicine_key] = int
|
df = df.sort_values(by="date").reset_index(drop=True)
|
||||||
dtype_dict[f"{medicine_key}_doses"] = str
|
|
||||||
|
# Cache the data and timestamp
|
||||||
|
self._data_cache = df.copy()
|
||||||
|
self._cache_timestamp = os.path.getmtime(self.filename)
|
||||||
|
|
||||||
|
return df.copy()
|
||||||
|
|
||||||
df: pd.DataFrame = pd.read_csv(self.filename, dtype=dtype_dict).fillna("")
|
|
||||||
return df.sort_values(by="date").reset_index(drop=True)
|
|
||||||
except pd.errors.EmptyDataError:
|
except pd.errors.EmptyDataError:
|
||||||
self.logger.warning("CSV file is empty. No data to load.")
|
self.logger.warning("CSV file is empty. No data to load.")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
@@ -75,69 +135,104 @@ class DataManager:
|
|||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
def add_entry(self, entry_data: list[str | int]) -> bool:
|
def add_entry(self, entry_data: list[str | int]) -> bool:
|
||||||
"""Add a new entry to the CSV file."""
|
"""Add a new entry to the CSV file with optimized duplicate checking."""
|
||||||
try:
|
try:
|
||||||
# Check if date already exists
|
# Quick duplicate check using cached data if available
|
||||||
df: pd.DataFrame = self.load_data()
|
|
||||||
date_to_add: str = str(entry_data[0])
|
date_to_add: str = str(entry_data[0])
|
||||||
|
|
||||||
if not df.empty and date_to_add in df["date"].values:
|
if self._data_cache is not None:
|
||||||
self.logger.warning(f"Entry with date {date_to_add} already exists.")
|
# Use cached data for duplicate check
|
||||||
return False
|
if date_to_add in self._data_cache["date"].values:
|
||||||
|
self.logger.warning(
|
||||||
|
f"Entry with date {date_to_add} already exists."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# Fallback to loading data if no cache
|
||||||
|
df: pd.DataFrame = self.load_data()
|
||||||
|
if not df.empty and date_to_add in df["date"].values:
|
||||||
|
self.logger.warning(
|
||||||
|
f"Entry with date {date_to_add} already exists."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Write to file
|
||||||
with open(self.filename, mode="a", newline="") as file:
|
with open(self.filename, mode="a", newline="") as file:
|
||||||
writer = csv.writer(file)
|
writer = csv.writer(file)
|
||||||
writer.writerow(entry_data)
|
writer.writerow(entry_data)
|
||||||
|
|
||||||
|
# Invalidate cache since data changed
|
||||||
|
self._invalidate_cache()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error adding entry: {str(e)}")
|
self.logger.error(f"Error adding entry: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update_entry(self, original_date: str, values: list[str | int]) -> bool:
|
def update_entry(self, original_date: str, values: list[str | int]) -> bool:
|
||||||
"""Update an existing entry identified by original_date."""
|
"""Update an existing entry identified by original_date
|
||||||
|
with optimized processing."""
|
||||||
try:
|
try:
|
||||||
df: pd.DataFrame = self.load_data()
|
df: pd.DataFrame = self.load_data()
|
||||||
new_date: str = str(values[0])
|
new_date: str = str(values[0])
|
||||||
|
|
||||||
# If the date is being changed, check if the new date already exists
|
# Optimized duplicate check
|
||||||
if original_date != new_date and new_date in df["date"].values:
|
if original_date != new_date:
|
||||||
|
date_exists = (df["date"] == new_date).any()
|
||||||
|
if date_exists:
|
||||||
|
self.logger.warning(
|
||||||
|
f"Cannot update: entry with date {new_date} already exists."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get current CSV headers to match with values
|
||||||
|
headers = list(self._get_csv_headers())
|
||||||
|
|
||||||
|
# Ensure we have the right number of values with optimized padding
|
||||||
|
if len(values) < len(headers):
|
||||||
|
# Pad with defaults efficiently
|
||||||
|
padding_needed = len(headers) - len(values)
|
||||||
|
for i in range(padding_needed):
|
||||||
|
header_idx = len(values) + i
|
||||||
|
if header_idx < len(headers):
|
||||||
|
header = headers[header_idx]
|
||||||
|
if header == "note" or header.endswith("_doses"):
|
||||||
|
values.append("")
|
||||||
|
else:
|
||||||
|
values.append(0)
|
||||||
|
|
||||||
|
# Use vectorized update for better performance
|
||||||
|
mask = df["date"] == original_date
|
||||||
|
if mask.any():
|
||||||
|
df.loc[mask, headers] = values
|
||||||
|
# Write back to CSV with optimized method
|
||||||
|
df.to_csv(self.filename, index=False, mode="w")
|
||||||
|
self._invalidate_cache()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
f"Cannot update: entry with date {new_date} already exists."
|
f"Entry with date {original_date} not found for update."
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Get current CSV headers to match with values
|
|
||||||
headers = self._get_csv_headers()
|
|
||||||
|
|
||||||
# Ensure we have the right number of values
|
|
||||||
if len(values) != len(headers):
|
|
||||||
self.logger.warning(
|
|
||||||
f"Value count mismatch: expected {len(headers)}, got {len(values)}"
|
|
||||||
)
|
|
||||||
# Pad with defaults if too few values
|
|
||||||
while len(values) < len(headers):
|
|
||||||
header = headers[len(values)]
|
|
||||||
if header == "note" or header.endswith("_doses"):
|
|
||||||
values.append("")
|
|
||||||
else:
|
|
||||||
values.append(0)
|
|
||||||
|
|
||||||
# Update the row using column names
|
|
||||||
df.loc[df["date"] == original_date, headers] = values
|
|
||||||
df.to_csv(self.filename, index=False)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error updating entry: {str(e)}")
|
self.logger.error(f"Error updating entry: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_entry(self, date: str) -> bool:
|
def delete_entry(self, date: str) -> bool:
|
||||||
"""Delete an entry identified by date."""
|
"""Delete an entry identified by date with optimized processing."""
|
||||||
try:
|
try:
|
||||||
df: pd.DataFrame = self.load_data()
|
df: pd.DataFrame = self.load_data()
|
||||||
# Remove the row with the matching date
|
original_len = len(df)
|
||||||
|
|
||||||
|
# Use vectorized filtering for better performance
|
||||||
df = df[df["date"] != date]
|
df = df[df["date"] != date]
|
||||||
# Write the updated dataframe back to the CSV
|
|
||||||
df.to_csv(self.filename, index=False)
|
# Only write if something was actually deleted
|
||||||
|
if len(df) < original_len:
|
||||||
|
df.to_csv(self.filename, index=False, mode="w")
|
||||||
|
self._invalidate_cache()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error deleting entry: {str(e)}")
|
self.logger.error(f"Error deleting entry: {str(e)}")
|
||||||
@@ -146,23 +241,34 @@ class DataManager:
|
|||||||
def get_today_medicine_doses(
|
def get_today_medicine_doses(
|
||||||
self, date: str, medicine_name: str
|
self, date: str, medicine_name: str
|
||||||
) -> list[tuple[str, str]]:
|
) -> list[tuple[str, str]]:
|
||||||
"""Get list of (timestamp, dose) tuples for a medicine on a given date."""
|
"""Get list of (timestamp, dose) tuples for a medicine on a given date
|
||||||
|
with caching."""
|
||||||
try:
|
try:
|
||||||
df: pd.DataFrame = self.load_data()
|
df: pd.DataFrame = self.load_data()
|
||||||
if df.empty or date not in df["date"].values:
|
if df.empty:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Use vectorized filtering for better performance
|
||||||
|
date_mask = df["date"] == date
|
||||||
|
if not date_mask.any():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
dose_column = f"{medicine_name}_doses"
|
dose_column = f"{medicine_name}_doses"
|
||||||
doses_str = df.loc[df["date"] == date, dose_column].iloc[0]
|
if dose_column not in df.columns:
|
||||||
|
return []
|
||||||
|
|
||||||
|
doses_str = df.loc[date_mask, dose_column].iloc[0]
|
||||||
|
|
||||||
if not doses_str:
|
if not doses_str:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Optimized dose parsing
|
||||||
doses = []
|
doses = []
|
||||||
for dose_entry in doses_str.split("|"):
|
for dose_entry in doses_str.split("|"):
|
||||||
if ":" in dose_entry:
|
if ":" in dose_entry:
|
||||||
timestamp, dose = dose_entry.split(":", 1)
|
parts = dose_entry.split(":", 1)
|
||||||
doses.append((timestamp, dose))
|
if len(parts) == 2:
|
||||||
|
doses.append((parts[0], parts[1]))
|
||||||
|
|
||||||
return doses
|
return doses
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -0,0 +1,385 @@
|
|||||||
|
"""
|
||||||
|
Export Manager for TheChart Application
|
||||||
|
|
||||||
|
Handles exporting data and graphs to various formats:
|
||||||
|
- CSV data to JSON, XML
|
||||||
|
- Graphs to PDF (with data tables)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from xml.dom import minidom
|
||||||
|
from xml.etree.ElementTree import Element, SubElement, tostring
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from reportlab.lib import colors
|
||||||
|
from reportlab.lib.pagesizes import A4
|
||||||
|
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||||
|
from reportlab.lib.units import inch
|
||||||
|
from reportlab.platypus import (
|
||||||
|
Image,
|
||||||
|
Paragraph,
|
||||||
|
SimpleDocTemplate,
|
||||||
|
Spacer,
|
||||||
|
Table,
|
||||||
|
TableStyle,
|
||||||
|
)
|
||||||
|
|
||||||
|
from data_manager import DataManager
|
||||||
|
from graph_manager import GraphManager
|
||||||
|
from medicine_manager import MedicineManager
|
||||||
|
from pathology_manager import PathologyManager
|
||||||
|
|
||||||
|
|
||||||
|
class ExportManager:
|
||||||
|
"""Handle data and graph export operations."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data_manager: DataManager,
|
||||||
|
graph_manager: GraphManager,
|
||||||
|
medicine_manager: MedicineManager,
|
||||||
|
pathology_manager: PathologyManager,
|
||||||
|
logger: logging.Logger,
|
||||||
|
) -> None:
|
||||||
|
self.data_manager = data_manager
|
||||||
|
self.graph_manager = graph_manager
|
||||||
|
self.medicine_manager = medicine_manager
|
||||||
|
self.pathology_manager = pathology_manager
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def export_data_to_json(self, export_path: str) -> bool:
|
||||||
|
"""Export CSV data to JSON format."""
|
||||||
|
try:
|
||||||
|
df = self.data_manager.load_data()
|
||||||
|
if df.empty:
|
||||||
|
self.logger.warning("No data to export")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Convert DataFrame to dictionary with better structure
|
||||||
|
export_data = {
|
||||||
|
"metadata": {
|
||||||
|
"export_date": datetime.now().isoformat(),
|
||||||
|
"total_entries": len(df),
|
||||||
|
"date_range": {
|
||||||
|
"start": df["date"].min() if not df.empty else None,
|
||||||
|
"end": df["date"].max() if not df.empty else None,
|
||||||
|
},
|
||||||
|
"pathologies": list(self.pathology_manager.get_pathology_keys()),
|
||||||
|
"medicines": list(self.medicine_manager.get_medicine_keys()),
|
||||||
|
},
|
||||||
|
"entries": df.to_dict(orient="records"),
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(export_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(export_data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
self.logger.info(f"Data exported to JSON: {export_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error exporting to JSON: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def export_data_to_xml(self, export_path: str) -> bool:
|
||||||
|
"""Export CSV data to XML format."""
|
||||||
|
try:
|
||||||
|
df = self.data_manager.load_data()
|
||||||
|
if df.empty:
|
||||||
|
self.logger.warning("No data to export")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create root element
|
||||||
|
root = Element("thechart_data")
|
||||||
|
|
||||||
|
# Add metadata
|
||||||
|
metadata = SubElement(root, "metadata")
|
||||||
|
SubElement(metadata, "export_date").text = datetime.now().isoformat()
|
||||||
|
SubElement(metadata, "total_entries").text = str(len(df))
|
||||||
|
|
||||||
|
# Date range
|
||||||
|
date_range = SubElement(metadata, "date_range")
|
||||||
|
SubElement(date_range, "start").text = (
|
||||||
|
df["date"].min() if not df.empty else ""
|
||||||
|
)
|
||||||
|
SubElement(date_range, "end").text = (
|
||||||
|
df["date"].max() if not df.empty else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pathologies
|
||||||
|
pathologies = SubElement(metadata, "pathologies")
|
||||||
|
for pathology in self.pathology_manager.get_pathology_keys():
|
||||||
|
SubElement(pathologies, "pathology").text = pathology
|
||||||
|
|
||||||
|
# Medicines
|
||||||
|
medicines = SubElement(metadata, "medicines")
|
||||||
|
for medicine in self.medicine_manager.get_medicine_keys():
|
||||||
|
SubElement(medicines, "medicine").text = medicine
|
||||||
|
|
||||||
|
# Add entries
|
||||||
|
entries = SubElement(root, "entries")
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
entry = SubElement(entries, "entry")
|
||||||
|
for column, value in row.items():
|
||||||
|
elem = SubElement(entry, column.replace(" ", "_"))
|
||||||
|
elem.text = str(value) if pd.notna(value) else ""
|
||||||
|
|
||||||
|
# Pretty print XML
|
||||||
|
rough_string = tostring(root, "utf-8")
|
||||||
|
reparsed = minidom.parseString(rough_string)
|
||||||
|
pretty_xml = reparsed.toprettyxml(indent=" ")
|
||||||
|
|
||||||
|
with open(export_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(pretty_xml)
|
||||||
|
|
||||||
|
self.logger.info(f"Data exported to XML: {export_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error exporting to XML: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _save_graph_as_image(self, temp_dir: Path) -> str | None:
|
||||||
|
"""Save current graph as temporary image for PDF inclusion."""
|
||||||
|
try:
|
||||||
|
# Check if graph manager exists
|
||||||
|
if self.graph_manager is None:
|
||||||
|
self.logger.warning("No graph manager available for export")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check if graph manager and figure exist
|
||||||
|
if not hasattr(self.graph_manager, "fig") or self.graph_manager.fig is None:
|
||||||
|
self.logger.warning("No graph figure available for export")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Ensure graph is up to date with current data
|
||||||
|
df = self.data_manager.load_data()
|
||||||
|
if not df.empty:
|
||||||
|
self.graph_manager.update_graph(df)
|
||||||
|
else:
|
||||||
|
self.logger.warning("No data available to update graph for export")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Ensure temp directory exists
|
||||||
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
temp_image_path = temp_dir / "graph.png"
|
||||||
|
|
||||||
|
# Save the current figure
|
||||||
|
self.graph_manager.fig.savefig(
|
||||||
|
str(temp_image_path),
|
||||||
|
dpi=150,
|
||||||
|
bbox_inches="tight",
|
||||||
|
facecolor="white",
|
||||||
|
edgecolor="none",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the file was actually created
|
||||||
|
if not temp_image_path.exists():
|
||||||
|
self.logger.error(
|
||||||
|
f"Graph image file was not created: {temp_image_path}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.logger.info(f"Graph image saved successfully: {temp_image_path}")
|
||||||
|
return str(temp_image_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error saving graph image: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def export_to_pdf(self, export_path: str, include_graph: bool = True) -> bool:
|
||||||
|
"""Export data and optionally graph to PDF format."""
|
||||||
|
try:
|
||||||
|
df = self.data_manager.load_data()
|
||||||
|
|
||||||
|
# Create PDF document
|
||||||
|
doc = SimpleDocTemplate(
|
||||||
|
export_path,
|
||||||
|
pagesize=A4,
|
||||||
|
rightMargin=72,
|
||||||
|
leftMargin=72,
|
||||||
|
topMargin=72,
|
||||||
|
bottomMargin=18,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get styles
|
||||||
|
styles = getSampleStyleSheet()
|
||||||
|
title_style = ParagraphStyle(
|
||||||
|
"CustomTitle",
|
||||||
|
parent=styles["Heading1"],
|
||||||
|
fontSize=18,
|
||||||
|
spaceAfter=30,
|
||||||
|
textColor=colors.darkblue,
|
||||||
|
)
|
||||||
|
|
||||||
|
story = []
|
||||||
|
|
||||||
|
# Title
|
||||||
|
story.append(Paragraph("TheChart - Medication Tracker Export", title_style))
|
||||||
|
story.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
# Export metadata
|
||||||
|
export_info = [
|
||||||
|
f"Export Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||||
|
f"Total Entries: {len(df) if not df.empty else 0}",
|
||||||
|
]
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
export_info.extend(
|
||||||
|
[
|
||||||
|
f"Date Range: {df['date'].min()} to {df['date'].max()}",
|
||||||
|
(
|
||||||
|
"Pathologies: "
|
||||||
|
+ ", ".join(self.pathology_manager.get_pathology_keys())
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Medicines: "
|
||||||
|
+ ", ".join(self.medicine_manager.get_medicine_keys())
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for info in export_info:
|
||||||
|
story.append(Paragraph(info, styles["Normal"]))
|
||||||
|
|
||||||
|
story.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
# Include graph if requested and available
|
||||||
|
if include_graph:
|
||||||
|
temp_dir = Path(export_path).parent / "temp_export"
|
||||||
|
|
||||||
|
try:
|
||||||
|
graph_path = self._save_graph_as_image(temp_dir)
|
||||||
|
if graph_path and os.path.exists(graph_path):
|
||||||
|
story.append(
|
||||||
|
Paragraph("Data Visualization", styles["Heading2"])
|
||||||
|
)
|
||||||
|
story.append(Spacer(1, 10))
|
||||||
|
|
||||||
|
# Add graph image
|
||||||
|
img = Image(graph_path, width=6 * inch, height=3.6 * inch)
|
||||||
|
story.append(img)
|
||||||
|
story.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
# Clean up temp image
|
||||||
|
os.remove(graph_path)
|
||||||
|
else:
|
||||||
|
# Graph not available, add a note instead
|
||||||
|
story.append(
|
||||||
|
Paragraph("Data Visualization", styles["Heading2"])
|
||||||
|
)
|
||||||
|
story.append(Spacer(1, 10))
|
||||||
|
story.append(
|
||||||
|
Paragraph(
|
||||||
|
"Graph not available - no data to visualize or graph "
|
||||||
|
"not generated yet.",
|
||||||
|
styles["Normal"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
story.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error including graph in PDF: {str(e)}")
|
||||||
|
# Add error note instead of failing completely
|
||||||
|
story.append(Paragraph("Data Visualization", styles["Heading2"]))
|
||||||
|
story.append(Spacer(1, 10))
|
||||||
|
story.append(
|
||||||
|
Paragraph(
|
||||||
|
f"Graph could not be included: {str(e)}", styles["Normal"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
story.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up temp directory
|
||||||
|
if temp_dir.exists():
|
||||||
|
with contextlib.suppress(OSError):
|
||||||
|
temp_dir.rmdir()
|
||||||
|
|
||||||
|
# Add data table if we have data
|
||||||
|
if not df.empty:
|
||||||
|
story.append(Paragraph("Data Table", styles["Heading2"]))
|
||||||
|
story.append(Spacer(1, 10))
|
||||||
|
|
||||||
|
# Prepare table data - limit columns for better PDF formatting
|
||||||
|
display_columns = ["date"]
|
||||||
|
for pathology_key in self.pathology_manager.get_pathology_keys():
|
||||||
|
display_columns.append(pathology_key)
|
||||||
|
for medicine_key in self.medicine_manager.get_medicine_keys():
|
||||||
|
display_columns.append(medicine_key)
|
||||||
|
display_columns.append("note")
|
||||||
|
|
||||||
|
# Filter dataframe to display columns that exist
|
||||||
|
available_columns = [
|
||||||
|
col for col in display_columns if col in df.columns
|
||||||
|
]
|
||||||
|
display_df = df[available_columns].copy()
|
||||||
|
|
||||||
|
# Truncate long notes for better table formatting
|
||||||
|
if "note" in display_df.columns:
|
||||||
|
display_df["note"] = display_df["note"].apply(
|
||||||
|
lambda x: (str(x)[:50] + "...") if len(str(x)) > 50 else str(x)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert to table data
|
||||||
|
table_data = [available_columns] # Headers
|
||||||
|
for _, row in display_df.iterrows():
|
||||||
|
table_data.append(
|
||||||
|
[str(val) if pd.notna(val) else "" for val in row]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create table with styling
|
||||||
|
table = Table(table_data, repeatRows=1)
|
||||||
|
table.setStyle(
|
||||||
|
TableStyle(
|
||||||
|
[
|
||||||
|
("BACKGROUND", (0, 0), (-1, 0), colors.grey),
|
||||||
|
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
|
||||||
|
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||||
|
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||||
|
("FONTSIZE", (0, 0), (-1, 0), 10),
|
||||||
|
("BOTTOMPADDING", (0, 0), (-1, 0), 12),
|
||||||
|
("BACKGROUND", (0, 1), (-1, -1), colors.beige),
|
||||||
|
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
|
||||||
|
("FONTSIZE", (0, 1), (-1, -1), 8),
|
||||||
|
("GRID", (0, 0), (-1, -1), 1, colors.black),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
story.append(table)
|
||||||
|
else:
|
||||||
|
story.append(
|
||||||
|
Paragraph("No data available to export.", styles["Normal"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build PDF
|
||||||
|
doc.build(story)
|
||||||
|
|
||||||
|
self.logger.info(f"Data exported to PDF: {export_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error exporting to PDF: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_export_info(self) -> dict[str, Any]:
|
||||||
|
"""Get information about available data for export."""
|
||||||
|
df = self.data_manager.load_data()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_entries": len(df) if not df.empty else 0,
|
||||||
|
"date_range": {
|
||||||
|
"start": df["date"].min() if not df.empty else None,
|
||||||
|
"end": df["date"].max() if not df.empty else None,
|
||||||
|
},
|
||||||
|
"pathologies": list(self.pathology_manager.get_pathology_keys()),
|
||||||
|
"medicines": list(self.medicine_manager.get_medicine_keys()),
|
||||||
|
"has_data": not df.empty,
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
"""
|
||||||
|
Export Window for TheChart Application
|
||||||
|
|
||||||
|
Provides a GUI interface for exporting data and graphs to various formats.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from pathlib import Path
|
||||||
|
from tkinter import filedialog, messagebox, ttk
|
||||||
|
|
||||||
|
from export_manager import ExportManager
|
||||||
|
|
||||||
|
|
||||||
|
class ExportWindow:
|
||||||
|
"""Export window for data and graph export functionality."""
|
||||||
|
|
||||||
|
def __init__(self, parent: tk.Tk, export_manager: ExportManager) -> None:
|
||||||
|
self.parent = parent
|
||||||
|
self.export_manager = export_manager
|
||||||
|
|
||||||
|
# Create the export window
|
||||||
|
self.window = tk.Toplevel(parent)
|
||||||
|
self.window.title("Export Data")
|
||||||
|
self.window.geometry("500x450") # Made taller to ensure buttons are visible
|
||||||
|
self.window.resizable(False, False)
|
||||||
|
|
||||||
|
# Center the window
|
||||||
|
self._center_window()
|
||||||
|
|
||||||
|
# Make window modal
|
||||||
|
self.window.transient(parent)
|
||||||
|
self.window.grab_set()
|
||||||
|
|
||||||
|
# Setup the UI
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _center_window(self) -> None:
|
||||||
|
"""Center the export window on the parent window."""
|
||||||
|
self.window.update_idletasks()
|
||||||
|
|
||||||
|
# Get window dimensions
|
||||||
|
width = self.window.winfo_width()
|
||||||
|
height = self.window.winfo_height()
|
||||||
|
|
||||||
|
# Get parent window position and size
|
||||||
|
parent_x = self.parent.winfo_rootx()
|
||||||
|
parent_y = self.parent.winfo_rooty()
|
||||||
|
parent_width = self.parent.winfo_width()
|
||||||
|
parent_height = self.parent.winfo_height()
|
||||||
|
|
||||||
|
# Calculate position to center on parent
|
||||||
|
x = parent_x + (parent_width // 2) - (width // 2)
|
||||||
|
y = parent_y + (parent_height // 2) - (height // 2)
|
||||||
|
|
||||||
|
self.window.geometry(f"{width}x{height}+{x}+{y}")
|
||||||
|
|
||||||
|
def _setup_ui(self) -> None:
|
||||||
|
"""Setup the export window UI."""
|
||||||
|
# Main frame
|
||||||
|
main_frame = ttk.Frame(self.window, padding="15")
|
||||||
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title_label = ttk.Label(
|
||||||
|
main_frame, text="Export Data & Graphs", font=("Arial", 14, "bold")
|
||||||
|
)
|
||||||
|
title_label.pack(pady=(0, 15))
|
||||||
|
|
||||||
|
# Create scrollable content area for the main content
|
||||||
|
content_frame = ttk.Frame(main_frame)
|
||||||
|
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Export info section
|
||||||
|
self._create_info_section(content_frame)
|
||||||
|
|
||||||
|
# Export options section
|
||||||
|
self._create_options_section(content_frame)
|
||||||
|
|
||||||
|
# Buttons section - always at the bottom
|
||||||
|
self._create_buttons_section(main_frame)
|
||||||
|
|
||||||
|
def _create_info_section(self, parent: ttk.Frame) -> None:
|
||||||
|
"""Create the data information section."""
|
||||||
|
info_frame = ttk.LabelFrame(parent, text="Data Summary", padding="10")
|
||||||
|
info_frame.pack(fill=tk.X, pady=(0, 20))
|
||||||
|
|
||||||
|
# Get export info
|
||||||
|
export_info = self.export_manager.get_export_info()
|
||||||
|
|
||||||
|
# Display information
|
||||||
|
if export_info["has_data"]:
|
||||||
|
info_text = f"""Total Entries: {export_info["total_entries"]}
|
||||||
|
Date Range: {export_info["date_range"]["start"]} to {export_info["date_range"]["end"]}
|
||||||
|
Pathologies: {", ".join(export_info["pathologies"])}
|
||||||
|
Medicines: {", ".join(export_info["medicines"])}"""
|
||||||
|
else:
|
||||||
|
info_text = "No data available for export."
|
||||||
|
|
||||||
|
info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT)
|
||||||
|
info_label.pack(anchor=tk.W)
|
||||||
|
|
||||||
|
def _create_options_section(self, parent: ttk.Frame) -> None:
|
||||||
|
"""Create the export options section."""
|
||||||
|
options_frame = ttk.LabelFrame(parent, text="Export Options", padding="10")
|
||||||
|
options_frame.pack(fill=tk.X, pady=(0, 20))
|
||||||
|
|
||||||
|
# Include graph option (for PDF export)
|
||||||
|
self.include_graph_var = tk.BooleanVar(value=True)
|
||||||
|
graph_check = ttk.Checkbutton(
|
||||||
|
options_frame,
|
||||||
|
text="Include graph in PDF export",
|
||||||
|
variable=self.include_graph_var,
|
||||||
|
)
|
||||||
|
graph_check.pack(anchor=tk.W, pady=(0, 10))
|
||||||
|
|
||||||
|
# Format selection
|
||||||
|
format_label = ttk.Label(options_frame, text="Export Format:")
|
||||||
|
format_label.pack(anchor=tk.W)
|
||||||
|
|
||||||
|
self.format_var = tk.StringVar(value="JSON")
|
||||||
|
formats = ["JSON", "XML", "PDF"]
|
||||||
|
|
||||||
|
for fmt in formats:
|
||||||
|
radio = ttk.Radiobutton(
|
||||||
|
options_frame, text=fmt, variable=self.format_var, value=fmt
|
||||||
|
)
|
||||||
|
radio.pack(anchor=tk.W, padx=(20, 0))
|
||||||
|
|
||||||
|
def _create_buttons_section(self, parent: ttk.Frame) -> None:
|
||||||
|
"""Create the buttons section."""
|
||||||
|
# Add a separator for visual clarity
|
||||||
|
separator = ttk.Separator(parent, orient="horizontal")
|
||||||
|
separator.pack(fill=tk.X, pady=(10, 10))
|
||||||
|
|
||||||
|
button_frame = ttk.Frame(parent)
|
||||||
|
button_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
|
# Export button with more prominent styling
|
||||||
|
export_btn = ttk.Button(
|
||||||
|
button_frame, text="Export...", command=self._handle_export
|
||||||
|
)
|
||||||
|
export_btn.pack(side=tk.LEFT, padx=(10, 10), pady=5)
|
||||||
|
|
||||||
|
# Cancel button
|
||||||
|
cancel_btn = ttk.Button(
|
||||||
|
button_frame, text="Cancel", command=self.window.destroy
|
||||||
|
)
|
||||||
|
cancel_btn.pack(side=tk.RIGHT, padx=(10, 10), pady=5)
|
||||||
|
|
||||||
|
def _handle_export(self) -> None:
|
||||||
|
"""Handle the export button click."""
|
||||||
|
# Check if we have data to export
|
||||||
|
export_info = self.export_manager.get_export_info()
|
||||||
|
if not export_info["has_data"]:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"No Data", "There is no data available to export.", parent=self.window
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get selected format
|
||||||
|
selected_format = self.format_var.get()
|
||||||
|
|
||||||
|
# Define file types for dialog
|
||||||
|
file_types = {
|
||||||
|
"JSON": [("JSON files", "*.json"), ("All files", "*.*")],
|
||||||
|
"XML": [("XML files", "*.xml"), ("All files", "*.*")],
|
||||||
|
"PDF": [("PDF files", "*.pdf"), ("All files", "*.*")],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default filename
|
||||||
|
default_name = f"thechart_export.{selected_format.lower()}"
|
||||||
|
|
||||||
|
# Show save dialog
|
||||||
|
filename = filedialog.asksaveasfilename(
|
||||||
|
parent=self.window,
|
||||||
|
title=f"Export as {selected_format}",
|
||||||
|
defaultextension=f".{selected_format.lower()}",
|
||||||
|
filetypes=file_types[selected_format],
|
||||||
|
initialfile=default_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not filename:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Perform export based on selected format
|
||||||
|
success = False
|
||||||
|
try:
|
||||||
|
if selected_format == "JSON":
|
||||||
|
success = self.export_manager.export_data_to_json(filename)
|
||||||
|
elif selected_format == "XML":
|
||||||
|
success = self.export_manager.export_data_to_xml(filename)
|
||||||
|
elif selected_format == "PDF":
|
||||||
|
include_graph = self.include_graph_var.get()
|
||||||
|
success = self.export_manager.export_to_pdf(
|
||||||
|
filename, include_graph=include_graph
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Export Successful",
|
||||||
|
f"Data exported successfully to:\n{filename}",
|
||||||
|
parent=self.window,
|
||||||
|
)
|
||||||
|
# Ask if user wants to open the file location
|
||||||
|
if messagebox.askyesno(
|
||||||
|
"Open Location",
|
||||||
|
"Would you like to open the file location?",
|
||||||
|
parent=self.window,
|
||||||
|
):
|
||||||
|
self._open_file_location(filename)
|
||||||
|
|
||||||
|
self.window.destroy()
|
||||||
|
else:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Export Failed",
|
||||||
|
f"Failed to export data as {selected_format}. "
|
||||||
|
"Please check the logs for more details.",
|
||||||
|
parent=self.window,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Export Error",
|
||||||
|
f"An error occurred during export:\n{str(e)}",
|
||||||
|
parent=self.window,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _open_file_location(self, filepath: str) -> None:
|
||||||
|
"""Open the file location in the system file manager."""
|
||||||
|
try:
|
||||||
|
file_path = Path(filepath)
|
||||||
|
directory = file_path.parent
|
||||||
|
|
||||||
|
# Use system-specific command to open file manager
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
subprocess.run(["explorer", str(directory)], check=False)
|
||||||
|
elif sys.platform == "darwin":
|
||||||
|
subprocess.run(["open", str(directory)], check=False)
|
||||||
|
else: # Linux and other Unix-like systems
|
||||||
|
subprocess.run(["xdg-open", str(directory)], check=False)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# If opening file location fails, just ignore silently
|
||||||
|
pass
|
||||||
+221
-169
@@ -12,7 +12,8 @@ from pathology_manager import PathologyManager
|
|||||||
|
|
||||||
|
|
||||||
class GraphManager:
|
class GraphManager:
|
||||||
"""Handle all graph-related operations for the application."""
|
"""Optimized version - Handle all graph-related operations for the
|
||||||
|
application with performance improvements."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -24,166 +25,206 @@ class GraphManager:
|
|||||||
self.medicine_manager = medicine_manager
|
self.medicine_manager = medicine_manager
|
||||||
self.pathology_manager = pathology_manager
|
self.pathology_manager = pathology_manager
|
||||||
|
|
||||||
# Configure graph frame to expand
|
# Initialize matplotlib with optimized settings
|
||||||
self.parent_frame.grid_rowconfigure(0, weight=1)
|
self.fig: matplotlib.figure.Figure = plt.figure(figsize=(10, 6), dpi=80)
|
||||||
self.parent_frame.grid_columnconfigure(0, weight=1)
|
self.ax: Axes = self.fig.add_subplot(111)
|
||||||
|
|
||||||
self._initialize_toggle_vars()
|
# Cache for current data to avoid reprocessing
|
||||||
|
self.current_data: pd.DataFrame = pd.DataFrame()
|
||||||
|
self._last_plot_hash: str = ""
|
||||||
|
|
||||||
|
# Initialize UI components
|
||||||
|
self.toggle_vars: dict[str, tk.IntVar] = {}
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
|
self._initialize_toggle_vars()
|
||||||
def _initialize_toggle_vars(self) -> None:
|
|
||||||
"""Initialize toggle variables for chart elements."""
|
|
||||||
self.toggle_vars: dict[str, tk.BooleanVar] = {}
|
|
||||||
|
|
||||||
# Initialize pathology toggles dynamically
|
|
||||||
for pathology_key in self.pathology_manager.get_pathology_keys():
|
|
||||||
pathology = self.pathology_manager.get_pathology(pathology_key)
|
|
||||||
default_value = pathology.default_enabled if pathology else True
|
|
||||||
self.toggle_vars[pathology_key] = tk.BooleanVar(value=default_value)
|
|
||||||
|
|
||||||
# Add medicine toggles dynamically
|
|
||||||
for medicine_key in self.medicine_manager.get_medicine_keys():
|
|
||||||
medicine = self.medicine_manager.get_medicine(medicine_key)
|
|
||||||
default_value = medicine.default_enabled if medicine else False
|
|
||||||
self.toggle_vars[medicine_key] = tk.BooleanVar(value=default_value)
|
|
||||||
|
|
||||||
def _setup_ui(self) -> None:
|
|
||||||
"""Set up the UI components."""
|
|
||||||
# Create control frame for toggles
|
|
||||||
self.control_frame: ttk.Frame = ttk.Frame(self.parent_frame)
|
|
||||||
self.control_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
|
||||||
|
|
||||||
# Create toggle checkboxes
|
|
||||||
self._create_chart_toggles()
|
self._create_chart_toggles()
|
||||||
|
|
||||||
# Create graph frame
|
def _initialize_toggle_vars(self) -> None:
|
||||||
self.graph_frame: ttk.Frame = ttk.Frame(self.parent_frame)
|
"""Initialize toggle variables for chart elements with optimization."""
|
||||||
self.graph_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
|
# Initialize pathology toggles
|
||||||
|
for pathology_key in self.pathology_manager.get_pathology_keys():
|
||||||
|
self.toggle_vars[pathology_key] = tk.IntVar(value=1)
|
||||||
|
|
||||||
# Reconfigure parent frame for new layout
|
# Initialize medicine toggles (unchecked by default)
|
||||||
self.parent_frame.grid_rowconfigure(1, weight=1)
|
for medicine_key in self.medicine_manager.get_medicine_keys():
|
||||||
self.parent_frame.grid_columnconfigure(0, weight=1)
|
self.toggle_vars[medicine_key] = tk.IntVar(value=0)
|
||||||
|
|
||||||
# Initialize matplotlib figure and canvas
|
def _setup_ui(self) -> None:
|
||||||
self.fig: matplotlib.figure.Figure
|
"""Set up the UI components with performance optimizations."""
|
||||||
self.ax: Axes
|
# Create canvas with optimized settings
|
||||||
self.fig, self.ax = plt.subplots()
|
self.canvas = FigureCanvasTkAgg(self.fig, master=self.parent_frame)
|
||||||
self.canvas: FigureCanvasTkAgg = FigureCanvasTkAgg(
|
self.canvas.draw_idle() # Use draw_idle for better performance
|
||||||
figure=self.fig, master=self.graph_frame
|
|
||||||
)
|
|
||||||
self.canvas.get_tk_widget().pack(fill="both", expand=True)
|
|
||||||
|
|
||||||
# Store current data for replotting
|
# Pack canvas
|
||||||
self.current_data: pd.DataFrame = pd.DataFrame()
|
canvas_widget = self.canvas.get_tk_widget()
|
||||||
|
canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Create control frame
|
||||||
|
self.control_frame = ttk.Frame(self.parent_frame)
|
||||||
|
self.control_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=2)
|
||||||
|
|
||||||
def _create_chart_toggles(self) -> None:
|
def _create_chart_toggles(self) -> None:
|
||||||
"""Create toggle controls for chart elements."""
|
"""Create toggle controls for chart elements with improved layout."""
|
||||||
ttk.Label(self.control_frame, text="Show/Hide Elements:").pack(
|
# Pathology toggles
|
||||||
side="left", padx=5
|
pathology_frame = ttk.LabelFrame(
|
||||||
|
self.control_frame, text="Pathologies", padding="5"
|
||||||
)
|
)
|
||||||
|
pathology_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
|
||||||
|
|
||||||
# Pathologies toggles - dynamic based on pathology manager
|
# Use grid for better layout
|
||||||
pathologies_frame = ttk.LabelFrame(self.control_frame, text="Pathologies")
|
row, col = 0, 0
|
||||||
pathologies_frame.pack(side="left", padx=5, pady=2)
|
|
||||||
|
|
||||||
for pathology_key in self.pathology_manager.get_pathology_keys():
|
for pathology_key in self.pathology_manager.get_pathology_keys():
|
||||||
pathology = self.pathology_manager.get_pathology(pathology_key)
|
pathology = self.pathology_manager.get_pathology(pathology_key)
|
||||||
if pathology:
|
if pathology:
|
||||||
checkbox = ttk.Checkbutton(
|
display_name = pathology.display_name
|
||||||
pathologies_frame,
|
text = (
|
||||||
text=pathology.display_name,
|
display_name[:10] + "..."
|
||||||
|
if len(display_name) > 10
|
||||||
|
else display_name
|
||||||
|
)
|
||||||
|
cb = ttk.Checkbutton(
|
||||||
|
pathology_frame,
|
||||||
|
text=text,
|
||||||
variable=self.toggle_vars[pathology_key],
|
variable=self.toggle_vars[pathology_key],
|
||||||
command=self._handle_toggle_changed,
|
command=self._handle_toggle_changed,
|
||||||
)
|
)
|
||||||
checkbox.pack(side="left", padx=3)
|
cb.grid(row=row, column=col, sticky="w", padx=2)
|
||||||
|
col += 1
|
||||||
|
if col > 1: # 2 columns max
|
||||||
|
col = 0
|
||||||
|
row += 1
|
||||||
|
|
||||||
# Medicines toggles - dynamic based on medicine manager
|
# Medicine toggles
|
||||||
medicines_frame = ttk.LabelFrame(self.control_frame, text="Medicines")
|
medicine_frame = ttk.LabelFrame(
|
||||||
medicines_frame.pack(side="left", padx=5, pady=2)
|
self.control_frame, text="Medicines", padding="5"
|
||||||
|
)
|
||||||
|
medicine_frame.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=2)
|
||||||
|
|
||||||
|
# Use grid for medicines too
|
||||||
|
row, col = 0, 0
|
||||||
for medicine_key in self.medicine_manager.get_medicine_keys():
|
for medicine_key in self.medicine_manager.get_medicine_keys():
|
||||||
medicine = self.medicine_manager.get_medicine(medicine_key)
|
medicine = self.medicine_manager.get_medicine(medicine_key)
|
||||||
if medicine:
|
if medicine:
|
||||||
checkbox = ttk.Checkbutton(
|
med_name = medicine.display_name
|
||||||
medicines_frame,
|
text = med_name[:10] + "..." if len(med_name) > 10 else med_name
|
||||||
text=medicine.display_name,
|
cb = ttk.Checkbutton(
|
||||||
|
medicine_frame,
|
||||||
|
text=text,
|
||||||
variable=self.toggle_vars[medicine_key],
|
variable=self.toggle_vars[medicine_key],
|
||||||
command=self._handle_toggle_changed,
|
command=self._handle_toggle_changed,
|
||||||
)
|
)
|
||||||
checkbox.pack(side="left", padx=3)
|
cb.grid(row=row, column=col, sticky="w", padx=2)
|
||||||
|
col += 1
|
||||||
|
if col > 2: # 3 columns max for medicines
|
||||||
|
col = 0
|
||||||
|
row += 1
|
||||||
|
|
||||||
def _handle_toggle_changed(self) -> None:
|
def _handle_toggle_changed(self) -> None:
|
||||||
"""Handle toggle changes by replotting the graph."""
|
"""Handle toggle changes by replotting the graph with optimization."""
|
||||||
if not self.current_data.empty:
|
if not self.current_data.empty:
|
||||||
self._plot_graph_data(self.current_data)
|
self._plot_graph_data(self.current_data)
|
||||||
|
|
||||||
def update_graph(self, df: pd.DataFrame) -> None:
|
def update_graph(self, df: pd.DataFrame) -> None:
|
||||||
"""Update the graph with new data."""
|
"""Update the graph with new data using optimization checks."""
|
||||||
self.current_data = df.copy() if not df.empty else pd.DataFrame()
|
# Create hash of data to avoid unnecessary redraws
|
||||||
self._plot_graph_data(df)
|
data_hash = str(hash(str(df.values.tobytes()) if not df.empty else "empty"))
|
||||||
|
|
||||||
|
# Only update if data actually changed
|
||||||
|
if data_hash != self._last_plot_hash or self.current_data.empty:
|
||||||
|
self.current_data = df.copy() if not df.empty else pd.DataFrame()
|
||||||
|
self._last_plot_hash = data_hash
|
||||||
|
self._plot_graph_data(df)
|
||||||
|
|
||||||
def _plot_graph_data(self, df: pd.DataFrame) -> None:
|
def _plot_graph_data(self, df: pd.DataFrame) -> None:
|
||||||
"""Plot the graph data with current toggle settings."""
|
"""Plot the graph data with current toggle settings using optimizations."""
|
||||||
self.ax.clear()
|
# Use batch updates to reduce redraws
|
||||||
if not df.empty:
|
with plt.ioff(): # Turn off interactive mode for batch updates
|
||||||
# Convert dates and sort
|
self.ax.clear()
|
||||||
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)
|
|
||||||
|
|
||||||
# Track if any series are plotted
|
if not df.empty:
|
||||||
has_plotted_series = False
|
# Optimize data processing
|
||||||
|
df_processed = self._preprocess_data(df)
|
||||||
|
|
||||||
# Plot pathology data series based on toggle states
|
# Track if any series are plotted
|
||||||
for pathology_key in self.pathology_manager.get_pathology_keys():
|
has_plotted_series = self._plot_pathology_data(df_processed)
|
||||||
if self.toggle_vars[pathology_key].get():
|
medicine_data = self._plot_medicine_data(df_processed)
|
||||||
pathology = self.pathology_manager.get_pathology(pathology_key)
|
|
||||||
if pathology and pathology_key in df.columns:
|
|
||||||
label = f"{pathology.display_name} ({pathology.scale_info})"
|
|
||||||
linestyle = (
|
|
||||||
"dashed"
|
|
||||||
if pathology.scale_orientation == "inverted"
|
|
||||||
else "-"
|
|
||||||
)
|
|
||||||
self._plot_series(df, pathology_key, label, "o", linestyle)
|
|
||||||
has_plotted_series = True
|
|
||||||
|
|
||||||
# Plot medicine dose data
|
if has_plotted_series or medicine_data["has_plotted"]:
|
||||||
# Get medicine colors from medicine manager
|
self._configure_graph_appearance(medicine_data)
|
||||||
medicine_colors = self.medicine_manager.get_graph_colors()
|
|
||||||
|
|
||||||
# Get medicines dynamically from medicine manager
|
# Single draw call at the end
|
||||||
medicines = self.medicine_manager.get_medicine_keys()
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
# Track medicines with and without data for legend
|
def _preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||||
medicines_with_data = []
|
"""Preprocess data for plotting with optimizations."""
|
||||||
medicines_without_data = []
|
df = df.copy()
|
||||||
|
# Batch convert dates and sort
|
||||||
|
df["date"] = pd.to_datetime(df["date"], cache=True)
|
||||||
|
df = df.sort_values(by="date")
|
||||||
|
df.set_index(keys="date", inplace=True)
|
||||||
|
return df
|
||||||
|
|
||||||
for medicine in medicines:
|
def _plot_pathology_data(self, df: pd.DataFrame) -> bool:
|
||||||
dose_column = f"{medicine}_doses"
|
"""Plot pathology data series with optimizations."""
|
||||||
if self.toggle_vars[medicine].get() and dose_column in df.columns:
|
has_plotted_series = False
|
||||||
# Calculate daily dose totals
|
|
||||||
daily_doses = []
|
|
||||||
for dose_str in df[dose_column]:
|
|
||||||
total_dose = self._calculate_daily_dose(dose_str)
|
|
||||||
daily_doses.append(total_dose)
|
|
||||||
|
|
||||||
# Only plot if there are non-zero doses
|
# Batch plot pathology data
|
||||||
if any(dose > 0 for dose in daily_doses):
|
pathology_keys = self.pathology_manager.get_pathology_keys()
|
||||||
medicines_with_data.append(medicine)
|
active_pathologies = [
|
||||||
# Scale doses for better visibility
|
key
|
||||||
# (divide by 10 to fit with 0-10 scale)
|
for key in pathology_keys
|
||||||
scaled_doses = [dose / 10 for dose in daily_doses]
|
if self.toggle_vars[key].get() and key in df.columns
|
||||||
|
]
|
||||||
|
|
||||||
# Calculate total dosage for this medicine across all days
|
for pathology_key in active_pathologies:
|
||||||
total_medicine_dose = sum(daily_doses)
|
pathology = self.pathology_manager.get_pathology(pathology_key)
|
||||||
non_zero_doses = [d for d in daily_doses if d > 0]
|
if pathology:
|
||||||
avg_dose = total_medicine_dose / len(non_zero_doses)
|
label = f"{pathology.display_name} ({pathology.scale_info})"
|
||||||
|
linestyle = (
|
||||||
|
"dashed" if pathology.scale_orientation == "inverted" else "-"
|
||||||
|
)
|
||||||
|
self._plot_series(df, pathology_key, label, "o", linestyle)
|
||||||
|
has_plotted_series = True
|
||||||
|
|
||||||
# Create more informative label
|
return has_plotted_series
|
||||||
|
|
||||||
|
def _plot_medicine_data(self, df: pd.DataFrame) -> dict:
|
||||||
|
"""Plot medicine data with optimizations."""
|
||||||
|
result = {"has_plotted": False, "with_data": [], "without_data": []}
|
||||||
|
|
||||||
|
# Get medicine colors and keys in batch
|
||||||
|
medicine_colors = self.medicine_manager.get_graph_colors()
|
||||||
|
medicines = self.medicine_manager.get_medicine_keys()
|
||||||
|
|
||||||
|
# Pre-calculate daily doses for all medicines to avoid repeated computation
|
||||||
|
medicine_doses = {}
|
||||||
|
for medicine in medicines:
|
||||||
|
dose_column = f"{medicine}_doses"
|
||||||
|
if dose_column in df.columns:
|
||||||
|
daily_doses = [
|
||||||
|
self._calculate_daily_dose(dose_str) for dose_str in df[dose_column]
|
||||||
|
]
|
||||||
|
medicine_doses[medicine] = daily_doses
|
||||||
|
|
||||||
|
# Plot medicines with data
|
||||||
|
for medicine in medicines:
|
||||||
|
if self.toggle_vars[medicine].get() and medicine in medicine_doses:
|
||||||
|
daily_doses = medicine_doses[medicine]
|
||||||
|
|
||||||
|
# Check if there's any data to plot
|
||||||
|
if any(dose > 0 for dose in daily_doses):
|
||||||
|
result["with_data"].append(medicine)
|
||||||
|
|
||||||
|
# Optimize dose scaling and bar plotting
|
||||||
|
scaled_doses = [dose / 10 for dose in daily_doses]
|
||||||
|
|
||||||
|
# Calculate statistics more efficiently
|
||||||
|
non_zero_doses = [d for d in daily_doses if d > 0]
|
||||||
|
if non_zero_doses:
|
||||||
|
avg_dose = sum(daily_doses) / len(non_zero_doses)
|
||||||
label = f"{medicine.capitalize()} (avg: {avg_dose:.1f}mg)"
|
label = f"{medicine.capitalize()} (avg: {avg_dose:.1f}mg)"
|
||||||
|
|
||||||
|
# Single bar plot call
|
||||||
self.ax.bar(
|
self.ax.bar(
|
||||||
df.index,
|
df.index,
|
||||||
scaled_doses,
|
scaled_doses,
|
||||||
@@ -193,56 +234,59 @@ class GraphManager:
|
|||||||
width=0.6,
|
width=0.6,
|
||||||
bottom=-max(scaled_doses) * 1.1 if scaled_doses else -1,
|
bottom=-max(scaled_doses) * 1.1 if scaled_doses else -1,
|
||||||
)
|
)
|
||||||
has_plotted_series = True
|
result["has_plotted"] = True
|
||||||
else:
|
else:
|
||||||
# Medicine is toggled on but has no dose data
|
# Medicine is toggled on but has no dose data
|
||||||
if self.toggle_vars[medicine].get():
|
if self.toggle_vars[medicine].get():
|
||||||
medicines_without_data.append(medicine)
|
result["without_data"].append(medicine)
|
||||||
|
|
||||||
# Configure graph appearance
|
return result
|
||||||
if has_plotted_series:
|
|
||||||
# Get current legend handles and labels
|
|
||||||
handles, labels = self.ax.get_legend_handles_labels()
|
|
||||||
|
|
||||||
# Add information about medicines without data if any are toggled on
|
def _configure_graph_appearance(self, medicine_data: dict) -> None:
|
||||||
if medicines_without_data:
|
"""Configure graph appearance with optimizations."""
|
||||||
# Add a text note about medicines without dose data
|
# Get legend data in batch
|
||||||
med_list = ", ".join(medicines_without_data)
|
handles, labels = self.ax.get_legend_handles_labels()
|
||||||
info_text = f"Tracked (no doses): {med_list}"
|
|
||||||
labels.append(info_text)
|
|
||||||
# Create a dummy handle for the info text (invisible)
|
|
||||||
from matplotlib.patches import Rectangle
|
|
||||||
|
|
||||||
dummy_handle = Rectangle(
|
# Add information about medicines without data if any are toggled on
|
||||||
(0, 0), 1, 1, fc="w", fill=False, edgecolor="none", linewidth=0
|
if medicine_data["without_data"]:
|
||||||
)
|
med_list = ", ".join(medicine_data["without_data"])
|
||||||
handles.append(dummy_handle)
|
info_text = f"Tracked (no doses): {med_list}"
|
||||||
|
labels.append(info_text)
|
||||||
|
|
||||||
# Create an expanded legend with better formatting
|
# Create dummy handle more efficiently
|
||||||
self.ax.legend(
|
from matplotlib.patches import Rectangle
|
||||||
handles,
|
|
||||||
labels,
|
|
||||||
loc="upper left",
|
|
||||||
bbox_to_anchor=(0, 1),
|
|
||||||
ncol=2, # Display in 2 columns for better space usage
|
|
||||||
fontsize="small",
|
|
||||||
frameon=True,
|
|
||||||
fancybox=True,
|
|
||||||
shadow=True,
|
|
||||||
framealpha=0.9,
|
|
||||||
)
|
|
||||||
self.ax.set_title("Medication Effects Over Time")
|
|
||||||
self.ax.set_xlabel("Date")
|
|
||||||
self.ax.set_ylabel("Rating (0-10) / Dose (mg)")
|
|
||||||
|
|
||||||
# Adjust y-axis to accommodate medicine bars at bottom
|
dummy_handle = Rectangle(
|
||||||
current_ylim = self.ax.get_ylim()
|
(0, 0), 1, 1, fc="w", fill=False, edgecolor="none", linewidth=0
|
||||||
self.ax.set_ylim(bottom=current_ylim[0], top=max(10, current_ylim[1]))
|
)
|
||||||
|
handles.append(dummy_handle)
|
||||||
|
|
||||||
self.fig.autofmt_xdate()
|
# Create legend with optimized settings
|
||||||
|
if handles and labels:
|
||||||
|
self.ax.legend(
|
||||||
|
handles,
|
||||||
|
labels,
|
||||||
|
loc="upper left",
|
||||||
|
bbox_to_anchor=(0, 1),
|
||||||
|
ncol=2,
|
||||||
|
fontsize="small",
|
||||||
|
frameon=True,
|
||||||
|
fancybox=True,
|
||||||
|
shadow=True,
|
||||||
|
framealpha=0.9,
|
||||||
|
)
|
||||||
|
|
||||||
# Redraw the canvas
|
# Set titles and labels
|
||||||
self.canvas.draw()
|
self.ax.set_title("Medication Effects Over Time")
|
||||||
|
self.ax.set_xlabel("Date")
|
||||||
|
self.ax.set_ylabel("Rating (0-10) / Dose (mg)")
|
||||||
|
|
||||||
|
# Optimize y-axis configuration
|
||||||
|
current_ylim = self.ax.get_ylim()
|
||||||
|
self.ax.set_ylim(bottom=current_ylim[0], top=max(10, current_ylim[1]))
|
||||||
|
|
||||||
|
# Optimize date formatting
|
||||||
|
self.fig.autofmt_xdate()
|
||||||
|
|
||||||
def _plot_series(
|
def _plot_series(
|
||||||
self,
|
self,
|
||||||
@@ -252,25 +296,28 @@ class GraphManager:
|
|||||||
marker: str,
|
marker: str,
|
||||||
linestyle: str,
|
linestyle: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Helper method to plot a data series."""
|
"""Helper method to plot a data series with optimizations."""
|
||||||
|
# Use more efficient plotting parameters
|
||||||
self.ax.plot(
|
self.ax.plot(
|
||||||
df.index,
|
df.index,
|
||||||
df[column],
|
df[column],
|
||||||
marker=marker,
|
marker=marker,
|
||||||
linestyle=linestyle,
|
linestyle=linestyle,
|
||||||
label=label,
|
label=label,
|
||||||
|
markersize=4, # Smaller markers for better performance
|
||||||
|
linewidth=1.5, # Optimized line width
|
||||||
)
|
)
|
||||||
|
|
||||||
def _calculate_daily_dose(self, dose_str: str) -> float:
|
def _calculate_daily_dose(self, dose_str: str) -> float:
|
||||||
"""Calculate total daily dose from dose string format."""
|
"""Calculate total daily dose from dose string format with optimizations."""
|
||||||
if not dose_str or pd.isna(dose_str) or str(dose_str).lower() == "nan":
|
if not dose_str or pd.isna(dose_str) or str(dose_str).lower() == "nan":
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
total_dose = 0.0
|
total_dose = 0.0
|
||||||
# Handle different separators and clean the string
|
# Optimize string processing
|
||||||
dose_str = str(dose_str).replace("•", "").strip()
|
dose_str = str(dose_str).replace("•", "").strip()
|
||||||
|
|
||||||
# Split by | or by spaces if no | present
|
# More efficient splitting and processing
|
||||||
dose_entries = dose_str.split("|") if "|" in dose_str else [dose_str]
|
dose_entries = dose_str.split("|") if "|" in dose_str else [dose_str]
|
||||||
|
|
||||||
for entry in dose_entries:
|
for entry in dose_entries:
|
||||||
@@ -279,15 +326,15 @@ class GraphManager:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Extract dose part after the last colon (timestamp:dose format)
|
# More efficient dose extraction
|
||||||
dose_part = entry.split(":")[-1] if ":" in entry else entry
|
dose_part = entry.split(":")[-1] if ":" in entry else entry
|
||||||
|
|
||||||
# Extract numeric part from dose (e.g., "150mg" -> 150)
|
# Optimized numeric extraction
|
||||||
dose_value = ""
|
dose_value = ""
|
||||||
for char in dose_part:
|
for char in dose_part:
|
||||||
if char.isdigit() or char == ".":
|
if char.isdigit() or char == ".":
|
||||||
dose_value += char
|
dose_value += char
|
||||||
elif dose_value: # Stop at first non-digit after finding digits
|
elif dose_value:
|
||||||
break
|
break
|
||||||
|
|
||||||
if dose_value:
|
if dose_value:
|
||||||
@@ -298,5 +345,10 @@ class GraphManager:
|
|||||||
return total_dose
|
return total_dose
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Clean up resources."""
|
"""Clean up resources with proper optimization."""
|
||||||
plt.close(self.fig)
|
try:
|
||||||
|
# Clear the plot before closing
|
||||||
|
self.ax.clear()
|
||||||
|
plt.close(self.fig)
|
||||||
|
except Exception:
|
||||||
|
pass # Ignore cleanup errors
|
||||||
|
|||||||
+348
-34
@@ -7,14 +7,18 @@ from typing import Any
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from constants import LOG_LEVEL, LOG_PATH
|
from constants import LOG_CLEAR, LOG_LEVEL, LOG_PATH
|
||||||
from data_manager import DataManager
|
from data_manager import DataManager
|
||||||
|
from export_manager import ExportManager
|
||||||
|
from export_window import ExportWindow
|
||||||
from graph_manager import GraphManager
|
from graph_manager import GraphManager
|
||||||
from init import logger
|
from init import logger
|
||||||
from medicine_management_window import MedicineManagementWindow
|
from medicine_management_window import MedicineManagementWindow
|
||||||
from medicine_manager import MedicineManager
|
from medicine_manager import MedicineManager
|
||||||
from pathology_management_window import PathologyManagementWindow
|
from pathology_management_window import PathologyManagementWindow
|
||||||
from pathology_manager import PathologyManager
|
from pathology_manager import PathologyManager
|
||||||
|
from settings_window import SettingsWindow
|
||||||
|
from theme_manager import ThemeManager
|
||||||
from ui_manager import UIManager
|
from ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
@@ -40,16 +44,26 @@ class MedTrackerApp:
|
|||||||
Using default file: {self.filename}"
|
Using default file: {self.filename}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(f"Log level: {LOG_LEVEL}")
|
||||||
|
|
||||||
|
# Initialize theme manager first
|
||||||
|
self.theme_manager: ThemeManager = ThemeManager(self.root, logger)
|
||||||
|
|
||||||
if LOG_LEVEL == "DEBUG":
|
if LOG_LEVEL == "DEBUG":
|
||||||
logger.debug(f"Script name: {sys.argv[0]}")
|
logger.debug(f"Script name: {sys.argv[0]}")
|
||||||
logger.debug(f"Logs path: {LOG_PATH}")
|
logger.debug(f"Logs path: {LOG_PATH}")
|
||||||
|
logger.debug(f"Log clear: {LOG_CLEAR}")
|
||||||
logger.debug(f"First argument: {first_argument}")
|
logger.debug(f"First argument: {first_argument}")
|
||||||
|
|
||||||
# Initialize managers
|
# Initialize managers
|
||||||
self.medicine_manager: MedicineManager = MedicineManager(logger=logger)
|
self.medicine_manager: MedicineManager = MedicineManager(logger=logger)
|
||||||
self.pathology_manager: PathologyManager = PathologyManager(logger=logger)
|
self.pathology_manager: PathologyManager = PathologyManager(logger=logger)
|
||||||
self.ui_manager: UIManager = UIManager(
|
self.ui_manager: UIManager = UIManager(
|
||||||
root, logger, self.medicine_manager, self.pathology_manager
|
root,
|
||||||
|
logger,
|
||||||
|
self.medicine_manager,
|
||||||
|
self.pathology_manager,
|
||||||
|
self.theme_manager,
|
||||||
)
|
)
|
||||||
self.data_manager: DataManager = DataManager(
|
self.data_manager: DataManager = DataManager(
|
||||||
self.filename, logger, self.medicine_manager, self.pathology_manager
|
self.filename, logger, self.medicine_manager, self.pathology_manager
|
||||||
@@ -67,12 +81,38 @@ class MedTrackerApp:
|
|||||||
# Add menu bar
|
# Add menu bar
|
||||||
self._setup_menu()
|
self._setup_menu()
|
||||||
|
|
||||||
|
# Setup keyboard shortcuts
|
||||||
|
self._setup_keyboard_shortcuts()
|
||||||
|
|
||||||
|
# Center the window on screen
|
||||||
|
self._center_window()
|
||||||
|
|
||||||
|
def _center_window(self) -> None:
|
||||||
|
"""Center the main window on the screen."""
|
||||||
|
# Update the window to get accurate dimensions
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
|
# Get window dimensions
|
||||||
|
window_width = self.root.winfo_reqwidth()
|
||||||
|
window_height = self.root.winfo_reqheight()
|
||||||
|
|
||||||
|
# Get screen dimensions
|
||||||
|
screen_width = self.root.winfo_screenwidth()
|
||||||
|
screen_height = self.root.winfo_screenheight()
|
||||||
|
|
||||||
|
# Calculate position to center the window
|
||||||
|
x = (screen_width // 2) - (window_width // 2)
|
||||||
|
y = (screen_height // 2) - (window_height // 2)
|
||||||
|
|
||||||
|
# Set the window geometry
|
||||||
|
self.root.geometry(f"{window_width}x{window_height}+{x}+{y}")
|
||||||
|
|
||||||
def _setup_main_ui(self) -> None:
|
def _setup_main_ui(self) -> None:
|
||||||
"""Set up the main UI components."""
|
"""Set up the main UI components."""
|
||||||
import tkinter.ttk as ttk
|
import tkinter.ttk as ttk
|
||||||
|
|
||||||
# --- Main Frame ---
|
# --- Main Frame ---
|
||||||
main_frame: ttk.Frame = ttk.Frame(self.root, padding="10")
|
main_frame: ttk.Frame = ttk.Frame(self.root, padding="10", style="Card.TFrame")
|
||||||
main_frame.grid(row=0, column=0, sticky="nsew")
|
main_frame.grid(row=0, column=0, sticky="nsew")
|
||||||
|
|
||||||
# Configure root window grid
|
# Configure root window grid
|
||||||
@@ -80,7 +120,7 @@ class MedTrackerApp:
|
|||||||
self.root.grid_columnconfigure(0, weight=1)
|
self.root.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
# Configure main frame grid for scaling
|
# Configure main frame grid for scaling
|
||||||
for i in range(2):
|
for i in range(3): # Changed from 2 to 3 to accommodate status bar
|
||||||
main_frame.grid_rowconfigure(i, weight=1 if i == 1 else 0)
|
main_frame.grid_rowconfigure(i, weight=1 if i == 1 else 0)
|
||||||
main_frame.grid_columnconfigure(i, weight=3 if i == 1 else 1)
|
main_frame.grid_columnconfigure(i, weight=3 if i == 1 else 1)
|
||||||
logger.debug("Main frame and root grid configured for scaling.")
|
logger.debug("Main frame and root grid configured for scaling.")
|
||||||
@@ -91,6 +131,15 @@ class MedTrackerApp:
|
|||||||
graph_frame, self.medicine_manager, self.pathology_manager
|
graph_frame, self.medicine_manager, self.pathology_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Initialize export manager
|
||||||
|
self.export_manager: ExportManager = ExportManager(
|
||||||
|
self.data_manager,
|
||||||
|
self.graph_manager,
|
||||||
|
self.medicine_manager,
|
||||||
|
self.pathology_manager,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
|
||||||
# --- Create Input Frame ---
|
# --- Create Input Frame ---
|
||||||
input_ui: dict[str, Any] = self.ui_manager.create_input_frame(main_frame)
|
input_ui: dict[str, Any] = self.ui_manager.create_input_frame(main_frame)
|
||||||
self.input_frame: ttk.Frame = input_ui["frame"]
|
self.input_frame: ttk.Frame = input_ui["frame"]
|
||||||
@@ -104,12 +153,12 @@ class MedTrackerApp:
|
|||||||
self.input_frame,
|
self.input_frame,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"text": "Add Entry",
|
"text": "Add Entry (Ctrl+S)",
|
||||||
"command": self.add_new_entry,
|
"command": self.add_new_entry,
|
||||||
"fill": "both",
|
"fill": "both",
|
||||||
"expand": True,
|
"expand": True,
|
||||||
},
|
},
|
||||||
{"text": "Quit", "command": self.handle_window_closing},
|
{"text": "Quit (Ctrl+Q)", "command": self.handle_window_closing},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -118,38 +167,222 @@ class MedTrackerApp:
|
|||||||
self.tree: ttk.Treeview = table_ui["tree"]
|
self.tree: ttk.Treeview = table_ui["tree"]
|
||||||
self.tree.bind("<Double-1>", self.handle_double_click)
|
self.tree.bind("<Double-1>", self.handle_double_click)
|
||||||
|
|
||||||
|
# --- Create Status Bar ---
|
||||||
|
self.status_bar = self.ui_manager.create_status_bar(main_frame)
|
||||||
|
|
||||||
# Load data
|
# Load data
|
||||||
self.refresh_data_display()
|
self.refresh_data_display()
|
||||||
|
|
||||||
|
# Initialize status bar with ready message
|
||||||
|
self.ui_manager.update_status("Application ready", "info")
|
||||||
|
|
||||||
def _setup_menu(self) -> None:
|
def _setup_menu(self) -> None:
|
||||||
"""Set up the menu bar."""
|
"""Set up the menu bar."""
|
||||||
menubar = tk.Menu(self.root)
|
menubar = tk.Menu(self.root)
|
||||||
self.root.config(menu=menubar)
|
self.root.config(menu=menubar)
|
||||||
|
|
||||||
|
# File menu
|
||||||
|
file_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
menubar.add_cascade(label="File", menu=file_menu)
|
||||||
|
file_menu.add_command(
|
||||||
|
label="Export Data...",
|
||||||
|
command=self._open_export_window,
|
||||||
|
accelerator="Ctrl+E",
|
||||||
|
)
|
||||||
|
file_menu.add_separator()
|
||||||
|
file_menu.add_command(
|
||||||
|
label="Exit", command=self.handle_window_closing, accelerator="Ctrl+Q"
|
||||||
|
)
|
||||||
|
|
||||||
# Tools menu
|
# Tools menu
|
||||||
tools_menu = tk.Menu(menubar, tearoff=0)
|
tools_menu = tk.Menu(menubar, tearoff=0)
|
||||||
menubar.add_cascade(label="Tools", menu=tools_menu)
|
menubar.add_cascade(label="Tools", menu=tools_menu)
|
||||||
tools_menu.add_command(
|
tools_menu.add_command(
|
||||||
label="Manage Pathologies...", command=self._open_pathology_manager
|
label="Manage Pathologies...",
|
||||||
|
command=self._open_pathology_manager,
|
||||||
|
accelerator="Ctrl+P",
|
||||||
)
|
)
|
||||||
tools_menu.add_command(
|
tools_menu.add_command(
|
||||||
label="Manage Medicines...", command=self._open_medicine_manager
|
label="Manage Medicines...",
|
||||||
|
command=self._open_medicine_manager,
|
||||||
|
accelerator="Ctrl+M",
|
||||||
)
|
)
|
||||||
|
tools_menu.add_separator()
|
||||||
|
tools_menu.add_command(
|
||||||
|
label="Clear Entries", command=self._clear_entries, accelerator="Ctrl+N"
|
||||||
|
)
|
||||||
|
tools_menu.add_command(
|
||||||
|
label="Refresh Data", command=self.refresh_data_display, accelerator="F5"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Theme menu
|
||||||
|
theme_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
menubar.add_cascade(label="Theme", menu=theme_menu)
|
||||||
|
|
||||||
|
# Add quick theme options
|
||||||
|
available_themes = self.theme_manager.get_available_themes()
|
||||||
|
current_theme = self.theme_manager.get_current_theme()
|
||||||
|
|
||||||
|
for theme in available_themes:
|
||||||
|
theme_menu.add_radiobutton(
|
||||||
|
label=theme.title(),
|
||||||
|
command=lambda t=theme: self._change_theme(t),
|
||||||
|
value=theme == current_theme,
|
||||||
|
)
|
||||||
|
|
||||||
|
theme_menu.add_separator()
|
||||||
|
theme_menu.add_command(
|
||||||
|
label="More Settings...",
|
||||||
|
command=self._open_settings_window,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Help menu
|
||||||
|
help_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
menubar.add_cascade(label="Help", menu=help_menu)
|
||||||
|
help_menu.add_command(
|
||||||
|
label="Settings...",
|
||||||
|
command=self._open_settings_window,
|
||||||
|
accelerator="F2",
|
||||||
|
)
|
||||||
|
help_menu.add_separator()
|
||||||
|
help_menu.add_command(
|
||||||
|
label="Keyboard Shortcuts",
|
||||||
|
command=self._show_keyboard_shortcuts,
|
||||||
|
accelerator="F1",
|
||||||
|
)
|
||||||
|
help_menu.add_command(label="About", command=self._show_about_dialog)
|
||||||
|
|
||||||
|
def _setup_keyboard_shortcuts(self) -> None:
|
||||||
|
"""Set up keyboard shortcuts for common actions."""
|
||||||
|
# Bind keyboard shortcuts to the main window
|
||||||
|
self.root.bind("<Control-s>", lambda e: self.add_new_entry())
|
||||||
|
self.root.bind("<Control-S>", lambda e: self.add_new_entry())
|
||||||
|
self.root.bind("<Control-q>", lambda e: self.handle_window_closing())
|
||||||
|
self.root.bind("<Control-Q>", lambda e: self.handle_window_closing())
|
||||||
|
self.root.bind("<Control-e>", lambda e: self._open_export_window())
|
||||||
|
self.root.bind("<Control-E>", lambda e: self._open_export_window())
|
||||||
|
self.root.bind("<Control-n>", lambda e: self._clear_entries())
|
||||||
|
self.root.bind("<Control-N>", lambda e: self._clear_entries())
|
||||||
|
self.root.bind("<Control-r>", lambda e: self.refresh_data_display())
|
||||||
|
self.root.bind("<Control-R>", lambda e: self.refresh_data_display())
|
||||||
|
self.root.bind("<F5>", lambda e: self.refresh_data_display())
|
||||||
|
self.root.bind("<Control-m>", lambda e: self._open_medicine_manager())
|
||||||
|
self.root.bind("<Control-M>", lambda e: self._open_medicine_manager())
|
||||||
|
self.root.bind("<Control-p>", lambda e: self._open_pathology_manager())
|
||||||
|
self.root.bind("<Control-P>", lambda e: self._open_pathology_manager())
|
||||||
|
self.root.bind("<Delete>", lambda e: self._delete_selected_entry())
|
||||||
|
self.root.bind("<Escape>", lambda e: self._clear_selection())
|
||||||
|
self.root.bind("<F1>", lambda e: self._show_keyboard_shortcuts())
|
||||||
|
self.root.bind("<F2>", lambda e: self._open_settings_window())
|
||||||
|
|
||||||
|
# Make the window focusable so it can receive key events
|
||||||
|
self.root.focus_set()
|
||||||
|
|
||||||
|
logger.info("Keyboard shortcuts configured:")
|
||||||
|
logger.info(" Ctrl+S: Save/Add new entry")
|
||||||
|
logger.info(" Ctrl+Q: Quit application")
|
||||||
|
logger.info(" Ctrl+E: Export data")
|
||||||
|
logger.info(" Ctrl+N: Clear entries")
|
||||||
|
logger.info(" Ctrl+R/F5: Refresh data")
|
||||||
|
logger.info(" Ctrl+M: Manage medicines")
|
||||||
|
logger.info(" Ctrl+P: Manage pathologies")
|
||||||
|
logger.info(" Delete: Delete selected entry")
|
||||||
|
logger.info(" Escape: Clear selection")
|
||||||
|
logger.info(" F1: Show keyboard shortcuts help")
|
||||||
|
|
||||||
|
def _show_keyboard_shortcuts(self) -> None:
|
||||||
|
"""Show a dialog with keyboard shortcuts information."""
|
||||||
|
shortcuts_text = """Keyboard Shortcuts:
|
||||||
|
|
||||||
|
File Operations:
|
||||||
|
• Ctrl+S: Save/Add new entry
|
||||||
|
• Ctrl+Q: Quit application
|
||||||
|
• Ctrl+E: Export data
|
||||||
|
|
||||||
|
Data Management:
|
||||||
|
• Ctrl+N: Clear entries
|
||||||
|
• Ctrl+R / F5: Refresh data
|
||||||
|
|
||||||
|
Window Management:
|
||||||
|
• Ctrl+M: Manage medicines
|
||||||
|
• Ctrl+P: Manage pathologies
|
||||||
|
|
||||||
|
Table Operations:
|
||||||
|
• Delete: Delete selected entry
|
||||||
|
• Escape: Clear selection
|
||||||
|
• Double-click: Edit entry
|
||||||
|
|
||||||
|
Help:
|
||||||
|
• F1: Show this help dialog
|
||||||
|
• F2: Open settings window"""
|
||||||
|
|
||||||
|
messagebox.showinfo("Keyboard Shortcuts", shortcuts_text, parent=self.root)
|
||||||
|
|
||||||
|
def _change_theme(self, theme_name: str) -> None:
|
||||||
|
"""Change the application theme."""
|
||||||
|
if self.theme_manager.apply_theme(theme_name):
|
||||||
|
self.ui_manager.update_status(
|
||||||
|
f"Theme changed to: {theme_name.title()}", "info"
|
||||||
|
)
|
||||||
|
# Refresh the menu to update radio button selection
|
||||||
|
self._setup_menu()
|
||||||
|
else:
|
||||||
|
self.ui_manager.update_status(
|
||||||
|
f"Failed to apply theme: {theme_name}", "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_about_dialog(self) -> None:
|
||||||
|
"""Show about dialog."""
|
||||||
|
about_text = """TheChart - Medication Tracker
|
||||||
|
|
||||||
|
A simple application for tracking medications and pathologies.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
• Add daily medication and pathology entries
|
||||||
|
• Visual graphs and charts
|
||||||
|
• Data export capabilities
|
||||||
|
• Keyboard shortcuts for efficiency
|
||||||
|
|
||||||
|
Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||||
|
|
||||||
|
messagebox.showinfo("About TheChart", about_text, parent=self.root)
|
||||||
|
|
||||||
|
def _open_export_window(self) -> None:
|
||||||
|
"""Open the export window."""
|
||||||
|
self.ui_manager.update_status("Opening export window", "info")
|
||||||
|
ExportWindow(self.root, self.export_manager)
|
||||||
|
|
||||||
def _open_pathology_manager(self) -> None:
|
def _open_pathology_manager(self) -> None:
|
||||||
"""Open the pathology management window."""
|
"""Open the pathology management window."""
|
||||||
|
self.ui_manager.update_status("Opening pathology manager", "info")
|
||||||
PathologyManagementWindow(
|
PathologyManagementWindow(
|
||||||
self.root, self.pathology_manager, self._refresh_ui_after_config_change
|
self.root, self.pathology_manager, self._refresh_ui_after_config_change
|
||||||
)
|
)
|
||||||
|
|
||||||
def _open_medicine_manager(self) -> None:
|
def _open_medicine_manager(self) -> None:
|
||||||
"""Open the medicine management window."""
|
"""Open the medicine management window."""
|
||||||
|
self.ui_manager.update_status("Opening medicine manager", "info")
|
||||||
MedicineManagementWindow(
|
MedicineManagementWindow(
|
||||||
self.root, self.medicine_manager, self._refresh_ui_after_config_change
|
self.root, self.medicine_manager, self._refresh_ui_after_config_change
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _open_settings_window(self) -> None:
|
||||||
|
"""Open the settings window."""
|
||||||
|
self.ui_manager.update_status("Opening settings window", "info")
|
||||||
|
SettingsWindow(self.root, self.theme_manager, self.ui_manager)
|
||||||
|
|
||||||
def _refresh_ui_after_config_change(self) -> None:
|
def _refresh_ui_after_config_change(self) -> None:
|
||||||
"""Refresh UI components after pathology or medicine configuration changes."""
|
"""Refresh UI components after pathology or medicine configuration changes."""
|
||||||
|
self.ui_manager.update_status(
|
||||||
|
"Refreshing UI after configuration change", "info"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clear caches in optimized data manager
|
||||||
|
if hasattr(self.data_manager, "_invalidate_cache"):
|
||||||
|
self.data_manager._invalidate_cache()
|
||||||
|
self.data_manager._headers_cache = None
|
||||||
|
self.data_manager._dtype_cache = None
|
||||||
|
|
||||||
# Recreate the input frame with new pathologies and medicines
|
# Recreate the input frame with new pathologies and medicines
|
||||||
self.input_frame.destroy()
|
self.input_frame.destroy()
|
||||||
input_ui: dict[str, Any] = self.ui_manager.create_input_frame(
|
input_ui: dict[str, Any] = self.ui_manager.create_input_frame(
|
||||||
@@ -164,12 +397,12 @@ class MedTrackerApp:
|
|||||||
self.input_frame,
|
self.input_frame,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"text": "Add Entry",
|
"text": "Add Entry (Ctrl+S)",
|
||||||
"command": self.add_new_entry,
|
"command": self.add_new_entry,
|
||||||
"fill": "both",
|
"fill": "both",
|
||||||
"expand": True,
|
"expand": True,
|
||||||
},
|
},
|
||||||
{"text": "Quit", "command": self.handle_window_closing},
|
{"text": "Quit (Ctrl+Q)", "command": self.handle_window_closing},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -184,14 +417,59 @@ class MedTrackerApp:
|
|||||||
# Refresh data display
|
# Refresh data display
|
||||||
self.refresh_data_display()
|
self.refresh_data_display()
|
||||||
|
|
||||||
|
# Update status to show completion
|
||||||
|
self.ui_manager.update_status("UI refreshed successfully", "success")
|
||||||
|
|
||||||
|
def _delete_selected_entry(self) -> None:
|
||||||
|
"""Delete the currently selected entry in the table."""
|
||||||
|
selection = self.tree.selection()
|
||||||
|
if not selection:
|
||||||
|
self.ui_manager.update_status("No entry selected for deletion", "warning")
|
||||||
|
return
|
||||||
|
|
||||||
|
item_id = selection[0]
|
||||||
|
item_values = self.tree.item(item_id, "values")
|
||||||
|
|
||||||
|
if messagebox.askyesno(
|
||||||
|
"Delete Entry",
|
||||||
|
f"Are you sure you want to delete the entry for {item_values[0]}?",
|
||||||
|
parent=self.root,
|
||||||
|
):
|
||||||
|
date: str = item_values[0]
|
||||||
|
logger.debug(f"Deleting entry with date={date}")
|
||||||
|
|
||||||
|
self.ui_manager.update_status("Deleting entry...", "info")
|
||||||
|
if self.data_manager.delete_entry(date):
|
||||||
|
self.ui_manager.update_status("Entry deleted successfully!", "success")
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Success", "Entry deleted successfully!", parent=self.root
|
||||||
|
)
|
||||||
|
self.refresh_data_display()
|
||||||
|
else:
|
||||||
|
self.ui_manager.update_status("Failed to delete entry", "error")
|
||||||
|
messagebox.showerror(
|
||||||
|
"Error", "Failed to delete entry", parent=self.root
|
||||||
|
)
|
||||||
|
|
||||||
|
def _clear_selection(self) -> None:
|
||||||
|
"""Clear the current selection in the table."""
|
||||||
|
if self.tree.selection():
|
||||||
|
self.tree.selection_remove(self.tree.selection())
|
||||||
|
self.ui_manager.update_status("Selection cleared", "info")
|
||||||
|
|
||||||
def handle_double_click(self, event: tk.Event) -> None:
|
def handle_double_click(self, event: tk.Event) -> 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.")
|
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.ui_manager.update_status(
|
||||||
|
f"Opening entry for {item_values[0]} for editing", "info"
|
||||||
|
)
|
||||||
logger.debug(f"Editing item_id={item_id}, values={item_values}")
|
logger.debug(f"Editing item_id={item_id}, values={item_values}")
|
||||||
self._create_edit_window(item_id, item_values)
|
self._create_edit_window(item_id, item_values)
|
||||||
|
else:
|
||||||
|
self.ui_manager.update_status("No entries to edit", "warning")
|
||||||
|
|
||||||
def _create_edit_window(self, item_id: str, values: tuple[str, ...]) -> 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."""
|
||||||
@@ -293,8 +571,10 @@ class MedTrackerApp:
|
|||||||
|
|
||||||
values.append(note)
|
values.append(note)
|
||||||
|
|
||||||
|
self.ui_manager.update_status("Saving changes...", "info")
|
||||||
if self.data_manager.update_entry(original_date, values):
|
if self.data_manager.update_entry(original_date, values):
|
||||||
edit_win.destroy()
|
edit_win.destroy()
|
||||||
|
self.ui_manager.update_status("Entry updated successfully!", "success")
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"Success", "Entry updated successfully!", parent=self.root
|
"Success", "Entry updated successfully!", parent=self.root
|
||||||
)
|
)
|
||||||
@@ -304,6 +584,7 @@ class MedTrackerApp:
|
|||||||
# Check if it's a duplicate date issue
|
# Check if it's a duplicate date issue
|
||||||
df = self.data_manager.load_data()
|
df = self.data_manager.load_data()
|
||||||
if original_date != date and not df.empty and date in df["date"].values:
|
if original_date != date and not df.empty and date in df["date"].values:
|
||||||
|
self.ui_manager.update_status("Duplicate date found", "error")
|
||||||
messagebox.showerror(
|
messagebox.showerror(
|
||||||
"Error",
|
"Error",
|
||||||
f"An entry for date '{date}' already exists. "
|
f"An entry for date '{date}' already exists. "
|
||||||
@@ -311,6 +592,7 @@ class MedTrackerApp:
|
|||||||
parent=edit_win,
|
parent=edit_win,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
self.ui_manager.update_status("Failed to save changes", "error")
|
||||||
messagebox.showerror("Error", "Failed to save changes", parent=edit_win)
|
messagebox.showerror("Error", "Failed to save changes", parent=edit_win)
|
||||||
|
|
||||||
def handle_window_closing(self) -> None:
|
def handle_window_closing(self) -> None:
|
||||||
@@ -355,10 +637,13 @@ class MedTrackerApp:
|
|||||||
|
|
||||||
# Check if date is empty
|
# Check if date is empty
|
||||||
if not self.date_var.get().strip():
|
if not self.date_var.get().strip():
|
||||||
|
self.ui_manager.update_status("Please enter a date", "error")
|
||||||
messagebox.showerror("Error", "Please enter a date.", parent=self.root)
|
messagebox.showerror("Error", "Please enter a date.", parent=self.root)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.ui_manager.update_status("Adding new entry...", "info")
|
||||||
if self.data_manager.add_entry(entry):
|
if self.data_manager.add_entry(entry):
|
||||||
|
self.ui_manager.update_status("Entry added successfully!", "success")
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"Success", "Entry added successfully!", parent=self.root
|
"Success", "Entry added successfully!", parent=self.root
|
||||||
)
|
)
|
||||||
@@ -368,6 +653,7 @@ class MedTrackerApp:
|
|||||||
# Check if it's a duplicate date by trying to load existing data
|
# Check if it's a duplicate date by trying to load existing data
|
||||||
df = self.data_manager.load_data()
|
df = self.data_manager.load_data()
|
||||||
if not df.empty and self.date_var.get() in df["date"].values:
|
if not df.empty and self.date_var.get() in df["date"].values:
|
||||||
|
self.ui_manager.update_status("Duplicate entry found", "error")
|
||||||
messagebox.showerror(
|
messagebox.showerror(
|
||||||
"Error",
|
"Error",
|
||||||
f"An entry for date '{self.date_var.get()}' already exists. "
|
f"An entry for date '{self.date_var.get()}' already exists. "
|
||||||
@@ -375,6 +661,7 @@ class MedTrackerApp:
|
|||||||
parent=self.root,
|
parent=self.root,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
self.ui_manager.update_status("Failed to add entry", "error")
|
||||||
messagebox.showerror("Error", "Failed to add entry", parent=self.root)
|
messagebox.showerror("Error", "Failed to add entry", parent=self.root)
|
||||||
|
|
||||||
def _delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None:
|
def _delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None:
|
||||||
@@ -389,13 +676,16 @@ class MedTrackerApp:
|
|||||||
date: str = self.tree.item(item_id, "values")[0]
|
date: str = self.tree.item(item_id, "values")[0]
|
||||||
logger.debug(f"Deleting entry with date={date}")
|
logger.debug(f"Deleting entry with date={date}")
|
||||||
|
|
||||||
|
self.ui_manager.update_status("Deleting entry...", "info")
|
||||||
if self.data_manager.delete_entry(date):
|
if self.data_manager.delete_entry(date):
|
||||||
edit_win.destroy()
|
edit_win.destroy()
|
||||||
|
self.ui_manager.update_status("Entry deleted successfully!", "success")
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"Success", "Entry deleted successfully!", parent=self.root
|
"Success", "Entry deleted successfully!", parent=self.root
|
||||||
)
|
)
|
||||||
self.refresh_data_display()
|
self.refresh_data_display()
|
||||||
else:
|
else:
|
||||||
|
self.ui_manager.update_status("Failed to delete entry", "error")
|
||||||
messagebox.showerror("Error", "Failed to delete entry", parent=edit_win)
|
messagebox.showerror("Error", "Failed to delete entry", parent=edit_win)
|
||||||
|
|
||||||
def _clear_entries(self) -> None:
|
def _clear_entries(self) -> None:
|
||||||
@@ -412,37 +702,61 @@ class MedTrackerApp:
|
|||||||
"""Load data from the CSV file into the table and graph."""
|
"""Load data from the CSV file into the table and graph."""
|
||||||
logger.debug("Loading data from CSV.")
|
logger.debug("Loading data from CSV.")
|
||||||
|
|
||||||
# Clear existing data in the treeview
|
# Clear existing data in the treeview efficiently
|
||||||
for i in self.tree.get_children():
|
children = self.tree.get_children()
|
||||||
self.tree.delete(i)
|
if children:
|
||||||
|
self.tree.delete(*children)
|
||||||
|
|
||||||
# Load data from the CSV file
|
try:
|
||||||
df: pd.DataFrame = self.data_manager.load_data()
|
# Load data from the CSV file
|
||||||
|
df: pd.DataFrame = self.data_manager.load_data()
|
||||||
|
|
||||||
# Update the treeview with the data
|
# Update the treeview with the data
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
# Build display columns dynamically (exclude dose columns for table view)
|
# Build display columns dynamically
|
||||||
display_columns = ["date", "depression", "anxiety", "sleep", "appetite"]
|
# (exclude dose columns for table view)
|
||||||
|
display_columns = ["date"]
|
||||||
|
|
||||||
# Add medicine columns (without dose columns)
|
# Add pathology columns
|
||||||
for medicine_key in self.medicine_manager.get_medicine_keys():
|
for pathology_key in self.pathology_manager.get_pathology_keys():
|
||||||
display_columns.append(medicine_key)
|
display_columns.append(pathology_key)
|
||||||
|
|
||||||
display_columns.append("note")
|
# Add medicine columns (without dose columns)
|
||||||
|
for medicine_key in self.medicine_manager.get_medicine_keys():
|
||||||
|
display_columns.append(medicine_key)
|
||||||
|
|
||||||
# Filter to only the columns we want to display
|
display_columns.append("note")
|
||||||
if all(col in df.columns for col in display_columns):
|
|
||||||
display_df = df[display_columns]
|
# Filter to only the columns we want to display
|
||||||
|
if all(col in df.columns for col in display_columns):
|
||||||
|
display_df = df[display_columns]
|
||||||
|
else:
|
||||||
|
# Fallback - just use all columns
|
||||||
|
display_df = df
|
||||||
|
|
||||||
|
# Batch insert for better performance with alternating row colors
|
||||||
|
for index, row in display_df.iterrows():
|
||||||
|
# Add alternating row tags for better visibility
|
||||||
|
tag = "evenrow" if index % 2 == 0 else "oddrow"
|
||||||
|
self.tree.insert(
|
||||||
|
parent="", index="end", values=list(row), tags=(tag,)
|
||||||
|
)
|
||||||
|
logger.debug(f"Loaded {len(display_df)} entries into treeview.")
|
||||||
|
|
||||||
|
# Update the graph
|
||||||
|
self.graph_manager.update_graph(df)
|
||||||
|
|
||||||
|
# Update status bar with file info
|
||||||
|
entry_count = len(df) if not df.empty else 0
|
||||||
|
self.ui_manager.update_file_info(self.filename, entry_count)
|
||||||
|
if entry_count == 0:
|
||||||
|
self.ui_manager.update_status("No data to display", "warning")
|
||||||
else:
|
else:
|
||||||
# Fallback - just use all columns
|
self.ui_manager.update_status("Data loaded successfully", "success")
|
||||||
display_df = df
|
|
||||||
|
|
||||||
for _index, row in display_df.iterrows():
|
except Exception as e:
|
||||||
self.tree.insert(parent="", index="end", values=list(row))
|
logger.error(f"Error loading data: {e}")
|
||||||
logger.debug(f"Loaded {len(display_df)} entries into treeview.")
|
self.ui_manager.update_status(f"Error loading data: {str(e)}", "error")
|
||||||
|
|
||||||
# Update the graph
|
|
||||||
self.graph_manager.update_graph(df)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
"""Settings window for TheChart application."""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox, ttk
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsWindow:
|
||||||
|
"""Settings window for application preferences."""
|
||||||
|
|
||||||
|
def __init__(self, parent: tk.Tk, theme_manager, ui_manager) -> None:
|
||||||
|
self.parent = parent
|
||||||
|
self.theme_manager = theme_manager
|
||||||
|
self.ui_manager = ui_manager
|
||||||
|
|
||||||
|
# Create window
|
||||||
|
self.window = tk.Toplevel(parent)
|
||||||
|
self.window.title("Settings - TheChart")
|
||||||
|
self.window.geometry("500x400")
|
||||||
|
self.window.resizable(False, False)
|
||||||
|
|
||||||
|
# Make window modal
|
||||||
|
self.window.transient(parent)
|
||||||
|
self.window.grab_set()
|
||||||
|
|
||||||
|
# Center the window
|
||||||
|
self._center_window()
|
||||||
|
|
||||||
|
# Setup UI
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
# Set initial values
|
||||||
|
self._load_current_settings()
|
||||||
|
|
||||||
|
def _center_window(self) -> None:
|
||||||
|
"""Center the settings window on the parent."""
|
||||||
|
self.window.update_idletasks()
|
||||||
|
|
||||||
|
# Get window dimensions
|
||||||
|
window_width = self.window.winfo_reqwidth()
|
||||||
|
window_height = self.window.winfo_reqheight()
|
||||||
|
|
||||||
|
# Get parent window position and size
|
||||||
|
parent_x = self.parent.winfo_x()
|
||||||
|
parent_y = self.parent.winfo_y()
|
||||||
|
parent_width = self.parent.winfo_width()
|
||||||
|
parent_height = self.parent.winfo_height()
|
||||||
|
|
||||||
|
# Calculate centered position
|
||||||
|
x = parent_x + (parent_width // 2) - (window_width // 2)
|
||||||
|
y = parent_y + (parent_height // 2) - (window_height // 2)
|
||||||
|
|
||||||
|
self.window.geometry(f"{window_width}x{window_height}+{x}+{y}")
|
||||||
|
|
||||||
|
def _setup_ui(self) -> None:
|
||||||
|
"""Setup the settings UI."""
|
||||||
|
# Main container
|
||||||
|
main_frame = ttk.Frame(self.window, padding="20", style="Card.TFrame")
|
||||||
|
main_frame.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title_label = ttk.Label(
|
||||||
|
main_frame,
|
||||||
|
text="Application Settings",
|
||||||
|
font=("TkDefaultFont", 16, "bold"),
|
||||||
|
)
|
||||||
|
title_label.pack(pady=(0, 20))
|
||||||
|
|
||||||
|
# Create notebook for different setting categories
|
||||||
|
notebook = ttk.Notebook(main_frame, style="Modern.TNotebook")
|
||||||
|
notebook.pack(fill="both", expand=True, pady=(0, 20))
|
||||||
|
|
||||||
|
# Theme settings tab
|
||||||
|
self._create_theme_tab(notebook)
|
||||||
|
|
||||||
|
# UI settings tab
|
||||||
|
self._create_ui_tab(notebook)
|
||||||
|
|
||||||
|
# About tab
|
||||||
|
self._create_about_tab(notebook)
|
||||||
|
|
||||||
|
# Button frame
|
||||||
|
button_frame = ttk.Frame(main_frame)
|
||||||
|
button_frame.pack(fill="x", pady=(10, 0))
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="Apply",
|
||||||
|
command=self._apply_settings,
|
||||||
|
style="Action.TButton",
|
||||||
|
).pack(side="right", padx=(5, 0))
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="Cancel",
|
||||||
|
command=self._cancel,
|
||||||
|
style="Action.TButton",
|
||||||
|
).pack(side="right")
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="OK",
|
||||||
|
command=self._ok,
|
||||||
|
style="Action.TButton",
|
||||||
|
).pack(side="right", padx=(0, 5))
|
||||||
|
|
||||||
|
def _create_theme_tab(self, notebook: ttk.Notebook) -> None:
|
||||||
|
"""Create the theme settings tab."""
|
||||||
|
theme_frame = ttk.Frame(notebook, style="Card.TFrame")
|
||||||
|
notebook.add(theme_frame, text="Theme")
|
||||||
|
|
||||||
|
# Theme selection
|
||||||
|
theme_label_frame = ttk.LabelFrame(
|
||||||
|
theme_frame, text="Theme Selection", style="Card.TLabelframe"
|
||||||
|
)
|
||||||
|
theme_label_frame.pack(fill="x", padx=10, pady=10)
|
||||||
|
|
||||||
|
ttk.Label(
|
||||||
|
theme_label_frame,
|
||||||
|
text="Choose your preferred theme:",
|
||||||
|
font=("TkDefaultFont", 10),
|
||||||
|
).pack(anchor="w", padx=10, pady=(10, 5))
|
||||||
|
|
||||||
|
# Theme radio buttons
|
||||||
|
self.theme_var = tk.StringVar()
|
||||||
|
themes = self.theme_manager.get_available_themes()
|
||||||
|
|
||||||
|
theme_buttons_frame = ttk.Frame(theme_label_frame)
|
||||||
|
theme_buttons_frame.pack(fill="x", padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
# Create radio buttons in a grid
|
||||||
|
for i, theme in enumerate(themes):
|
||||||
|
row = i // 3
|
||||||
|
col = i % 3
|
||||||
|
|
||||||
|
ttk.Radiobutton(
|
||||||
|
theme_buttons_frame,
|
||||||
|
text=theme.title(),
|
||||||
|
variable=self.theme_var,
|
||||||
|
value=theme,
|
||||||
|
style="Modern.TCheckbutton",
|
||||||
|
).grid(row=row, column=col, sticky="w", padx=5, pady=2)
|
||||||
|
|
||||||
|
# Theme preview info
|
||||||
|
preview_frame = ttk.LabelFrame(
|
||||||
|
theme_frame, text="Theme Preview", style="Card.TLabelframe"
|
||||||
|
)
|
||||||
|
preview_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
preview_text = tk.Text(
|
||||||
|
preview_frame,
|
||||||
|
height=6,
|
||||||
|
wrap="word",
|
||||||
|
font=("TkDefaultFont", 9),
|
||||||
|
state="disabled",
|
||||||
|
)
|
||||||
|
preview_text.pack(fill="both", expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
|
# Theme change callback
|
||||||
|
def on_theme_change():
|
||||||
|
selected_theme = self.theme_var.get()
|
||||||
|
preview_text.config(state="normal")
|
||||||
|
preview_text.delete("1.0", "end")
|
||||||
|
preview_text.insert(
|
||||||
|
"1.0",
|
||||||
|
f"Selected theme: {selected_theme.title()}\\n\\n"
|
||||||
|
"Theme changes will be applied when you click 'Apply' or 'OK'. "
|
||||||
|
"The new theme will affect all windows and UI elements "
|
||||||
|
"in the application.",
|
||||||
|
)
|
||||||
|
preview_text.config(state="disabled")
|
||||||
|
|
||||||
|
self.theme_var.trace("w", lambda *args: on_theme_change())
|
||||||
|
|
||||||
|
def _create_ui_tab(self, notebook: ttk.Notebook) -> None:
|
||||||
|
"""Create the UI settings tab."""
|
||||||
|
ui_frame = ttk.Frame(notebook, style="Card.TFrame")
|
||||||
|
notebook.add(ui_frame, text="Interface")
|
||||||
|
|
||||||
|
# Font settings
|
||||||
|
font_frame = ttk.LabelFrame(
|
||||||
|
ui_frame, text="Font Settings", style="Card.TLabelframe"
|
||||||
|
)
|
||||||
|
font_frame.pack(fill="x", padx=10, pady=10)
|
||||||
|
|
||||||
|
ttk.Label(
|
||||||
|
font_frame,
|
||||||
|
text="Font size adjustments (requires restart):",
|
||||||
|
font=("TkDefaultFont", 10),
|
||||||
|
).pack(anchor="w", padx=10, pady=10)
|
||||||
|
|
||||||
|
# Font size scale
|
||||||
|
self.font_scale_var = tk.DoubleVar(value=1.0)
|
||||||
|
font_scale = ttk.Scale(
|
||||||
|
font_frame,
|
||||||
|
from_=0.8,
|
||||||
|
to=1.5,
|
||||||
|
variable=self.font_scale_var,
|
||||||
|
orient="horizontal",
|
||||||
|
style="Modern.Horizontal.TScale",
|
||||||
|
)
|
||||||
|
font_scale.pack(fill="x", padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
# Scale labels
|
||||||
|
scale_labels_frame = ttk.Frame(font_frame)
|
||||||
|
scale_labels_frame.pack(fill="x", padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
ttk.Label(scale_labels_frame, text="Small").pack(side="left")
|
||||||
|
ttk.Label(scale_labels_frame, text="Large").pack(side="right")
|
||||||
|
ttk.Label(scale_labels_frame, text="Normal").pack()
|
||||||
|
|
||||||
|
# Window settings
|
||||||
|
window_frame = ttk.LabelFrame(
|
||||||
|
ui_frame, text="Window Settings", style="Card.TLabelframe"
|
||||||
|
)
|
||||||
|
window_frame.pack(fill="x", padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
# Remember window size
|
||||||
|
self.remember_size_var = tk.BooleanVar(value=True)
|
||||||
|
ttk.Checkbutton(
|
||||||
|
window_frame,
|
||||||
|
text="Remember window size and position",
|
||||||
|
variable=self.remember_size_var,
|
||||||
|
style="Modern.TCheckbutton",
|
||||||
|
).pack(anchor="w", padx=10, pady=10)
|
||||||
|
|
||||||
|
# Always on top
|
||||||
|
self.always_on_top_var = tk.BooleanVar(value=False)
|
||||||
|
ttk.Checkbutton(
|
||||||
|
window_frame,
|
||||||
|
text="Keep window always on top",
|
||||||
|
variable=self.always_on_top_var,
|
||||||
|
style="Modern.TCheckbutton",
|
||||||
|
).pack(anchor="w", padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
def _create_about_tab(self, notebook: ttk.Notebook) -> None:
|
||||||
|
"""Create the about tab."""
|
||||||
|
about_frame = ttk.Frame(notebook, style="Card.TFrame")
|
||||||
|
notebook.add(about_frame, text="About")
|
||||||
|
|
||||||
|
# App info
|
||||||
|
info_frame = ttk.LabelFrame(
|
||||||
|
about_frame, text="Application Information", style="Card.TLabelframe"
|
||||||
|
)
|
||||||
|
info_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
|
about_text = tk.Text(
|
||||||
|
info_frame,
|
||||||
|
wrap="word",
|
||||||
|
font=("TkDefaultFont", 10),
|
||||||
|
state="disabled",
|
||||||
|
bg=self.theme_manager.get_theme_colors()["bg"],
|
||||||
|
fg=self.theme_manager.get_theme_colors()["fg"],
|
||||||
|
)
|
||||||
|
about_text.pack(fill="both", expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
|
about_content = """TheChart - Medication Tracker
|
||||||
|
|
||||||
|
Version: 1.9.5
|
||||||
|
Built with: Python, Tkinter, ttkthemes
|
||||||
|
|
||||||
|
Features:
|
||||||
|
• Modern themed interface with multiple themes
|
||||||
|
• Medication and pathology tracking
|
||||||
|
• Visual graphs and charts
|
||||||
|
• Data export capabilities
|
||||||
|
• Keyboard shortcuts for efficiency
|
||||||
|
• Customizable UI settings
|
||||||
|
|
||||||
|
This application helps you track your daily medications and health
|
||||||
|
conditions with an intuitive, modern interface.
|
||||||
|
|
||||||
|
Enhanced with ttkthemes for better visual appeal and user experience."""
|
||||||
|
|
||||||
|
about_text.config(state="normal")
|
||||||
|
about_text.insert("1.0", about_content)
|
||||||
|
about_text.config(state="disabled")
|
||||||
|
|
||||||
|
def _load_current_settings(self) -> None:
|
||||||
|
"""Load current application settings."""
|
||||||
|
# Set current theme
|
||||||
|
current_theme = self.theme_manager.get_current_theme()
|
||||||
|
self.theme_var.set(current_theme)
|
||||||
|
|
||||||
|
# Trigger theme change to update preview
|
||||||
|
if hasattr(self, "theme_var"):
|
||||||
|
self.theme_var.set(current_theme)
|
||||||
|
|
||||||
|
def _apply_settings(self) -> None:
|
||||||
|
"""Apply the selected settings."""
|
||||||
|
# Apply theme if changed
|
||||||
|
selected_theme = self.theme_var.get()
|
||||||
|
current_theme = self.theme_manager.get_current_theme()
|
||||||
|
|
||||||
|
if selected_theme != current_theme:
|
||||||
|
if self.theme_manager.apply_theme(selected_theme):
|
||||||
|
self.ui_manager.update_status(
|
||||||
|
f"Theme changed to: {selected_theme.title()}", "info"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Error",
|
||||||
|
f"Failed to apply theme: {selected_theme}",
|
||||||
|
parent=self.window,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Apply other settings (font size, window settings, etc.)
|
||||||
|
# These would typically be saved to a config file
|
||||||
|
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Settings Applied",
|
||||||
|
"Settings have been applied successfully!",
|
||||||
|
parent=self.window,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _ok(self) -> None:
|
||||||
|
"""Apply settings and close window."""
|
||||||
|
self._apply_settings()
|
||||||
|
self.window.destroy()
|
||||||
|
|
||||||
|
def _cancel(self) -> None:
|
||||||
|
"""Close window without applying settings."""
|
||||||
|
self.window.destroy()
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
"""Theme manager for the application using ttkthemes."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
|
||||||
|
from ttkthemes import ThemedStyle
|
||||||
|
|
||||||
|
|
||||||
|
class ThemeManager:
|
||||||
|
"""Manages application themes and styling."""
|
||||||
|
|
||||||
|
def __init__(self, root: tk.Tk, logger: logging.Logger) -> None:
|
||||||
|
self.root = root
|
||||||
|
self.logger = logger
|
||||||
|
self.style: ThemedStyle | None = None
|
||||||
|
self.current_theme: str = "arc" # Default theme
|
||||||
|
|
||||||
|
# Available themes - these are some of the best looking ones
|
||||||
|
self.available_themes = [
|
||||||
|
"arc",
|
||||||
|
"equilux",
|
||||||
|
"adapta",
|
||||||
|
"yaru",
|
||||||
|
"ubuntu",
|
||||||
|
"plastik",
|
||||||
|
"breeze",
|
||||||
|
"elegance",
|
||||||
|
]
|
||||||
|
|
||||||
|
self.initialize_theme()
|
||||||
|
|
||||||
|
def initialize_theme(self) -> None:
|
||||||
|
"""Initialize the themed style."""
|
||||||
|
try:
|
||||||
|
self.style = ThemedStyle(self.root)
|
||||||
|
self.apply_theme(self.current_theme)
|
||||||
|
self._configure_custom_styles()
|
||||||
|
self.logger.info(
|
||||||
|
f"Theme manager initialized with theme: {self.current_theme}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to initialize theme manager: {e}")
|
||||||
|
# Fallback to default ttk styling
|
||||||
|
self.style = ttk.Style()
|
||||||
|
|
||||||
|
def apply_theme(self, theme_name: str) -> bool:
|
||||||
|
"""Apply a specific theme."""
|
||||||
|
try:
|
||||||
|
if self.style and theme_name in self.get_available_themes():
|
||||||
|
self.style.set_theme(theme_name)
|
||||||
|
self.current_theme = theme_name
|
||||||
|
self._configure_custom_styles()
|
||||||
|
self.logger.info(f"Applied theme: {theme_name}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"Theme '{theme_name}' not available")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to apply theme '{theme_name}': {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_available_themes(self) -> list[str]:
|
||||||
|
"""Get list of available themes."""
|
||||||
|
if self.style:
|
||||||
|
try:
|
||||||
|
# Get all available themes from ttkthemes
|
||||||
|
all_themes = self.style.theme_names()
|
||||||
|
# Filter to only include our curated list
|
||||||
|
return [theme for theme in self.available_themes if theme in all_themes]
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to get available themes: {e}")
|
||||||
|
return self.available_themes
|
||||||
|
return self.available_themes
|
||||||
|
|
||||||
|
def get_current_theme(self) -> str:
|
||||||
|
"""Get the currently active theme."""
|
||||||
|
return self.current_theme
|
||||||
|
|
||||||
|
def _configure_custom_styles(self) -> None:
|
||||||
|
"""Configure custom styles for better appearance."""
|
||||||
|
if not self.style:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get current theme colors for consistent styling
|
||||||
|
colors = self.get_theme_colors()
|
||||||
|
|
||||||
|
# Configure frame styles with better padding and borders
|
||||||
|
self.style.configure(
|
||||||
|
"Card.TFrame",
|
||||||
|
relief="flat",
|
||||||
|
borderwidth=0,
|
||||||
|
background=colors["bg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure label frame styles with modern appearance
|
||||||
|
self.style.configure(
|
||||||
|
"Card.TLabelframe",
|
||||||
|
relief="solid",
|
||||||
|
borderwidth=1,
|
||||||
|
background=colors["bg"],
|
||||||
|
foreground=colors["fg"],
|
||||||
|
padding=(10, 5, 10, 10),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.style.configure(
|
||||||
|
"Card.TLabelframe.Label",
|
||||||
|
background=colors["bg"],
|
||||||
|
foreground=colors["fg"],
|
||||||
|
font=("TkDefaultFont", 10, "bold"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure button styles for better appearance
|
||||||
|
self.style.configure(
|
||||||
|
"Action.TButton",
|
||||||
|
padding=(15, 8),
|
||||||
|
font=("TkDefaultFont", 9, "normal"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure entry styles with modern look
|
||||||
|
self.style.configure(
|
||||||
|
"Modern.TEntry",
|
||||||
|
padding=(8, 5),
|
||||||
|
borderwidth=1,
|
||||||
|
relief="solid",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure scale styles for pathology inputs
|
||||||
|
self.style.configure(
|
||||||
|
"Modern.Horizontal.TScale",
|
||||||
|
borderwidth=0,
|
||||||
|
background=colors["bg"],
|
||||||
|
troughcolor="#e0e0e0",
|
||||||
|
lightcolor=colors["select_bg"],
|
||||||
|
darkcolor=colors["select_bg"],
|
||||||
|
focuscolor=colors["select_bg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure treeview for better data display
|
||||||
|
self.style.configure(
|
||||||
|
"Modern.Treeview",
|
||||||
|
rowheight=28,
|
||||||
|
borderwidth=1,
|
||||||
|
relief="solid",
|
||||||
|
background=colors["bg"],
|
||||||
|
foreground=colors["fg"],
|
||||||
|
fieldbackground=colors["bg"],
|
||||||
|
selectbackground=colors["select_bg"],
|
||||||
|
selectforeground=colors["select_fg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.style.configure(
|
||||||
|
"Modern.Treeview.Heading",
|
||||||
|
padding=(8, 6),
|
||||||
|
relief="flat",
|
||||||
|
borderwidth=1,
|
||||||
|
background=colors["select_bg"],
|
||||||
|
foreground=colors["select_fg"],
|
||||||
|
font=("TkDefaultFont", 9, "bold"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure comprehensive row selection colors for better visibility
|
||||||
|
self.style.map(
|
||||||
|
"Modern.Treeview",
|
||||||
|
background=[
|
||||||
|
("selected", colors["select_bg"]),
|
||||||
|
("active", colors["select_bg"]),
|
||||||
|
("focus", colors["select_bg"]),
|
||||||
|
("", colors["bg"]),
|
||||||
|
],
|
||||||
|
foreground=[
|
||||||
|
("selected", colors["select_fg"]),
|
||||||
|
("active", colors["select_fg"]),
|
||||||
|
("focus", colors["select_fg"]),
|
||||||
|
("", colors["fg"]),
|
||||||
|
],
|
||||||
|
selectbackground=[
|
||||||
|
("focus", colors["select_bg"]),
|
||||||
|
("", colors["select_bg"]),
|
||||||
|
],
|
||||||
|
selectforeground=[
|
||||||
|
("focus", colors["select_fg"]),
|
||||||
|
("", colors["select_fg"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure notebook tabs with modern styling
|
||||||
|
self.style.configure(
|
||||||
|
"Modern.TNotebook.Tab",
|
||||||
|
padding=(15, 8),
|
||||||
|
borderwidth=1,
|
||||||
|
relief="flat",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.style.map(
|
||||||
|
"Modern.TNotebook.Tab",
|
||||||
|
background=[("selected", colors["select_bg"])],
|
||||||
|
foreground=[("selected", colors["select_fg"])],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure checkbutton for medicine selection
|
||||||
|
self.style.configure(
|
||||||
|
"Modern.TCheckbutton",
|
||||||
|
padding=(8, 4),
|
||||||
|
background=colors["bg"],
|
||||||
|
foreground=colors["fg"],
|
||||||
|
focuscolor=colors["select_bg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.debug("Enhanced custom styles configured")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to configure custom styles: {e}")
|
||||||
|
|
||||||
|
def configure_widget_style(self, widget: tk.Widget, style_name: str) -> None:
|
||||||
|
"""Apply a specific style to a widget."""
|
||||||
|
try:
|
||||||
|
if hasattr(widget, "configure") and self.style:
|
||||||
|
widget.configure(style=style_name)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to configure widget style '{style_name}': {e}")
|
||||||
|
|
||||||
|
def get_theme_colors(self) -> dict[str, str]:
|
||||||
|
"""Get current theme colors for custom widgets."""
|
||||||
|
if not self.style:
|
||||||
|
return {
|
||||||
|
"bg": "#ffffff",
|
||||||
|
"fg": "#000000",
|
||||||
|
"select_bg": "#3584e4",
|
||||||
|
"select_fg": "#ffffff",
|
||||||
|
"alt_bg": "#f5f5f5",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get colors from current theme
|
||||||
|
bg = self.style.lookup("TFrame", "background") or "#ffffff"
|
||||||
|
fg = self.style.lookup("TLabel", "foreground") or "#000000"
|
||||||
|
|
||||||
|
# Try to get better selection colors from different widget states
|
||||||
|
select_bg = (
|
||||||
|
self.style.lookup("TButton", "background", ["pressed"])
|
||||||
|
or self.style.lookup("TButton", "background", ["active"])
|
||||||
|
or self.style.lookup("Treeview", "selectbackground")
|
||||||
|
or "#0078d4" # Modern blue fallback
|
||||||
|
)
|
||||||
|
select_fg = (
|
||||||
|
self.style.lookup("TButton", "foreground", ["pressed"])
|
||||||
|
or self.style.lookup("TButton", "foreground", ["active"])
|
||||||
|
or self.style.lookup("Treeview", "selectforeground")
|
||||||
|
or "#ffffff" # White fallback
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure contrast - if selection colors are too similar to background,
|
||||||
|
# use fallbacks
|
||||||
|
if select_bg == bg or select_bg.lower() == bg.lower():
|
||||||
|
select_bg = "#0078d4" if bg != "#0078d4" else "#0066cc"
|
||||||
|
|
||||||
|
if select_fg == fg or select_fg.lower() == fg.lower():
|
||||||
|
select_fg = "#ffffff" if fg != "#ffffff" else "#000000"
|
||||||
|
|
||||||
|
# Calculate alternating row color
|
||||||
|
if bg.startswith("#"):
|
||||||
|
try:
|
||||||
|
rgb = tuple(int(bg[i : i + 2], 16) for i in (1, 3, 5))
|
||||||
|
if sum(rgb) > 384: # Light theme
|
||||||
|
alt_bg = (
|
||||||
|
f"#{max(0, rgb[0] - 10):02x}"
|
||||||
|
f"{max(0, rgb[1] - 10):02x}"
|
||||||
|
f"{max(0, rgb[2] - 10):02x}"
|
||||||
|
)
|
||||||
|
else: # Dark theme
|
||||||
|
alt_bg = (
|
||||||
|
f"#{min(255, rgb[0] + 10):02x}"
|
||||||
|
f"{min(255, rgb[1] + 10):02x}"
|
||||||
|
f"{min(255, rgb[2] + 10):02x}"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
alt_bg = "#f5f5f5"
|
||||||
|
else:
|
||||||
|
alt_bg = "#f5f5f5"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"bg": bg,
|
||||||
|
"fg": fg,
|
||||||
|
"select_bg": select_bg,
|
||||||
|
"select_fg": select_fg,
|
||||||
|
"alt_bg": alt_bg, # Add alternating background color
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to get theme colors: {e}")
|
||||||
|
return {
|
||||||
|
"bg": "#ffffff",
|
||||||
|
"fg": "#000000",
|
||||||
|
"select_bg": "#3584e4",
|
||||||
|
"select_fg": "#ffffff",
|
||||||
|
"alt_bg": "#f5f5f5",
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
"""Tooltip system for enhanced user experience."""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
|
||||||
|
class ToolTip:
|
||||||
|
"""Create a tooltip for a given widget."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
widget: tk.Widget,
|
||||||
|
text: str,
|
||||||
|
delay: int = 500,
|
||||||
|
wrap_length: int = 250,
|
||||||
|
) -> None:
|
||||||
|
self.widget = widget
|
||||||
|
self.text = text
|
||||||
|
self.delay = delay
|
||||||
|
self.wrap_length = wrap_length
|
||||||
|
self.tooltip: tk.Toplevel | None = None
|
||||||
|
self.id_after: str | None = None
|
||||||
|
|
||||||
|
# Bind events
|
||||||
|
self.widget.bind("<Enter>", self._on_enter)
|
||||||
|
self.widget.bind("<Leave>", self._on_leave)
|
||||||
|
self.widget.bind("<ButtonPress>", self._on_leave)
|
||||||
|
|
||||||
|
def _on_enter(self, event: tk.Event | None = None) -> None:
|
||||||
|
"""Mouse entered widget - schedule tooltip."""
|
||||||
|
self._cancel_scheduled()
|
||||||
|
self.id_after = self.widget.after(self.delay, self._show_tooltip)
|
||||||
|
|
||||||
|
def _on_leave(self, event: tk.Event | None = None) -> None:
|
||||||
|
"""Mouse left widget - hide tooltip."""
|
||||||
|
self._cancel_scheduled()
|
||||||
|
self._hide_tooltip()
|
||||||
|
|
||||||
|
def _cancel_scheduled(self) -> None:
|
||||||
|
"""Cancel any scheduled tooltip."""
|
||||||
|
if self.id_after:
|
||||||
|
self.widget.after_cancel(self.id_after)
|
||||||
|
self.id_after = None
|
||||||
|
|
||||||
|
def _show_tooltip(self) -> None:
|
||||||
|
"""Display the tooltip."""
|
||||||
|
if self.tooltip:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get widget position
|
||||||
|
x = self.widget.winfo_rootx() + 25
|
||||||
|
y = self.widget.winfo_rooty() + 25
|
||||||
|
|
||||||
|
# Create tooltip window
|
||||||
|
self.tooltip = tk.Toplevel(self.widget)
|
||||||
|
self.tooltip.wm_overrideredirect(True)
|
||||||
|
self.tooltip.wm_geometry(f"+{x}+{y}")
|
||||||
|
|
||||||
|
# Create tooltip content
|
||||||
|
label = tk.Label(
|
||||||
|
self.tooltip,
|
||||||
|
text=self.text,
|
||||||
|
justify="left",
|
||||||
|
background="#ffffe0",
|
||||||
|
foreground="#000000",
|
||||||
|
relief="solid",
|
||||||
|
borderwidth=1,
|
||||||
|
font=("TkDefaultFont", "9", "normal"),
|
||||||
|
wraplength=self.wrap_length,
|
||||||
|
padx=8,
|
||||||
|
pady=6,
|
||||||
|
)
|
||||||
|
label.pack()
|
||||||
|
|
||||||
|
# Make sure tooltip appears above other windows
|
||||||
|
self.tooltip.lift()
|
||||||
|
|
||||||
|
def _hide_tooltip(self) -> None:
|
||||||
|
"""Hide the tooltip."""
|
||||||
|
if self.tooltip:
|
||||||
|
self.tooltip.destroy()
|
||||||
|
self.tooltip = None
|
||||||
|
|
||||||
|
def update_text(self, new_text: str) -> None:
|
||||||
|
"""Update the tooltip text."""
|
||||||
|
self.text = new_text
|
||||||
|
|
||||||
|
|
||||||
|
class TooltipManager:
|
||||||
|
"""Manages tooltips for UI elements."""
|
||||||
|
|
||||||
|
def __init__(self, theme_manager) -> None:
|
||||||
|
self.theme_manager = theme_manager
|
||||||
|
self.tooltips: list[ToolTip] = []
|
||||||
|
|
||||||
|
def add_tooltip(
|
||||||
|
self,
|
||||||
|
widget: tk.Widget,
|
||||||
|
text: str,
|
||||||
|
delay: int = 500,
|
||||||
|
wrap_length: int = 250,
|
||||||
|
) -> ToolTip:
|
||||||
|
"""Add a tooltip to a widget."""
|
||||||
|
tooltip = ToolTip(widget, text, delay, wrap_length)
|
||||||
|
self.tooltips.append(tooltip)
|
||||||
|
return tooltip
|
||||||
|
|
||||||
|
def add_scale_tooltip(self, scale_widget: tk.Widget, pathology_name: str) -> None:
|
||||||
|
"""Add a specialized tooltip for pathology scales."""
|
||||||
|
text = (
|
||||||
|
f"Adjust your {pathology_name} level\\n"
|
||||||
|
"• Drag the slider to set your current level\\n"
|
||||||
|
"• Higher values typically indicate worse symptoms\\n"
|
||||||
|
"• Use the full range for accurate tracking"
|
||||||
|
)
|
||||||
|
self.add_tooltip(scale_widget, text, delay=800)
|
||||||
|
|
||||||
|
def add_medicine_tooltip(self, widget: tk.Widget, medicine_name: str) -> None:
|
||||||
|
"""Add a specialized tooltip for medicine checkboxes."""
|
||||||
|
text = (
|
||||||
|
f"Mark if you took {medicine_name} today\\n"
|
||||||
|
"• Check the box when you've taken this medication\\n"
|
||||||
|
"• This helps track your medication adherence\\n"
|
||||||
|
"• You can add dose details when editing entries"
|
||||||
|
)
|
||||||
|
self.add_tooltip(widget, text, delay=600)
|
||||||
|
|
||||||
|
def add_button_tooltip(self, widget: tk.Widget, action: str) -> None:
|
||||||
|
"""Add a tooltip for action buttons."""
|
||||||
|
tooltips_map = {
|
||||||
|
"save": (
|
||||||
|
"Save your current entry (Ctrl+S)\\nThis will add a new daily record"
|
||||||
|
),
|
||||||
|
"export": (
|
||||||
|
"Export your data to various formats\\n"
|
||||||
|
"Supports CSV, PDF, and image exports"
|
||||||
|
),
|
||||||
|
"refresh": (
|
||||||
|
"Reload data from file (F5)\\nUpdates the display with latest changes"
|
||||||
|
),
|
||||||
|
"settings": (
|
||||||
|
"Open application settings (F2)\\nCustomize themes and preferences"
|
||||||
|
),
|
||||||
|
"quit": (
|
||||||
|
"Exit the application (Ctrl+Q)\\nYour data will be automatically saved"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
text = tooltips_map.get(action, f"Perform {action} action")
|
||||||
|
self.add_tooltip(widget, text, delay=400)
|
||||||
|
|
||||||
|
def add_menu_tooltip(self, widget: tk.Widget, menu_type: str) -> None:
|
||||||
|
"""Add tooltips for menu items."""
|
||||||
|
tooltips_map = {
|
||||||
|
"theme": (
|
||||||
|
"Quick theme selection\\nClick to instantly change the app's appearance"
|
||||||
|
),
|
||||||
|
"file": "File operations\\nExport data and manage files",
|
||||||
|
"tools": ("Data management tools\\nConfigure medicines and pathologies"),
|
||||||
|
"help": ("Get help and information\\nKeyboard shortcuts and about dialog"),
|
||||||
|
}
|
||||||
|
|
||||||
|
text = tooltips_map.get(menu_type, "Menu options")
|
||||||
|
self.add_tooltip(widget, text, delay=600)
|
||||||
+231
-359
@@ -11,6 +11,7 @@ from PIL import Image, ImageTk
|
|||||||
|
|
||||||
from medicine_manager import MedicineManager
|
from medicine_manager import MedicineManager
|
||||||
from pathology_manager import PathologyManager
|
from pathology_manager import PathologyManager
|
||||||
|
from tooltip_system import TooltipManager
|
||||||
|
|
||||||
|
|
||||||
class UIManager:
|
class UIManager:
|
||||||
@@ -22,11 +23,21 @@ class UIManager:
|
|||||||
logger: logging.Logger,
|
logger: logging.Logger,
|
||||||
medicine_manager: MedicineManager,
|
medicine_manager: MedicineManager,
|
||||||
pathology_manager: PathologyManager,
|
pathology_manager: PathologyManager,
|
||||||
|
theme_manager, # Import would create circular dependency
|
||||||
) -> None:
|
) -> None:
|
||||||
self.root: tk.Tk = root
|
self.root: tk.Tk = root
|
||||||
self.logger: logging.Logger = logger
|
self.logger: logging.Logger = logger
|
||||||
self.medicine_manager = medicine_manager
|
self.medicine_manager = medicine_manager
|
||||||
self.pathology_manager = pathology_manager
|
self.pathology_manager = pathology_manager
|
||||||
|
self.theme_manager = theme_manager
|
||||||
|
|
||||||
|
# Status bar attributes
|
||||||
|
self.status_bar: tk.Frame | None = None
|
||||||
|
self.status_label: tk.Label | None = None
|
||||||
|
self.file_info_label: tk.Label | None = None
|
||||||
|
|
||||||
|
# Initialize tooltip manager
|
||||||
|
self.tooltip_manager = TooltipManager(theme_manager)
|
||||||
|
|
||||||
def setup_application_icon(self, img_path: str) -> bool:
|
def setup_application_icon(self, img_path: str) -> bool:
|
||||||
"""Set up the application icon."""
|
"""Set up the application icon."""
|
||||||
@@ -65,13 +76,20 @@ class UIManager:
|
|||||||
def create_input_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
|
def create_input_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
|
||||||
"""Create and configure the input frame with all widgets."""
|
"""Create and configure the input frame with all widgets."""
|
||||||
# Create main container for the scrollable input frame
|
# Create main container for the scrollable input frame
|
||||||
main_container = ttk.LabelFrame(parent_frame, text="New Entry")
|
main_container = ttk.LabelFrame(
|
||||||
|
parent_frame, text="New Entry", style="Card.TLabelframe"
|
||||||
|
)
|
||||||
main_container.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
|
main_container.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
|
||||||
main_container.grid_rowconfigure(0, weight=1)
|
main_container.grid_rowconfigure(0, weight=1)
|
||||||
main_container.grid_columnconfigure(0, weight=1)
|
main_container.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
# Create canvas and scrollbar for scrolling
|
# Create canvas and scrollbar for scrolling
|
||||||
canvas = tk.Canvas(main_container, highlightthickness=0)
|
theme_colors = self.theme_manager.get_theme_colors()
|
||||||
|
canvas = tk.Canvas(
|
||||||
|
main_container,
|
||||||
|
highlightthickness=0,
|
||||||
|
bg=theme_colors["bg"],
|
||||||
|
)
|
||||||
scrollbar = ttk.Scrollbar(
|
scrollbar = ttk.Scrollbar(
|
||||||
main_container, orient="vertical", command=canvas.yview
|
main_container, orient="vertical", command=canvas.yview
|
||||||
)
|
)
|
||||||
@@ -159,7 +177,9 @@ class UIManager:
|
|||||||
ttk.Label(input_frame, text="Treatment:").grid(
|
ttk.Label(input_frame, text="Treatment:").grid(
|
||||||
row=medicine_row, column=0, sticky="w", padx=5, pady=2
|
row=medicine_row, column=0, sticky="w", padx=5, pady=2
|
||||||
)
|
)
|
||||||
medicine_frame = ttk.LabelFrame(input_frame, text="Medicine")
|
medicine_frame = ttk.LabelFrame(
|
||||||
|
input_frame, text="Medicine", style="Card.TLabelframe"
|
||||||
|
)
|
||||||
medicine_frame.grid(row=medicine_row, column=1, padx=0, pady=10, sticky="nsew")
|
medicine_frame.grid(row=medicine_row, column=1, padx=0, pady=10, sticky="nsew")
|
||||||
medicine_frame.grid_columnconfigure(0, weight=1)
|
medicine_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
@@ -173,11 +193,19 @@ class UIManager:
|
|||||||
text = f"{medicine.display_name} {medicine.dosage_info}"
|
text = f"{medicine.display_name} {medicine.dosage_info}"
|
||||||
medicine_vars[medicine_key] = (var, text)
|
medicine_vars[medicine_key] = (var, text)
|
||||||
|
|
||||||
for idx, (_med_name, (var, text)) in enumerate(medicine_vars.items()):
|
for idx, (med_key, (var, text)) in enumerate(medicine_vars.items()):
|
||||||
# Just checkbox for medicine taken
|
# Just checkbox for medicine taken
|
||||||
ttk.Checkbutton(medicine_frame, text=text, variable=var).grid(
|
checkbox = ttk.Checkbutton(
|
||||||
row=idx, column=0, sticky="w", padx=5, pady=2
|
medicine_frame, text=text, variable=var, style="Modern.TCheckbutton"
|
||||||
)
|
)
|
||||||
|
checkbox.grid(row=idx, column=0, sticky="w", padx=5, pady=2)
|
||||||
|
|
||||||
|
# Add tooltip for medicine checkbox
|
||||||
|
medicine = self.medicine_manager.get_medicine(med_key)
|
||||||
|
if medicine:
|
||||||
|
self.tooltip_manager.add_medicine_tooltip(
|
||||||
|
checkbox, medicine.display_name
|
||||||
|
)
|
||||||
|
|
||||||
# Note and Date fields - adjust row numbers
|
# Note and Date fields - adjust row numbers
|
||||||
note_row = medicine_row + 1
|
note_row = medicine_row + 1
|
||||||
@@ -189,16 +217,19 @@ class UIManager:
|
|||||||
ttk.Label(input_frame, text="Note:").grid(
|
ttk.Label(input_frame, text="Note:").grid(
|
||||||
row=note_row, column=0, sticky="w", padx=5, pady=2
|
row=note_row, column=0, sticky="w", padx=5, pady=2
|
||||||
)
|
)
|
||||||
ttk.Entry(input_frame, textvariable=note_var).grid(
|
ttk.Entry(input_frame, textvariable=note_var, style="Modern.TEntry").grid(
|
||||||
row=note_row, column=1, sticky="ew", padx=5, pady=2
|
row=note_row, column=1, sticky="ew", padx=5, pady=2
|
||||||
)
|
)
|
||||||
|
|
||||||
ttk.Label(input_frame, text="Date (mm/dd/yyyy):").grid(
|
ttk.Label(input_frame, text="Date (mm/dd/yyyy):").grid(
|
||||||
row=date_row, column=0, sticky="w", padx=5, pady=2
|
row=date_row, column=0, sticky="w", padx=5, pady=2
|
||||||
)
|
)
|
||||||
ttk.Entry(input_frame, textvariable=date_var, justify="center").grid(
|
ttk.Entry(
|
||||||
row=date_row, column=1, sticky="ew", padx=5, pady=2
|
input_frame,
|
||||||
)
|
textvariable=date_var,
|
||||||
|
justify="center",
|
||||||
|
style="Modern.TEntry",
|
||||||
|
).grid(row=date_row, column=1, sticky="ew", padx=5, pady=2)
|
||||||
|
|
||||||
# Set default date to today
|
# Set default date to today
|
||||||
date_var.set(datetime.now().strftime("%m/%d/%Y"))
|
date_var.set(datetime.now().strftime("%m/%d/%Y"))
|
||||||
@@ -220,7 +251,7 @@ class UIManager:
|
|||||||
def create_table_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
|
def create_table_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
|
||||||
"""Create and configure the table frame with a treeview."""
|
"""Create and configure the table frame with a treeview."""
|
||||||
table_frame: ttk.LabelFrame = ttk.LabelFrame(
|
table_frame: ttk.LabelFrame = ttk.LabelFrame(
|
||||||
parent_frame, text="Log (Double-click to edit)"
|
parent_frame, text="Log (Double-click to edit)", style="Card.TLabelframe"
|
||||||
)
|
)
|
||||||
table_frame.grid(row=1, column=1, padx=10, pady=10, sticky="nsew")
|
table_frame.grid(row=1, column=1, padx=10, pady=10, sticky="nsew")
|
||||||
|
|
||||||
@@ -253,7 +284,34 @@ class UIManager:
|
|||||||
col_labels.append("Note")
|
col_labels.append("Note")
|
||||||
col_settings.append(("Note", 300, "w"))
|
col_settings.append(("Note", 300, "w"))
|
||||||
|
|
||||||
tree: ttk.Treeview = ttk.Treeview(table_frame, columns=columns, show="headings")
|
tree: ttk.Treeview = ttk.Treeview(
|
||||||
|
table_frame, columns=columns, show="headings", style="Modern.Treeview"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure treeview selection behavior
|
||||||
|
tree.configure(selectmode="browse") # Single selection mode
|
||||||
|
|
||||||
|
# Configure row tags for alternating colors
|
||||||
|
theme_colors = self.theme_manager.get_theme_colors()
|
||||||
|
tree.tag_configure("evenrow", background=theme_colors["bg"])
|
||||||
|
tree.tag_configure("oddrow", background=theme_colors["alt_bg"])
|
||||||
|
|
||||||
|
# Configure selection highlighting
|
||||||
|
tree.tag_configure(
|
||||||
|
"selected",
|
||||||
|
background=theme_colors["select_bg"],
|
||||||
|
foreground=theme_colors["select_fg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bind selection events to ensure proper highlighting
|
||||||
|
def on_selection_change(event):
|
||||||
|
"""Handle treeview selection changes to ensure proper highlighting."""
|
||||||
|
selection = tree.selection()
|
||||||
|
if selection:
|
||||||
|
# Force focus to ensure selection is visible
|
||||||
|
tree.focus(selection[0])
|
||||||
|
|
||||||
|
tree.bind("<<TreeviewSelect>>", on_selection_change)
|
||||||
|
|
||||||
for col, label in zip(columns, col_labels, strict=False):
|
for col, label in zip(columns, col_labels, strict=False):
|
||||||
tree.heading(col, text=label)
|
tree.heading(col, text=label)
|
||||||
@@ -272,7 +330,9 @@ class UIManager:
|
|||||||
|
|
||||||
def create_graph_frame(self, parent_frame: ttk.Frame) -> ttk.LabelFrame:
|
def create_graph_frame(self, parent_frame: ttk.Frame) -> ttk.LabelFrame:
|
||||||
"""Create and configure the graph frame."""
|
"""Create and configure the graph frame."""
|
||||||
graph_frame: ttk.LabelFrame = ttk.LabelFrame(parent_frame, text="Evolution")
|
graph_frame: ttk.LabelFrame = ttk.LabelFrame(
|
||||||
|
parent_frame, text="Evolution", style="Card.TLabelframe"
|
||||||
|
)
|
||||||
graph_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky="nsew")
|
graph_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky="nsew")
|
||||||
return graph_frame
|
return graph_frame
|
||||||
|
|
||||||
@@ -284,19 +344,137 @@ class UIManager:
|
|||||||
button_frame.grid(row=7, column=0, columnspan=2, pady=10)
|
button_frame.grid(row=7, column=0, columnspan=2, pady=10)
|
||||||
|
|
||||||
for btn_config in buttons_config:
|
for btn_config in buttons_config:
|
||||||
ttk.Button(
|
button = ttk.Button(
|
||||||
button_frame,
|
button_frame,
|
||||||
text=btn_config["text"],
|
text=btn_config["text"],
|
||||||
command=btn_config["command"],
|
command=btn_config["command"],
|
||||||
).pack(
|
style="Action.TButton",
|
||||||
|
)
|
||||||
|
button.pack(
|
||||||
side="left",
|
side="left",
|
||||||
padx=5,
|
padx=5,
|
||||||
fill=btn_config.get("fill", None),
|
fill=btn_config.get("fill", None),
|
||||||
expand=btn_config.get("expand", False),
|
expand=btn_config.get("expand", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add tooltips based on button text
|
||||||
|
button_text = btn_config["text"].lower()
|
||||||
|
if "add" in button_text or "save" in button_text:
|
||||||
|
self.tooltip_manager.add_button_tooltip(button, "save")
|
||||||
|
elif "quit" in button_text or "exit" in button_text:
|
||||||
|
self.tooltip_manager.add_button_tooltip(button, "quit")
|
||||||
|
|
||||||
return button_frame
|
return button_frame
|
||||||
|
|
||||||
|
def create_status_bar(self, parent_frame: tk.Widget) -> tk.Frame:
|
||||||
|
"""Create and configure the status bar at the bottom of the application."""
|
||||||
|
# Get theme colors for consistent styling
|
||||||
|
theme_colors = self.theme_manager.get_theme_colors()
|
||||||
|
|
||||||
|
# Create the status bar frame
|
||||||
|
self.status_bar = tk.Frame(
|
||||||
|
parent_frame,
|
||||||
|
relief=tk.SUNKEN,
|
||||||
|
bd=1,
|
||||||
|
bg=theme_colors["bg"],
|
||||||
|
)
|
||||||
|
self.status_bar.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=2)
|
||||||
|
|
||||||
|
# Configure the parent to make the status bar stretch
|
||||||
|
parent_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# Create status message label (left side)
|
||||||
|
self.status_label = tk.Label(
|
||||||
|
self.status_bar,
|
||||||
|
text="Ready",
|
||||||
|
anchor=tk.W,
|
||||||
|
font=("TkDefaultFont", 9),
|
||||||
|
padx=10,
|
||||||
|
pady=2,
|
||||||
|
bg=theme_colors["bg"],
|
||||||
|
fg=theme_colors["fg"],
|
||||||
|
)
|
||||||
|
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||||
|
|
||||||
|
# Create file info label (right side)
|
||||||
|
self.file_info_label = tk.Label(
|
||||||
|
self.status_bar,
|
||||||
|
text="",
|
||||||
|
anchor=tk.E,
|
||||||
|
font=("TkDefaultFont", 9),
|
||||||
|
padx=10,
|
||||||
|
pady=2,
|
||||||
|
bg=theme_colors["bg"],
|
||||||
|
fg=theme_colors["fg"],
|
||||||
|
)
|
||||||
|
self.file_info_label.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
return self.status_bar
|
||||||
|
|
||||||
|
def update_status(self, message: str, message_type: str = "info") -> None:
|
||||||
|
"""
|
||||||
|
Update the status bar with a message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The message to display
|
||||||
|
message_type: Type of message ('info', 'success', 'warning', 'error')
|
||||||
|
"""
|
||||||
|
if not self.status_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Color mapping for different message types
|
||||||
|
colors = {
|
||||||
|
"info": "#000000", # Black
|
||||||
|
"success": "#28A745", # Green
|
||||||
|
"warning": "#FFC107", # Yellow/Orange
|
||||||
|
"error": "#DC3545", # Red
|
||||||
|
}
|
||||||
|
|
||||||
|
color = colors.get(message_type, "#000000")
|
||||||
|
self.status_label.config(text=message, fg=color)
|
||||||
|
|
||||||
|
# Clear the message after 5 seconds for non-info messages
|
||||||
|
if message_type != "info":
|
||||||
|
self.root.after(5000, lambda: self.update_status("Ready", "info"))
|
||||||
|
|
||||||
|
def update_file_info(self, filename: str, entry_count: int = 0) -> None:
|
||||||
|
"""
|
||||||
|
Update the file information in the status bar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Name of the current data file
|
||||||
|
entry_count: Number of entries in the file
|
||||||
|
"""
|
||||||
|
if not self.file_info_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
file_display = os.path.basename(filename) if filename else "No file"
|
||||||
|
info_text = f"{file_display}"
|
||||||
|
if entry_count > 0:
|
||||||
|
info_text += f" ({entry_count} entries)"
|
||||||
|
|
||||||
|
self.file_info_label.config(text=info_text)
|
||||||
|
|
||||||
|
def show_status_message(self, message: str, duration: int = 3000) -> None:
|
||||||
|
"""
|
||||||
|
Show a temporary status message for a specific duration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The message to display
|
||||||
|
duration: How long to show the message in milliseconds
|
||||||
|
"""
|
||||||
|
if not self.status_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
original_text = self.status_label.cget("text")
|
||||||
|
original_color = self.status_label.cget("fg")
|
||||||
|
|
||||||
|
self.status_label.config(text=message, fg="#2E86AB")
|
||||||
|
self.root.after(
|
||||||
|
duration,
|
||||||
|
lambda: self.status_label.config(text=original_text, fg=original_color),
|
||||||
|
)
|
||||||
|
|
||||||
def create_edit_window(
|
def create_edit_window(
|
||||||
self, values: tuple[str, ...], callbacks: dict[str, Callable]
|
self, values: tuple[str, ...], callbacks: dict[str, Callable]
|
||||||
) -> tk.Toplevel:
|
) -> tk.Toplevel:
|
||||||
@@ -417,8 +595,8 @@ class UIManager:
|
|||||||
# Extract note (should be the last value)
|
# Extract note (should be the last value)
|
||||||
note = values_list[-1] if len(values_list) > 0 else ""
|
note = values_list[-1] if len(values_list) > 0 else ""
|
||||||
|
|
||||||
# Create improved UI sections dynamically
|
# Create improved UI sections
|
||||||
vars_dict = self._create_edit_ui_dynamic(
|
vars_dict = self._create_edit_ui(
|
||||||
main_container,
|
main_container,
|
||||||
date,
|
date,
|
||||||
pathology_values,
|
pathology_values,
|
||||||
@@ -443,7 +621,7 @@ class UIManager:
|
|||||||
|
|
||||||
return edit_win
|
return edit_win
|
||||||
|
|
||||||
def _create_edit_ui_dynamic(
|
def _create_edit_ui(
|
||||||
self,
|
self,
|
||||||
parent: ttk.Frame,
|
parent: ttk.Frame,
|
||||||
date: str,
|
date: str,
|
||||||
@@ -500,7 +678,7 @@ class UIManager:
|
|||||||
meds_frame.grid_columnconfigure(0, weight=1)
|
meds_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
# Create medicine checkboxes dynamically
|
# Create medicine checkboxes dynamically
|
||||||
med_vars = self._create_medicine_section_dynamic(meds_frame, medicine_values)
|
med_vars = self._create_medicine_section(meds_frame, medicine_values)
|
||||||
vars_dict.update(med_vars)
|
vars_dict.update(med_vars)
|
||||||
|
|
||||||
row += 1
|
row += 1
|
||||||
@@ -510,7 +688,7 @@ class UIManager:
|
|||||||
dose_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
|
dose_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
|
||||||
dose_frame.grid_columnconfigure(0, weight=1)
|
dose_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
dose_vars = self._create_dose_tracking_dynamic(dose_frame, medicine_doses)
|
dose_vars = self._create_dose_tracking(dose_frame, medicine_doses)
|
||||||
vars_dict.update(dose_vars)
|
vars_dict.update(dose_vars)
|
||||||
|
|
||||||
row += 1
|
row += 1
|
||||||
@@ -532,6 +710,7 @@ class UIManager:
|
|||||||
)
|
)
|
||||||
note_text.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
note_text.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||||||
note_text.insert("1.0", str(note))
|
note_text.insert("1.0", str(note))
|
||||||
|
vars_dict["note_text"] = note_text # Store the widget for access during save
|
||||||
|
|
||||||
# Bind text widget to string var for easy access
|
# Bind text widget to string var for easy access
|
||||||
def update_note(*args):
|
def update_note(*args):
|
||||||
@@ -542,111 +721,6 @@ class UIManager:
|
|||||||
|
|
||||||
return vars_dict
|
return vars_dict
|
||||||
|
|
||||||
def _create_edit_ui(
|
|
||||||
self,
|
|
||||||
parent: ttk.Frame,
|
|
||||||
date: str,
|
|
||||||
dep: int,
|
|
||||||
anx: int,
|
|
||||||
slp: int,
|
|
||||||
app: int,
|
|
||||||
bup: int,
|
|
||||||
hydro: int,
|
|
||||||
gaba: int,
|
|
||||||
prop: int,
|
|
||||||
quet: int,
|
|
||||||
note: str,
|
|
||||||
dose_data: dict[str, str],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Create UI layout for edit window with organized sections."""
|
|
||||||
vars_dict = {}
|
|
||||||
row = 0
|
|
||||||
|
|
||||||
# Header with entry date
|
|
||||||
header_frame = ttk.Frame(parent)
|
|
||||||
header_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20))
|
|
||||||
header_frame.grid_columnconfigure(1, weight=1)
|
|
||||||
|
|
||||||
ttk.Label(
|
|
||||||
header_frame, text="Editing Entry for:", font=("TkDefaultFont", 12, "bold")
|
|
||||||
).grid(row=0, column=0, sticky="w")
|
|
||||||
|
|
||||||
vars_dict["date"] = tk.StringVar(value=str(date))
|
|
||||||
date_entry = ttk.Entry(
|
|
||||||
header_frame,
|
|
||||||
textvariable=vars_dict["date"],
|
|
||||||
font=("TkDefaultFont", 12),
|
|
||||||
width=15,
|
|
||||||
)
|
|
||||||
date_entry.grid(row=0, column=1, sticky="w", padx=(10, 0))
|
|
||||||
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
# Symptoms section
|
|
||||||
symptoms_frame = ttk.LabelFrame(
|
|
||||||
parent, text="Daily Symptoms (0-10 scale)", padding="15"
|
|
||||||
)
|
|
||||||
symptoms_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
|
|
||||||
symptoms_frame.grid_columnconfigure(1, weight=1)
|
|
||||||
|
|
||||||
# Create symptom scales with better layout
|
|
||||||
symptoms = [
|
|
||||||
("Depression", "depression", dep),
|
|
||||||
("Anxiety", "anxiety", anx),
|
|
||||||
("Sleep Quality", "sleep", slp),
|
|
||||||
("Appetite", "appetite", app),
|
|
||||||
]
|
|
||||||
|
|
||||||
for i, (label, key, value) in enumerate(symptoms):
|
|
||||||
self._create_symptom_scale(symptoms_frame, i, label, key, value, vars_dict)
|
|
||||||
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
# Medications section
|
|
||||||
meds_frame = ttk.LabelFrame(parent, text="Medications Taken", padding="15")
|
|
||||||
meds_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
|
|
||||||
meds_frame.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
# Create medicine checkboxes with better styling
|
|
||||||
med_vars = self._create_medicine_section(
|
|
||||||
meds_frame, bup, hydro, gaba, prop, quet
|
|
||||||
)
|
|
||||||
vars_dict.update(med_vars)
|
|
||||||
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
# Dose tracking section
|
|
||||||
dose_frame = ttk.LabelFrame(parent, text="Dose Tracking", padding="15")
|
|
||||||
dose_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
|
|
||||||
dose_frame.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
dose_vars = self._create_dose_tracking(dose_frame, dose_data)
|
|
||||||
vars_dict.update(dose_vars)
|
|
||||||
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
# Notes section
|
|
||||||
notes_frame = ttk.LabelFrame(parent, text="Notes", padding="15")
|
|
||||||
notes_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20))
|
|
||||||
notes_frame.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
vars_dict["note"] = tk.StringVar(value=str(note))
|
|
||||||
note_text = tk.Text(
|
|
||||||
notes_frame, height=4, wrap=tk.WORD, font=("TkDefaultFont", 10)
|
|
||||||
)
|
|
||||||
note_text.grid(row=0, column=0, sticky="ew")
|
|
||||||
note_text.insert(1.0, str(note))
|
|
||||||
vars_dict["note_text"] = note_text
|
|
||||||
|
|
||||||
# Add scrollbar for notes
|
|
||||||
note_scroll = ttk.Scrollbar(
|
|
||||||
notes_frame, orient="vertical", command=note_text.yview
|
|
||||||
)
|
|
||||||
note_scroll.grid(row=0, column=1, sticky="ns")
|
|
||||||
note_text.configure(yscrollcommand=note_scroll.set)
|
|
||||||
|
|
||||||
return vars_dict
|
|
||||||
|
|
||||||
def _create_symptom_scale(
|
def _create_symptom_scale(
|
||||||
self,
|
self,
|
||||||
parent: ttk.Frame,
|
parent: ttk.Frame,
|
||||||
@@ -733,91 +807,6 @@ class UIManager:
|
|||||||
scale.bind("<KeyRelease>", update_value_label)
|
scale.bind("<KeyRelease>", update_value_label)
|
||||||
update_value_label() # Set initial color
|
update_value_label() # Set initial color
|
||||||
|
|
||||||
def _create_enhanced_symptom_scale(
|
|
||||||
self,
|
|
||||||
parent: ttk.Frame,
|
|
||||||
row: int,
|
|
||||||
label: str,
|
|
||||||
key: str,
|
|
||||||
value: int,
|
|
||||||
vars_dict: dict[str, tk.IntVar],
|
|
||||||
) -> None:
|
|
||||||
"""Create enhanced symptom scale for new entry form (like edit window)."""
|
|
||||||
# Ensure value is properly converted
|
|
||||||
try:
|
|
||||||
value = int(float(value)) if value not in ["", None] else 0
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
value = 0
|
|
||||||
|
|
||||||
# Label
|
|
||||||
label_widget = ttk.Label(
|
|
||||||
parent, text=f"{label} (0-10):", font=("TkDefaultFont", 10, "bold")
|
|
||||||
)
|
|
||||||
label_widget.grid(row=row, column=0, sticky="w", padx=5, pady=8)
|
|
||||||
|
|
||||||
# Scale container
|
|
||||||
scale_container = ttk.Frame(parent)
|
|
||||||
scale_container.grid(row=row, column=1, sticky="ew", padx=(20, 5), pady=8)
|
|
||||||
scale_container.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
# Scale with value labels
|
|
||||||
scale_frame = ttk.Frame(scale_container)
|
|
||||||
scale_frame.grid(row=0, column=0, sticky="ew")
|
|
||||||
scale_frame.grid_columnconfigure(1, weight=1)
|
|
||||||
|
|
||||||
# Current value display
|
|
||||||
value_label = ttk.Label(
|
|
||||||
scale_frame,
|
|
||||||
text=str(value),
|
|
||||||
font=("TkDefaultFont", 12, "bold"),
|
|
||||||
foreground="#2E86AB",
|
|
||||||
width=3,
|
|
||||||
)
|
|
||||||
value_label.grid(row=0, column=0, padx=(0, 10))
|
|
||||||
|
|
||||||
# Scale widget
|
|
||||||
scale = ttk.Scale(
|
|
||||||
scale_frame,
|
|
||||||
from_=0,
|
|
||||||
to=10,
|
|
||||||
variable=vars_dict[key],
|
|
||||||
orient=tk.HORIZONTAL,
|
|
||||||
length=250, # Slightly smaller than edit window to fit better
|
|
||||||
)
|
|
||||||
scale.grid(row=0, column=1, sticky="ew")
|
|
||||||
|
|
||||||
# Scale labels (0, 5, 10)
|
|
||||||
labels_frame = ttk.Frame(scale_container)
|
|
||||||
labels_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0))
|
|
||||||
|
|
||||||
ttk.Label(labels_frame, text="0", font=("TkDefaultFont", 8)).grid(
|
|
||||||
row=0, column=0, sticky="w"
|
|
||||||
)
|
|
||||||
labels_frame.grid_columnconfigure(1, weight=1)
|
|
||||||
ttk.Label(labels_frame, text="5", font=("TkDefaultFont", 8)).grid(
|
|
||||||
row=0, column=1
|
|
||||||
)
|
|
||||||
ttk.Label(labels_frame, text="10", font=("TkDefaultFont", 8)).grid(
|
|
||||||
row=0, column=2, sticky="e"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update label when scale changes
|
|
||||||
def update_value_label(event=None):
|
|
||||||
current_val = vars_dict[key].get()
|
|
||||||
value_label.configure(text=str(current_val))
|
|
||||||
# Change color based on value
|
|
||||||
if current_val <= 3:
|
|
||||||
value_label.configure(foreground="#28A745") # Green for low/good
|
|
||||||
elif current_val <= 6:
|
|
||||||
value_label.configure(foreground="#FFC107") # Yellow for medium
|
|
||||||
else:
|
|
||||||
value_label.configure(foreground="#DC3545") # Red for high/bad
|
|
||||||
|
|
||||||
scale.bind("<Motion>", update_value_label)
|
|
||||||
scale.bind("<ButtonRelease-1>", update_value_label)
|
|
||||||
scale.bind("<KeyRelease>", update_value_label)
|
|
||||||
update_value_label() # Set initial color
|
|
||||||
|
|
||||||
def _create_enhanced_pathology_scale(
|
def _create_enhanced_pathology_scale(
|
||||||
self,
|
self,
|
||||||
parent: ttk.Frame,
|
parent: ttk.Frame,
|
||||||
@@ -880,9 +869,15 @@ class UIManager:
|
|||||||
variable=vars_dict[key],
|
variable=vars_dict[key],
|
||||||
orient=tk.HORIZONTAL,
|
orient=tk.HORIZONTAL,
|
||||||
length=250,
|
length=250,
|
||||||
|
style="Modern.Horizontal.TScale",
|
||||||
)
|
)
|
||||||
scale.grid(row=0, column=1, sticky="ew")
|
scale.grid(row=0, column=1, sticky="ew")
|
||||||
|
|
||||||
|
# Add tooltip for the scale
|
||||||
|
pathology = self.pathology_manager.get_pathology(key)
|
||||||
|
if pathology:
|
||||||
|
self.tooltip_manager.add_scale_tooltip(scale, pathology.display_name)
|
||||||
|
|
||||||
# Scale labels
|
# Scale labels
|
||||||
labels_frame = ttk.Frame(scale_container)
|
labels_frame = ttk.Frame(scale_container)
|
||||||
labels_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0))
|
labels_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0))
|
||||||
@@ -927,153 +922,6 @@ class UIManager:
|
|||||||
update_value_label_pathology() # Set initial color
|
update_value_label_pathology() # Set initial color
|
||||||
|
|
||||||
def _create_medicine_section(
|
def _create_medicine_section(
|
||||||
self, parent: ttk.Frame, bup: int, hydro: int, gaba: int, prop: int, quet: int
|
|
||||||
) -> dict[str, tk.IntVar]:
|
|
||||||
"""Create medicine checkboxes with organized layout."""
|
|
||||||
vars_dict = {}
|
|
||||||
|
|
||||||
# Create a grid layout for medicines
|
|
||||||
medicines = [
|
|
||||||
("bupropion", bup, "Bupropion", "150/300 mg", "#E8F4FD"),
|
|
||||||
("hydroxyzine", hydro, "Hydroxyzine", "25 mg", "#FFF2E8"),
|
|
||||||
("gabapentin", gaba, "Gabapentin", "100 mg", "#F0F8E8"),
|
|
||||||
("propranolol", prop, "Propranolol", "10 mg", "#FCE8F3"),
|
|
||||||
("quetiapine", quet, "Quetiapine", "25 mg", "#E8F0FF"),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create medicine cards in a 2-column layout
|
|
||||||
for i, (key, value, name, dose, _bg_color) in enumerate(medicines):
|
|
||||||
row = i // 2
|
|
||||||
col = i % 2
|
|
||||||
|
|
||||||
# Medicine card frame
|
|
||||||
med_card = ttk.Frame(parent, relief="solid", borderwidth=1)
|
|
||||||
med_card.grid(row=row, column=col, sticky="ew", padx=5, pady=5)
|
|
||||||
parent.grid_columnconfigure(col, weight=1)
|
|
||||||
|
|
||||||
vars_dict[key] = tk.IntVar(value=int(value))
|
|
||||||
|
|
||||||
# Checkbox with medicine name
|
|
||||||
check_frame = ttk.Frame(med_card)
|
|
||||||
check_frame.pack(fill="x", padx=10, pady=8)
|
|
||||||
|
|
||||||
checkbox = ttk.Checkbutton(
|
|
||||||
check_frame,
|
|
||||||
text=f"{name} ({dose})",
|
|
||||||
variable=vars_dict[key],
|
|
||||||
style="Medicine.TCheckbutton",
|
|
||||||
)
|
|
||||||
checkbox.pack(anchor="w")
|
|
||||||
|
|
||||||
return vars_dict
|
|
||||||
|
|
||||||
def _create_dose_tracking(
|
|
||||||
self, parent: ttk.Frame, dose_data: dict[str, str]
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Create dose tracking interface."""
|
|
||||||
vars_dict = {}
|
|
||||||
|
|
||||||
# Create notebook for organized dose tracking
|
|
||||||
notebook = ttk.Notebook(parent)
|
|
||||||
notebook.pack(fill="both", expand=True)
|
|
||||||
|
|
||||||
medicines = [
|
|
||||||
("bupropion", "Bupropion"),
|
|
||||||
("hydroxyzine", "Hydroxyzine"),
|
|
||||||
("gabapentin", "Gabapentin"),
|
|
||||||
("propranolol", "Propranolol"),
|
|
||||||
("quetiapine", "Quetiapine"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for med_key, med_name in medicines:
|
|
||||||
# Create tab for each medicine
|
|
||||||
tab_frame = ttk.Frame(notebook)
|
|
||||||
notebook.add(tab_frame, text=med_name)
|
|
||||||
|
|
||||||
# Configure tab layout
|
|
||||||
tab_frame.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
# Quick dose entry section
|
|
||||||
entry_frame = ttk.LabelFrame(tab_frame, text="Add New Dose", padding="10")
|
|
||||||
entry_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
|
|
||||||
entry_frame.grid_columnconfigure(1, weight=1)
|
|
||||||
|
|
||||||
ttk.Label(entry_frame, text="Dose amount:").grid(
|
|
||||||
row=0, column=0, sticky="w"
|
|
||||||
)
|
|
||||||
|
|
||||||
dose_entry_var = tk.StringVar()
|
|
||||||
vars_dict[f"{med_key}_entry_var"] = dose_entry_var
|
|
||||||
|
|
||||||
dose_entry = ttk.Entry(entry_frame, textvariable=dose_entry_var, width=15)
|
|
||||||
dose_entry.grid(row=0, column=1, sticky="w", padx=(10, 10))
|
|
||||||
|
|
||||||
# Quick dose buttons
|
|
||||||
quick_frame = ttk.Frame(entry_frame)
|
|
||||||
quick_frame.grid(row=0, column=2, sticky="w")
|
|
||||||
|
|
||||||
# Common dose amounts (customize per medicine)
|
|
||||||
quick_doses = self._get_quick_doses(med_key)
|
|
||||||
for i, dose in enumerate(quick_doses):
|
|
||||||
ttk.Button(
|
|
||||||
quick_frame,
|
|
||||||
text=dose,
|
|
||||||
width=8,
|
|
||||||
command=lambda d=dose, var=dose_entry_var: var.set(d),
|
|
||||||
).grid(row=0, column=i, padx=2)
|
|
||||||
|
|
||||||
# Take dose button
|
|
||||||
def create_take_dose_command(med_name, entry_var, med_key):
|
|
||||||
def take_dose():
|
|
||||||
self._take_dose(med_name, entry_var, med_key, vars_dict)
|
|
||||||
|
|
||||||
return take_dose
|
|
||||||
|
|
||||||
take_button = ttk.Button(
|
|
||||||
entry_frame,
|
|
||||||
text=f"Take {med_name}",
|
|
||||||
style="Accent.TButton",
|
|
||||||
command=create_take_dose_command(med_name, dose_entry_var, med_key),
|
|
||||||
)
|
|
||||||
take_button.grid(row=1, column=0, columnspan=3, pady=(10, 0), sticky="ew")
|
|
||||||
|
|
||||||
# Dose history section
|
|
||||||
history_frame = ttk.LabelFrame(
|
|
||||||
tab_frame, text="Today's Doses", padding="10"
|
|
||||||
)
|
|
||||||
history_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=5)
|
|
||||||
history_frame.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
# Dose history display with fixed height to prevent excessive expansion
|
|
||||||
dose_text = tk.Text(
|
|
||||||
history_frame,
|
|
||||||
height=4, # Reduced height to fit better in scrollable window
|
|
||||||
wrap=tk.WORD,
|
|
||||||
font=("Consolas", 10),
|
|
||||||
state="normal", # Start enabled
|
|
||||||
)
|
|
||||||
dose_text.grid(row=0, column=0, sticky="ew")
|
|
||||||
|
|
||||||
# Store raw dose string in a variable
|
|
||||||
doses_str = dose_data.get(med_key, "")
|
|
||||||
dose_str_var = tk.StringVar(value=doses_str)
|
|
||||||
vars_dict[f"{med_key}_doses_str"] = dose_str_var
|
|
||||||
|
|
||||||
# Populate with existing doses
|
|
||||||
self._populate_dose_history(dose_text, dose_str_var.get())
|
|
||||||
|
|
||||||
vars_dict[f"{med_key}_doses_text"] = dose_text
|
|
||||||
|
|
||||||
# Scrollbar for dose history
|
|
||||||
dose_scroll = ttk.Scrollbar(
|
|
||||||
history_frame, orient="vertical", command=dose_text.yview
|
|
||||||
)
|
|
||||||
dose_scroll.grid(row=0, column=1, sticky="ns")
|
|
||||||
dose_text.configure(yscrollcommand=dose_scroll.set)
|
|
||||||
|
|
||||||
return vars_dict
|
|
||||||
|
|
||||||
def _create_medicine_section_dynamic(
|
|
||||||
self, parent: ttk.Frame, medicine_values: dict[str, int]
|
self, parent: ttk.Frame, medicine_values: dict[str, int]
|
||||||
) -> dict[str, tk.IntVar]:
|
) -> dict[str, tk.IntVar]:
|
||||||
"""Create medicine checkboxes dynamically."""
|
"""Create medicine checkboxes dynamically."""
|
||||||
@@ -1120,7 +968,7 @@ class UIManager:
|
|||||||
|
|
||||||
return vars_dict
|
return vars_dict
|
||||||
|
|
||||||
def _create_dose_tracking_dynamic(
|
def _create_dose_tracking(
|
||||||
self, parent: ttk.Frame, medicine_doses: dict[str, str]
|
self, parent: ttk.Frame, medicine_doses: dict[str, str]
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Create dose tracking interface dynamically."""
|
"""Create dose tracking interface dynamically."""
|
||||||
@@ -1398,9 +1246,33 @@ class UIManager:
|
|||||||
|
|
||||||
# Get note text from Text widget
|
# Get note text from Text widget
|
||||||
note_text_widget = vars_dict.get("note_text")
|
note_text_widget = vars_dict.get("note_text")
|
||||||
|
self.logger.debug(f"note_text_widget found: {note_text_widget is not None}")
|
||||||
|
self.logger.debug(f"vars_dict keys: {list(vars_dict.keys())}")
|
||||||
|
|
||||||
note_content = ""
|
note_content = ""
|
||||||
if note_text_widget:
|
if note_text_widget:
|
||||||
note_content = note_text_widget.get(1.0, tk.END).strip()
|
try:
|
||||||
|
note_content = note_text_widget.get(1.0, tk.END).strip()
|
||||||
|
self.logger.debug(f"Note content from widget: '{note_content}'")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error getting note from text widget: {e}")
|
||||||
|
# Fallback to StringVar
|
||||||
|
note_var = vars_dict.get("note")
|
||||||
|
if note_var:
|
||||||
|
note_content = note_var.get()
|
||||||
|
self.logger.debug(
|
||||||
|
f"Note content from StringVar fallback: '{note_content}'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Fallback to StringVar if note_text widget not found
|
||||||
|
note_var = vars_dict.get("note")
|
||||||
|
if note_var:
|
||||||
|
note_content = note_var.get()
|
||||||
|
self.logger.debug(f"Note content from StringVar: '{note_content}'")
|
||||||
|
else:
|
||||||
|
self.logger.error("No note widget or StringVar found!")
|
||||||
|
|
||||||
|
self.logger.debug(f"Final note_content: '{note_content}'")
|
||||||
|
|
||||||
# Extract dose data dynamically from all medicines
|
# Extract dose data dynamically from all medicines
|
||||||
dose_data = {}
|
dose_data = {}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -20,6 +20,28 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
|
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -258,6 +280,30 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" },
|
{ url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lxml"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559, upload-time = "2025-06-28T18:47:31.091Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143, upload-time = "2025-06-28T18:47:33.612Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469, upload-time = "2025-07-03T19:19:13.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049, upload-time = "2025-07-03T19:19:16.409Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "macholib"
|
name = "macholib"
|
||||||
version = "1.16.3"
|
version = "1.16.3"
|
||||||
@@ -653,6 +699,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reportlab"
|
||||||
|
version = "4.4.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2f/83/3d44b873fa71ddc7d323c577fe4cfb61e05b34d14e64b6a232f9cfbff89d/reportlab-4.4.3.tar.gz", hash = "sha256:073b0975dab69536acd3251858e6b0524ed3e087e71f1d0d1895acb50acf9c7b", size = 3887532, upload-time = "2025-07-23T11:18:23.799Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/c8/aaf4e08679e7b1dc896ad30de0d0527f0fd55582c2e6deee4f2cc899bf9f/reportlab-4.4.3-py3-none-any.whl", hash = "sha256:df905dc5ec5ddaae91fc9cb3371af863311271d555236410954961c5ee6ee1b5", size = 1953896, upload-time = "2025-07-23T11:18:20.572Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -698,14 +757,17 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thechart"
|
name = "thechart"
|
||||||
version = "1.6.1"
|
version = "1.9.5"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorlog" },
|
{ name = "colorlog" },
|
||||||
{ name = "dotenv" },
|
{ name = "dotenv" },
|
||||||
|
{ name = "lxml" },
|
||||||
{ name = "matplotlib" },
|
{ name = "matplotlib" },
|
||||||
{ name = "pandas" },
|
{ name = "pandas" },
|
||||||
|
{ name = "reportlab" },
|
||||||
{ name = "tk" },
|
{ name = "tk" },
|
||||||
|
{ name = "ttkthemes" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
@@ -723,9 +785,12 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "colorlog", specifier = ">=6.9.0" },
|
{ name = "colorlog", specifier = ">=6.9.0" },
|
||||||
{ name = "dotenv", specifier = ">=0.9.9" },
|
{ name = "dotenv", specifier = ">=0.9.9" },
|
||||||
|
{ name = "lxml", specifier = ">=6.0.0" },
|
||||||
{ name = "matplotlib", specifier = ">=3.10.3" },
|
{ name = "matplotlib", specifier = ">=3.10.3" },
|
||||||
{ name = "pandas", specifier = ">=2.3.1" },
|
{ name = "pandas", specifier = ">=2.3.1" },
|
||||||
|
{ name = "reportlab", specifier = ">=4.4.3" },
|
||||||
{ name = "tk", specifier = ">=0.1.0" },
|
{ name = "tk", specifier = ">=0.1.0" },
|
||||||
|
{ name = "ttkthemes", specifier = ">=3.2.2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
@@ -748,6 +813,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1e/0b/029cbdb868bb555fed99bf6540fff072d500b3f895873709f25084e85e33/tk-0.1.0-py3-none-any.whl", hash = "sha256:703a69ff0d5ba2bd2f7440582ad10160e4a6561595d33457dc6caa79b9bf4930", size = 3879, upload-time = "2019-07-08T06:51:55.175Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/0b/029cbdb868bb555fed99bf6540fff072d500b3f895873709f25084e85e33/tk-0.1.0-py3-none-any.whl", hash = "sha256:703a69ff0d5ba2bd2f7440582ad10160e4a6561595d33457dc6caa79b9bf4930", size = 3879, upload-time = "2019-07-08T06:51:55.175Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ttkthemes"
|
||||||
|
version = "3.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pillow" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fa/45/ab8ada55281af99a03bc0f8be53a502eb37ee34b94819a9ced89e8b0c12f/ttkthemes-3.2.2.tar.gz", hash = "sha256:01daed001f2ff0e4f32832a0d9ea48176c0c505203b030756bdde3bd1bcb21d2", size = 891159, upload-time = "2021-02-15T12:57:14.719Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tzdata"
|
name = "tzdata"
|
||||||
version = "2025.2"
|
version = "2025.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user