12 Commits

Author SHA1 Message Date
William Valentin c3c88c63d2 Add theme management and settings functionality
Build and Push Docker Image / build-and-push (push) Has been cancelled
- Introduced `ThemeManager` to handle application themes using `ttkthemes`.
- Added `SettingsWindow` for user preferences including theme selection and UI settings.
- Integrated theme selection into the main application with a menu for quick access.
- Enhanced UI components with custom styles based on the selected theme.
- Implemented tooltips for better user guidance across various UI elements.
- Updated dependencies to include `ttkthemes` for improved visual appeal.
2025-08-05 11:58:25 -07:00
William Valentin 86606d56b6 feat: add comprehensive keyboard shortcuts for improved navigation and productivity
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-08-05 10:05:32 -07:00
William Valentin 9790f2730a feat: update version to 1.9.5 in Makefile and pyproject.toml
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-08-02 10:35:44 -07:00
William Valentin fdcc210fc4 feat: add status bar to UI for improved user feedback and information display 2025-08-02 10:31:17 -07:00
William Valentin b7a22524d7 Feat: add export functionality with GUI for data and graphs
Build and Push Docker Image / build-and-push (push) Has been cancelled
- Implemented ExportWindow class for exporting data and graphs in various formats (JSON, XML, PDF).
- Integrated ExportManager to handle export logic.
- Added export option in the main application menu.
- Enhanced user interface with data summary and export options.
- Included error handling and success messages for export operations.
- Updated dependencies in the lock file to include reportlab and lxml for PDF generation.
2025-08-02 10:00:24 -07:00
William Valentin 156dcd1651 feat: Import LOG_CLEAR constant for logging clarity 2025-08-01 15:15:04 -07:00
William Valentin 1d310dd081 feat: Update version to 1.7.5 in Makefile, docker-build.sh, and pyproject.toml
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-08-01 14:45:58 -07:00
William Valentin abd1fa33cf refactor: Simplify UI creation methods by removing dynamic variants and consolidating functionality 2025-08-01 14:41:58 -07:00
William Valentin 03ef9e761a feat: Update version to 1.7.4 in Makefile, docker-build.sh, and pyproject.toml
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-08-01 14:12:06 -07:00
William Valentin ca1f8c976d fix: notes are saved again
feat: Add test scripts for note saving and updating functionality
2025-08-01 14:09:29 -07:00
William Valentin 7392709a27 feat: Uncomment .vscode directory in .gitignore to include IDE settings 2025-08-01 13:25:47 -07:00
William Valentin 623050478a feat: Update version to 1.7.3 in Makefile, docker-build.sh, and pyproject.toml 2025-08-01 13:21:48 -07:00
26 changed files with 3211 additions and 460 deletions
+2 -1
View File
@@ -47,7 +47,7 @@ htmlcov/
.pylint.d/
# IDEs and editors
#.vscode/
.vscode/
!.vscode/tasks.json
!.vscode/launch.json
.idea/
@@ -81,3 +81,4 @@ Thumbs.db
.Trashes
ehthumbs.db
Thumbs.db
integration_test_exports/
+3 -18
View File
@@ -1,5 +1,5 @@
TARGET=thechart
VERSION=1.6.1
VERSION=1.9.5
ROOT=/home/will
ICON=chart-671.png
SHELL=fish
@@ -85,7 +85,7 @@ install: ## Set up the development environment
@echo "To run tests: make test"
build: ## Build 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
@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
@@ -121,21 +121,6 @@ test-watch: ## Run tests in watch mode
test-debug: ## Run tests with debug output
@echo "Running tests with debug output..."
.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
@echo "Running the linter..."
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; \
git add . && git commit --no-verify -m "$$msg"
@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
+48 -5
View File
@@ -1,7 +1,7 @@
# 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
# Install dependencies
make install
@@ -14,10 +14,22 @@ make test
```
## 📚 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
- **[Changelog](docs/CHANGELOG.md)** - Version history and feature evolution
- **[Quick Reference](#quick-reference)** - Common commands and shortcuts
- **[Changelog](docs/CHANGELOG.md)** - Version history and recent UI improvements
- **[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
- [Prerequisites](#prerequisites)
@@ -226,6 +238,13 @@ On first run, the application will:
- **Backward Compatibility**: Seamless upgrades without data loss
- **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)**.
## Development
@@ -475,6 +494,30 @@ thechart_data.csv # User data (created on first run)
- **`pyproject.toml`**: Project configuration and dependencies
- **`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?
+3 -3
View File
@@ -1,19 +1,19 @@
#!/usr/bin/bash
CONTAINER_ENGINE="docker" # podman | docker
VERSION="v1.0.0"
VERSION="v1.7.5"
REGISTRY="gitea-http.taildb3494.ts.net/will/thechart"
if [ "$CONTAINER_ENGINE" == "podman" ];
then
buildah build \
-t $REGISTRY:$VERSION \
--platform linux/amd64,linux/arm64/v8 \
--platform linux/amd64 \
--no-cache .
else
DOCKER_BUILDKIT=1 \
docker buildx build \
--platform linux/amd64,linux/arm64/v8 \
--platform linux/amd64 \
-t $REGISTRY:$VERSION \
--no-cache \
--push .
+69
View File
@@ -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/),
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
### 📚 Documentation Overhaul
+109
View File
@@ -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.
+215
View File
@@ -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
View File
@@ -1,7 +1,49 @@
# TheChart - Features Documentation
## 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
@@ -37,6 +79,36 @@ Each medicine includes:
2. **Manual Configuration**: Edit `medicines.json` directly
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
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
- **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
### 🏗️ Modular Design
- **MedicineManager**: Core medicine CRUD operations
- **PathologyManager**: Symptom and pathology management
- **GraphManager**: All graph-related operations and visualizations
- **UIManager**: User interface creation and management
- **DataManager**: CSV operations and data persistence
### Modern UI Architecture
- **ThemeManager**: Centralized theme management with dynamic switching
- **TooltipManager**: Smart tooltip system with context-sensitive help
- **UIManager**: Enhanced UI component creation with theme integration
- **SettingsWindow**: Advanced configuration interface with persistence
### 🔧 Configuration Management
- **JSON-based Configuration**: `medicines.json` and `pathologies.json`
- **Dynamic Loading**: Runtime configuration updates
- **Validation**: Input validation and error handling
- **Backward Compatibility**: Seamless updates and migrations
### 🏗️ Core Application Design
- **MedicineManager**: Core medicine CRUD operations with JSON persistence
- **PathologyManager**: Symptom and pathology management system
- **GraphManager**: Professional graph rendering with matplotlib integration
- **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
- **Matplotlib Visualization**: Professional graph rendering
- **Robust Parsing**: Handles various data formats and edge cases
- **Real-time Calculations**: Dynamic dose totals and averages
- **Real-time Calculations**: Dynamic dose totals, averages, and statistics
- **Robust Parsing**: Handles various data formats and edge cases gracefully
- **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
+71
View File
@@ -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
View File
@@ -1,16 +1,27 @@
# 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
### For Users
- **[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
- Advanced Dose Tracking
- Graph Visualizations
- 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
- **[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
- **[Changelog](CHANGELOG.md)** - Version history and feature evolution
- Recent updates and improvements
- Migration notes
- Future roadmap
- Recent UI/UX overhaul (v1.9.5)
- Keyboard shortcuts system (v1.7.0)
- Medicine and dose tracking improvements
- Migration notes and future roadmap
## 🚀 Quick Navigation
### Getting Started
1. **Installation**: See [README.md - Installation](../README.md#installation)
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
1. **Setup**: See [DEVELOPMENT.md - Development Environment Setup](DEVELOPMENT.md#development-environment-setup)
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
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)
## 📋 What's New in Documentation
## 📋 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:
- **Clear Structure**: Hierarchical organization with clear headings
- **Practical Examples**: Code snippets and usage examples
- **Up-to-date**: Synchronized with current codebase
- **Comprehensive**: Covers all major features and workflows
- **Cross-referenced**: Links between related sections
### Documentation Highlights
- **Professional UI/UX**: Complete documentation of the new theme system
- **Keyboard Efficiency**: Comprehensive shortcut system documentation
- **Developer-Friendly**: Enhanced development and testing documentation
- **User-Focused**: Clear separation of user vs developer documentation
## 🔍 Finding Information
### By Topic
- **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)
- **Keyboard Shortcuts** → [KEYBOARD_SHORTCUTS.md](KEYBOARD_SHORTCUTS.md)
- **Data Export** → [EXPORT_SYSTEM.md](EXPORT_SYSTEM.md)
- **Development** → [DEVELOPMENT.md](DEVELOPMENT.md)
- **Version History** → [CHANGELOG.md](CHANGELOG.md)
### By User Type
- **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)
### By Task
- **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)
- **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)
- **Deploy Application** → [README.md - Deployment](../README.md#deployment)
+4 -1
View File
@@ -1,15 +1,18 @@
[project]
name = "thechart"
version = "1.6.1"
version = "1.9.5"
description = "Chart to monitor your medication intake over time."
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"colorlog>=6.9.0",
"dotenv>=0.9.9",
"lxml>=6.0.0",
"matplotlib>=3.10.3",
"pandas>=2.3.1",
"reportlab>=4.4.3",
"tk>=0.1.0",
"ttkthemes>=3.2.2",
]
[dependency-groups]
+1
View File
@@ -3,3 +3,4 @@ matplotlib
pandas
dotenv
colorlog
ttkthemes
+5 -1
View File
@@ -24,7 +24,9 @@ packaging==25.0
pandas==2.3.1
# via -r requirements.in
pillow==11.3.0
# via matplotlib
# via
# matplotlib
# ttkthemes
pyparsing==3.2.3
# via matplotlib
python-dateutil==2.9.0.post0
@@ -39,5 +41,7 @@ six==1.17.0
# via python-dateutil
tk==0.1.0
# via -r requirements.in
ttkthemes==3.2.2
# via -r requirements.in
tzdata==2025.2
# via pandas
+61
View File
@@ -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
+128
View File
@@ -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)
+93
View File
@@ -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)
+69
View File
@@ -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()
+102
View File
@@ -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()
+385
View File
@@ -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,
}
+247
View File
@@ -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
+314 -35
View File
@@ -7,14 +7,18 @@ from typing import Any
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 export_manager import ExportManager
from export_window import ExportWindow
from graph_manager import GraphManager
from init import logger
from medicine_management_window import MedicineManagementWindow
from medicine_manager import MedicineManager
from pathology_management_window import PathologyManagementWindow
from pathology_manager import PathologyManager
from settings_window import SettingsWindow
from theme_manager import ThemeManager
from ui_manager import UIManager
@@ -40,16 +44,26 @@ class MedTrackerApp:
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":
logger.debug(f"Script name: {sys.argv[0]}")
logger.debug(f"Logs path: {LOG_PATH}")
logger.debug(f"Log clear: {LOG_CLEAR}")
logger.debug(f"First argument: {first_argument}")
# Initialize managers
self.medicine_manager: MedicineManager = MedicineManager(logger=logger)
self.pathology_manager: PathologyManager = PathologyManager(logger=logger)
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.filename, logger, self.medicine_manager, self.pathology_manager
@@ -67,6 +81,9 @@ class MedTrackerApp:
# Add menu bar
self._setup_menu()
# Setup keyboard shortcuts
self._setup_keyboard_shortcuts()
# Center the window on screen
self._center_window()
@@ -95,7 +112,7 @@ class MedTrackerApp:
import tkinter.ttk as ttk
# --- 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")
# Configure root window grid
@@ -103,7 +120,7 @@ class MedTrackerApp:
self.root.grid_columnconfigure(0, weight=1)
# 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_columnconfigure(i, weight=3 if i == 1 else 1)
logger.debug("Main frame and root grid configured for scaling.")
@@ -114,6 +131,15 @@ class MedTrackerApp:
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 ---
input_ui: dict[str, Any] = self.ui_manager.create_input_frame(main_frame)
self.input_frame: ttk.Frame = input_ui["frame"]
@@ -127,12 +153,12 @@ class MedTrackerApp:
self.input_frame,
[
{
"text": "Add Entry",
"text": "Add Entry (Ctrl+S)",
"command": self.add_new_entry,
"fill": "both",
"expand": True,
},
{"text": "Quit", "command": self.handle_window_closing},
{"text": "Quit (Ctrl+Q)", "command": self.handle_window_closing},
],
)
@@ -141,38 +167,216 @@ class MedTrackerApp:
self.tree: ttk.Treeview = table_ui["tree"]
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
self.refresh_data_display()
# Initialize status bar with ready message
self.ui_manager.update_status("Application ready", "info")
def _setup_menu(self) -> None:
"""Set up the menu bar."""
menubar = tk.Menu(self.root)
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 = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Tools", menu=tools_menu)
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(
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:
"""Open the pathology management window."""
self.ui_manager.update_status("Opening pathology manager", "info")
PathologyManagementWindow(
self.root, self.pathology_manager, self._refresh_ui_after_config_change
)
def _open_medicine_manager(self) -> None:
"""Open the medicine management window."""
self.ui_manager.update_status("Opening medicine manager", "info")
MedicineManagementWindow(
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:
"""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()
@@ -193,12 +397,12 @@ class MedTrackerApp:
self.input_frame,
[
{
"text": "Add Entry",
"text": "Add Entry (Ctrl+S)",
"command": self.add_new_entry,
"fill": "both",
"expand": True,
},
{"text": "Quit", "command": self.handle_window_closing},
{"text": "Quit (Ctrl+Q)", "command": self.handle_window_closing},
],
)
@@ -213,14 +417,59 @@ class MedTrackerApp:
# 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:
"""Handle double-click event to edit an entry."""
logger.debug("Double-click event triggered on treeview.")
if len(self.tree.get_children()) > 0:
item_id = self.tree.selection()[0]
item_values = self.tree.item(item_id, "values")
self.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}")
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:
"""Create a new Toplevel window for editing an entry."""
@@ -322,8 +571,10 @@ class MedTrackerApp:
values.append(note)
self.ui_manager.update_status("Saving changes...", "info")
if self.data_manager.update_entry(original_date, values):
edit_win.destroy()
self.ui_manager.update_status("Entry updated successfully!", "success")
messagebox.showinfo(
"Success", "Entry updated successfully!", parent=self.root
)
@@ -333,6 +584,7 @@ class MedTrackerApp:
# Check if it's a duplicate date issue
df = self.data_manager.load_data()
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(
"Error",
f"An entry for date '{date}' already exists. "
@@ -340,6 +592,7 @@ class MedTrackerApp:
parent=edit_win,
)
else:
self.ui_manager.update_status("Failed to save changes", "error")
messagebox.showerror("Error", "Failed to save changes", parent=edit_win)
def handle_window_closing(self) -> None:
@@ -384,10 +637,13 @@ class MedTrackerApp:
# Check if date is empty
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)
return
self.ui_manager.update_status("Adding new entry...", "info")
if self.data_manager.add_entry(entry):
self.ui_manager.update_status("Entry added successfully!", "success")
messagebox.showinfo(
"Success", "Entry added successfully!", parent=self.root
)
@@ -397,6 +653,7 @@ class MedTrackerApp:
# Check if it's a duplicate date by trying to load existing data
df = self.data_manager.load_data()
if not df.empty and self.date_var.get() in df["date"].values:
self.ui_manager.update_status("Duplicate entry found", "error")
messagebox.showerror(
"Error",
f"An entry for date '{self.date_var.get()}' already exists. "
@@ -404,6 +661,7 @@ class MedTrackerApp:
parent=self.root,
)
else:
self.ui_manager.update_status("Failed to add entry", "error")
messagebox.showerror("Error", "Failed to add entry", parent=self.root)
def _delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None:
@@ -418,13 +676,16 @@ class MedTrackerApp:
date: str = self.tree.item(item_id, "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):
edit_win.destroy()
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=edit_win)
def _clear_entries(self) -> None:
@@ -446,38 +707,56 @@ class MedTrackerApp:
if children:
self.tree.delete(*children)
# Load data from the CSV file
df: pd.DataFrame = self.data_manager.load_data()
try:
# Load data from the CSV file
df: pd.DataFrame = self.data_manager.load_data()
# Update the treeview with the data
if not df.empty:
# Build display columns dynamically (exclude dose columns for table view)
display_columns = ["date"]
# Update the treeview with the data
if not df.empty:
# Build display columns dynamically
# (exclude dose columns for table view)
display_columns = ["date"]
# Add pathology columns
for pathology_key in self.pathology_manager.get_pathology_keys():
display_columns.append(pathology_key)
# Add pathology columns
for pathology_key in self.pathology_manager.get_pathology_keys():
display_columns.append(pathology_key)
# Add medicine columns (without dose columns)
for medicine_key in self.medicine_manager.get_medicine_keys():
display_columns.append(medicine_key)
# Add medicine columns (without dose columns)
for medicine_key in self.medicine_manager.get_medicine_keys():
display_columns.append(medicine_key)
display_columns.append("note")
display_columns.append("note")
# 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]
# 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:
# Fallback - just use all columns
display_df = df
self.ui_manager.update_status("Data loaded successfully", "success")
# Batch insert for better performance
for _index, row in display_df.iterrows():
self.tree.insert(parent="", index="end", values=list(row))
logger.debug(f"Loaded {len(display_df)} entries into treeview.")
# Update the graph
self.graph_manager.update_graph(df)
except Exception as e:
logger.error(f"Error loading data: {e}")
self.ui_manager.update_status(f"Error loading data: {str(e)}", "error")
if __name__ == "__main__":
+324
View File
@@ -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()
+298
View File
@@ -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",
}
+163
View File
@@ -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
View File
@@ -11,6 +11,7 @@ from PIL import Image, ImageTk
from medicine_manager import MedicineManager
from pathology_manager import PathologyManager
from tooltip_system import TooltipManager
class UIManager:
@@ -22,11 +23,21 @@ class UIManager:
logger: logging.Logger,
medicine_manager: MedicineManager,
pathology_manager: PathologyManager,
theme_manager, # Import would create circular dependency
) -> None:
self.root: tk.Tk = root
self.logger: logging.Logger = logger
self.medicine_manager = medicine_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:
"""Set up the application icon."""
@@ -65,13 +76,20 @@ class UIManager:
def create_input_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
"""Create and configure the input frame with all widgets."""
# Create main container for the scrollable input frame
main_container = ttk.LabelFrame(parent_frame, text="New Entry")
main_container = 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_rowconfigure(0, weight=1)
main_container.grid_columnconfigure(0, weight=1)
# 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(
main_container, orient="vertical", command=canvas.yview
)
@@ -159,7 +177,9 @@ class UIManager:
ttk.Label(input_frame, text="Treatment:").grid(
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_columnconfigure(0, weight=1)
@@ -173,11 +193,19 @@ class UIManager:
text = f"{medicine.display_name} {medicine.dosage_info}"
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
ttk.Checkbutton(medicine_frame, text=text, variable=var).grid(
row=idx, column=0, sticky="w", padx=5, pady=2
checkbox = ttk.Checkbutton(
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_row = medicine_row + 1
@@ -189,16 +217,19 @@ class UIManager:
ttk.Label(input_frame, text="Note:").grid(
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
)
ttk.Label(input_frame, text="Date (mm/dd/yyyy):").grid(
row=date_row, column=0, sticky="w", padx=5, pady=2
)
ttk.Entry(input_frame, textvariable=date_var, justify="center").grid(
row=date_row, column=1, sticky="ew", padx=5, pady=2
)
ttk.Entry(
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
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]:
"""Create and configure the table frame with a treeview."""
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")
@@ -253,7 +284,34 @@ class UIManager:
col_labels.append("Note")
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):
tree.heading(col, text=label)
@@ -272,7 +330,9 @@ class UIManager:
def create_graph_frame(self, parent_frame: ttk.Frame) -> ttk.LabelFrame:
"""Create and configure the graph frame."""
graph_frame: ttk.LabelFrame = ttk.LabelFrame(parent_frame, text="Evolution")
graph_frame: 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")
return graph_frame
@@ -284,19 +344,137 @@ class UIManager:
button_frame.grid(row=7, column=0, columnspan=2, pady=10)
for btn_config in buttons_config:
ttk.Button(
button = ttk.Button(
button_frame,
text=btn_config["text"],
command=btn_config["command"],
).pack(
style="Action.TButton",
)
button.pack(
side="left",
padx=5,
fill=btn_config.get("fill", None),
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
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(
self, values: tuple[str, ...], callbacks: dict[str, Callable]
) -> tk.Toplevel:
@@ -417,8 +595,8 @@ class UIManager:
# Extract note (should be the last value)
note = values_list[-1] if len(values_list) > 0 else ""
# Create improved UI sections dynamically
vars_dict = self._create_edit_ui_dynamic(
# Create improved UI sections
vars_dict = self._create_edit_ui(
main_container,
date,
pathology_values,
@@ -443,7 +621,7 @@ class UIManager:
return edit_win
def _create_edit_ui_dynamic(
def _create_edit_ui(
self,
parent: ttk.Frame,
date: str,
@@ -500,7 +678,7 @@ class UIManager:
meds_frame.grid_columnconfigure(0, weight=1)
# 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)
row += 1
@@ -510,7 +688,7 @@ class UIManager:
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_dynamic(dose_frame, medicine_doses)
dose_vars = self._create_dose_tracking(dose_frame, medicine_doses)
vars_dict.update(dose_vars)
row += 1
@@ -532,6 +710,7 @@ class UIManager:
)
note_text.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
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
def update_note(*args):
@@ -542,111 +721,6 @@ class UIManager:
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(
self,
parent: ttk.Frame,
@@ -733,91 +807,6 @@ class UIManager:
scale.bind("<KeyRelease>", update_value_label)
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(
self,
parent: ttk.Frame,
@@ -880,9 +869,15 @@ class UIManager:
variable=vars_dict[key],
orient=tk.HORIZONTAL,
length=250,
style="Modern.Horizontal.TScale",
)
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
labels_frame = ttk.Frame(scale_container)
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
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]
) -> dict[str, tk.IntVar]:
"""Create medicine checkboxes dynamically."""
@@ -1120,7 +968,7 @@ class UIManager:
return vars_dict
def _create_dose_tracking_dynamic(
def _create_dose_tracking(
self, parent: ttk.Frame, medicine_doses: dict[str, str]
) -> dict[str, Any]:
"""Create dose tracking interface dynamically."""
@@ -1398,9 +1246,33 @@ class UIManager:
# Get note text from Text widget
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 = ""
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
dose_data = {}
Generated
+76 -2
View File
@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.13"
[[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" },
]
[[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]]
name = "colorama"
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" },
]
[[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]]
name = "macholib"
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" },
]
[[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]]
name = "ruff"
version = "0.12.5"
@@ -698,14 +757,17 @@ wheels = [
[[package]]
name = "thechart"
version = "1.6.1"
version = "1.9.5"
source = { virtual = "." }
dependencies = [
{ name = "colorlog" },
{ name = "dotenv" },
{ name = "lxml" },
{ name = "matplotlib" },
{ name = "pandas" },
{ name = "reportlab" },
{ name = "tk" },
{ name = "ttkthemes" },
]
[package.dev-dependencies]
@@ -723,9 +785,12 @@ dev = [
requires-dist = [
{ name = "colorlog", specifier = ">=6.9.0" },
{ name = "dotenv", specifier = ">=0.9.9" },
{ name = "lxml", specifier = ">=6.0.0" },
{ name = "matplotlib", specifier = ">=3.10.3" },
{ name = "pandas", specifier = ">=2.3.1" },
{ name = "reportlab", specifier = ">=4.4.3" },
{ name = "tk", specifier = ">=0.1.0" },
{ name = "ttkthemes", specifier = ">=3.2.2" },
]
[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" },
]
[[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]]
name = "tzdata"
version = "2025.2"