Move drag_info from global to class attribute for better encapsulation

## Code Quality Improvements

### Global State Removal
- Eliminated global drag_info dictionary
- Moved drag_state management into TimeTracker class
- Removed all global drag_info dependencies

### Updated Components
- **ClickableCell constructor**: Added time_tracker parameter for proper reference
- **ClickableCell methods**: Updated to use self.time_tracker.drag_info
- **TimeTracker methods**: Updated on_global_drag() and on_global_up()
- **Instance creation**: Updated ClickableCell instantiation calls

### Benefits Achieved
- **Better Encapsulation**: State properly contained within class boundaries
- **Thread Safety**: Reduced race conditions from shared global state
- **Testability**: Individual instance testing now possible
- **Instance Isolation**: Multiple TimeTracker instances work independently
- **Maintainability**: Clearer code structure with explicit dependencies

### Verification
-  All drag functionality preserved (paint/erase operations)
-  Drag state management works correctly
-  Multiple instances properly isolated
-  All 6 existing test suites pass (no regressions)
-  New comprehensive test suite created and passing
-  Application starts and runs correctly

## Files Modified
- **time_tracker.py**: Global state removal and class attribute implementation
- **AGENTS.md**: Updated coding guidelines for class preferences
- **TODO.md**: Marked drag_info task as completed, updated progress
- **tests/test_drag_info_class_attribute.py**: New comprehensive test suite

## Testing
- Added complete test suite for drag_info functionality
- Tests verify global state removal and class attribute access
- Confirms multiple instance isolation
- Validates drag state management

Code quality significantly improved with zero functional regressions.
This commit is contained in:
2025-10-29 17:38:00 -04:00
parent a564d430f8
commit fbdf450c14
4 changed files with 254 additions and 41 deletions

View File

@@ -12,13 +12,7 @@ import calendar
import re
from collections import defaultdict
# Global drag state
drag_info = {
'active': False,
'mode': None, # 'paint' or 'erase'
'start_row': None,
'last_cell': None
}
def sanitize_csv_text(text):
"""Sanitize text for safe CSV writing"""
@@ -169,10 +163,11 @@ def validate_input(input_type, value, **kwargs):
return sanitize_csv_text(value)
class ClickableCell(tk.Frame):
def __init__(self, parent, row_col_key, callback, width=5, height=2, start_hour=9):
def __init__(self, parent, row_col_key, callback, time_tracker, width=5, height=2, start_hour=9):
super().__init__(parent, relief="solid", borderwidth=1, width=width, height=height)
self.row_col_key = row_col_key
self.callback = callback
self.time_tracker = time_tracker
self.checked = False
self.start_hour = start_hour
@@ -196,12 +191,11 @@ class ClickableCell(tk.Frame):
self.bind("<Button-1>", self.on_mouse_down)
def on_mouse_down(self, event):
global drag_info
# Start drag mode
drag_info['active'] = True
drag_info['mode'] = 'paint' if not self.checked else 'erase'
drag_info['start_row'] = self.row_col_key[0]
self.time_tracker.drag_info['active'] = True
self.time_tracker.drag_info['mode'] = 'paint' if not self.checked else 'erase'
self.time_tracker.drag_info['start_row'] = self.row_col_key[0]
# Toggle this cell
self.checked = not self.checked
@@ -210,13 +204,11 @@ class ClickableCell(tk.Frame):
else:
self.label.config(bg=self.default_bg, text=" ")
self.callback(self.row_col_key, self.checked)
drag_info['last_cell'] = self.row_col_key
self.time_tracker.drag_info['last_cell'] = self.row_col_key
def apply_drag_state(self, force_mode=None):
"""Apply drag state to this cell"""
global drag_info
mode = force_mode or drag_info['mode']
mode = force_mode or self.time_tracker.drag_info['mode']
if mode == 'paint' and not self.checked:
self.checked = True
@@ -252,6 +244,14 @@ class TimeTracker:
self.work_hours = settings['work_hours']
self.archive_path = settings['archive_path']
# Drag state - moved from global to class attribute
self.drag_info = {
'active': False,
'mode': None, # 'paint' or 'erase'
'start_row': None,
'last_cell': None
}
# Main container with scrollbars
main_container = tk.Frame(root)
main_container.pack(fill=tk.BOTH, expand=True)
@@ -484,9 +484,8 @@ class TimeTracker:
def on_global_drag(self, event):
"""Global drag handler that finds which cell we're over"""
global drag_info
if not drag_info['active']:
if not self.drag_info['active']:
return
# Find which widget we're currently over
@@ -501,19 +500,18 @@ class TimeTracker:
break
current_widget = current_widget.master
if cell and cell.row_col_key != drag_info['last_cell']:
if cell and cell.row_col_key != self.drag_info['last_cell']:
applied = cell.apply_drag_state()
if applied:
drag_info['last_cell'] = cell.row_col_key
self.drag_info['last_cell'] = cell.row_col_key
def on_global_up(self, event):
"""Global mouse up handler"""
global drag_info
drag_info['active'] = False
drag_info['mode'] = None
drag_info['start_row'] = None
drag_info['last_cell'] = None
self.drag_info['active'] = False
self.drag_info['mode'] = None
self.drag_info['start_row'] = None
self.drag_info['last_cell'] = None
def create_headers(self):
headers = ["Job", "Task Name", "Notes", "Customer"]
@@ -578,7 +576,7 @@ class TimeTracker:
self.time_cells[row_num] = {}
time_slots = self.work_hours * 4 # Calculate based on current settings
for i in range(time_slots):
cell = ClickableCell(self.scrollable_frame, (row_num, i), self.on_time_cell_clicked, width=5, height=1, start_hour=self.start_hour)
cell = ClickableCell(self.scrollable_frame, (row_num, i), self.on_time_cell_clicked, self, width=5, height=1, start_hour=self.start_hour)
cell.grid(row=row_num, column=4 + i, sticky="nsew", padx=1, pady=1)
self.time_cells[row_num][i] = cell