55popup time selectors for time selection.
66"""
77
8+ import configparser
89import datetime
910import logging
1011import sys
1112import tkinter as tk
1213from dataclasses import dataclass
14+ from pathlib import Path
1315from tkinter import ttk
14- from typing import Optional
16+ from typing import Dict , Optional
1517
1618from ..widget import get_scaling_factor
1719from ..widget .timespinner import TimeSpinner
1820
1921
22+ def _load_theme_colors (theme_name : str = "light" ) -> Dict [str , str ]:
23+ """
24+ Load theme colors from .ini file.
25+
26+ Args:
27+ theme_name: Name of the theme ('light' or 'dark')
28+
29+ Returns:
30+ Dict with color values
31+ """
32+ try :
33+ current_dir = Path (__file__ ).parent .parent / "themes"
34+ theme_file = current_dir / f"{ theme_name } .ini"
35+
36+ if not theme_file .exists ():
37+ # Return default light theme if file doesn't exist
38+ return {
39+ 'time_background' : 'white' ,
40+ 'time_foreground' : '#333333' ,
41+ 'time_spinbox_bg' : '#f0f0f0' ,
42+ 'time_spinbox_button_bg' : 'white' ,
43+ 'time_spinbox_hover_bg' : '#e0e0e0' ,
44+ 'time_spinbox_active_bg' : '#d0d0d0' ,
45+ 'time_spinbox_outline' : '#cccccc' ,
46+ 'time_separator_color' : '#333333' ,
47+ 'time_label_color' : '#333333' ,
48+ 'time_toggle_bg' : '#f0f0f0' ,
49+ 'time_toggle_slider_bg' : 'white' ,
50+ 'time_toggle_outline' : '#cccccc' ,
51+ 'time_toggle_active_fg' : '#333333' ,
52+ 'time_toggle_inactive_fg' : '#999999' ,
53+ 'time_button_bg' : 'white' ,
54+ 'time_button_fg' : '#000000' ,
55+ 'time_button_active_bg' : '#f5f5f5' ,
56+ 'time_button_active_fg' : '#000000'
57+ }
58+
59+ config = configparser .ConfigParser ()
60+ config .read (theme_file )
61+
62+ if theme_name not in config :
63+ # Return default light theme if section doesn't exist
64+ return {
65+ 'time_background' : 'white' ,
66+ 'time_foreground' : '#333333' ,
67+ 'time_spinbox_bg' : '#f0f0f0' ,
68+ 'time_spinbox_button_bg' : 'white' ,
69+ 'time_spinbox_hover_bg' : '#e0e0e0' ,
70+ 'time_spinbox_active_bg' : '#d0d0d0' ,
71+ 'time_spinbox_outline' : '#cccccc' ,
72+ 'time_separator_color' : '#333333' ,
73+ 'time_label_color' : '#333333' ,
74+ 'time_toggle_bg' : '#f0f0f0' ,
75+ 'time_toggle_slider_bg' : 'white' ,
76+ 'time_toggle_outline' : '#cccccc' ,
77+ 'time_toggle_active_fg' : '#333333' ,
78+ 'time_toggle_inactive_fg' : '#999999' ,
79+ 'time_button_bg' : 'white' ,
80+ 'time_button_fg' : '#000000' ,
81+ 'time_button_active_bg' : '#f5f5f5' ,
82+ 'time_button_active_fg' : '#000000'
83+ }
84+
85+ theme_section = config [theme_name ]
86+ return dict (theme_section )
87+ except Exception :
88+ # Return default light theme on any error
89+ return {
90+ 'time_background' : 'white' ,
91+ 'time_foreground' : '#333333' ,
92+ 'time_spinbox_bg' : '#f0f0f0' ,
93+ 'time_spinbox_button_bg' : 'white' ,
94+ 'time_spinbox_hover_bg' : '#e0e0e0' ,
95+ 'time_spinbox_active_bg' : '#d0d0d0' ,
96+ 'time_spinbox_outline' : '#cccccc' ,
97+ 'time_separator_color' : '#333333' ,
98+ 'time_label_color' : '#333333' ,
99+ 'time_toggle_bg' : '#f0f0f0' ,
100+ 'time_toggle_slider_bg' : 'white' ,
101+ 'time_toggle_outline' : '#cccccc' ,
102+ 'time_toggle_active_fg' : '#333333' ,
103+ 'time_toggle_inactive_fg' : '#999999' ,
104+ 'time_button_bg' : 'white' ,
105+ 'time_button_fg' : '#000000' ,
106+ 'time_button_active_bg' : '#f5f5f5' ,
107+ 'time_button_active_fg' : '#000000'
108+ }
109+
110+
20111@dataclass
21112class TimePickerConfig :
22113 """Configuration for TimePicker widgets."""
@@ -70,6 +161,7 @@ def __init__( # pylint: disable=R0917
70161 self .time_format = config .time_format
71162 self .hour_format = config .hour_format
72163 self .show_seconds = config .show_seconds
164+ self .theme = config .theme if hasattr (config , 'theme' ) else theme
73165 self .selected_time = None
74166 self .popup = None
75167 self .time_callback = config .time_callback
@@ -313,52 +405,84 @@ def show_time_picker(self):
313405 self .logger .debug ("show_time_picker called but popup already exists; ignoring" )
314406 return
315407 self .logger .debug ("Creating time picker popup" )
316- self .popup = tk .Toplevel (self )
317- self .popup .withdraw ()
318- self .popup .overrideredirect (True )
319- self .popup .resizable (False , False )
320-
321- # Set theme colors
322- if self .hour_format == "12" :
323- bg_color = "#f0f0f0"
324- else :
325- bg_color = "#ffffff"
326- self .popup .configure (bg = bg_color )
327-
328- # Get the toplevel window from parent
329- toplevel = self .master .winfo_toplevel () if hasattr (self , "master" ) else None
330- if toplevel :
331- self .popup .transient (toplevel )
332- self .popup .after (100 , self ._setup_focus )
333-
334- # Create time picker widget
335- self .time_picker = TimeSpinner (
336- self .popup ,
337- hour_format = self .hour_format ,
338- show_seconds = self .show_seconds ,
339- time_callback = self ._on_time_selected ,
340- theme = self .theme if hasattr (self , "theme" ) else "light" ,
341- initial_time = self .selected_time ,
342- )
343- self .logger .debug (
344- "TimeSpinner created with hour_format=%s show_seconds=%s initial=%r" ,
345- self .hour_format ,
346- self .show_seconds ,
347- self .selected_time ,
348- )
408+ try :
409+ # Prefer using self as parent if it's a real widget; otherwise fallback to master
410+ parent_for_popup = self
411+ try :
412+ _ = self .winfo_toplevel # attribute presence check; may raise in fallback init
413+ except Exception : # pylint: disable=broad-except
414+ parent_for_popup = self .master
415+
416+ self .popup = tk .Toplevel (parent_for_popup )
417+ self .popup .withdraw ()
418+ self .popup .overrideredirect (True )
419+ self .popup .resizable (False , False )
420+ except tk .TclError as e :
421+ # In headless/tests or when the application has been destroyed, avoid raising
422+ self .logger .debug ("Failed to create Toplevel popup: %s" , e )
423+ return
349424
350- self ._bind_time_picker_events (self .time_picker )
351- self .time_picker .pack (expand = True , fill = "both" , padx = 2 , pady = 2 )
425+ try :
426+ # Set theme colors from theme file
427+ theme_colors = _load_theme_colors (self .theme )
428+ bg_color = theme_colors .get ('time_background' , 'white' )
429+ self .popup .configure (bg = bg_color )
352430
353- # Position the popup
354- self .popup .update_idletasks ()
355- self ._position_popup ()
431+ # Force update to ensure background color is applied
432+ self .popup .update_idletasks ()
433+
434+ # Get the toplevel window from parent
435+ toplevel = self .master .winfo_toplevel () if hasattr (self , "master" ) else None
436+ if toplevel :
437+ self .popup .transient (toplevel )
438+ self .popup .after (100 , self ._setup_focus )
439+
440+ # Create time picker widget
441+ self .time_picker = TimeSpinner (
442+ self .popup ,
443+ hour_format = self .hour_format ,
444+ show_seconds = self .show_seconds ,
445+ time_callback = self ._on_time_selected ,
446+ theme = self .theme if hasattr (self , "theme" ) else "light" ,
447+ initial_time = self .selected_time ,
448+ )
449+
450+ # Ensure TimeSpinner background matches popup background
451+ self .time_picker .configure (bg = bg_color )
452+ self .time_picker .canvas .configure (bg = bg_color )
453+ self .time_picker .button_frame .configure (bg = bg_color )
454+
455+ self .logger .debug (
456+ "TimeSpinner created with hour_format=%s show_seconds=%s theme=%s initial=%r" ,
457+ self .hour_format ,
458+ self .show_seconds ,
459+ self .theme if hasattr (self , "theme" ) else "light" ,
460+ self .selected_time ,
461+ )
462+
463+ self ._bind_time_picker_events (self .time_picker )
464+ self .time_picker .pack (expand = True , fill = "both" , padx = 2 , pady = 2 )
465+
466+ # Position the popup
467+ self .popup .update_idletasks ()
468+ self ._position_popup ()
356469
357- self .popup .deiconify ()
358- self .popup .lift ()
359- self .popup .bind ("<Escape>" , lambda e : self .hide_time_picker ())
360- self ._setup_click_outside_handling ()
361- self .time_picker .focus_set ()
470+ self .popup .deiconify ()
471+ self .popup .lift ()
472+ self .popup .bind ("<Escape>" , lambda e : self .hide_time_picker ())
473+ self ._setup_click_outside_handling ()
474+ self .time_picker .focus_set ()
475+ except tk .TclError as e :
476+ # If any part of popup setup fails (e.g., app destroyed), cleanup and exit gracefully
477+ self .logger .debug ("Popup setup failed: %s" , e )
478+ try :
479+ if self .popup :
480+ self .popup .destroy ()
481+ except Exception : # pylint: disable=broad-except
482+ pass
483+ self .popup = None
484+ self .time_picker = None
485+ return
362486
363487 def _position_popup (self ):
364488 """Position the popup relative to the parent widget."""
@@ -367,7 +491,10 @@ def _position_popup(self):
367491 popup_height = self .popup .winfo_reqheight ()
368492
369493 # Get parent widget position
370- parent_y = self .master .winfo_rooty ()
494+ try :
495+ parent_y = self .master .winfo_rooty ()
496+ except tk .TclError :
497+ parent_y = 0
371498
372499 # Position below the input area (left edge aligned)
373500 if hasattr (self , "button" ) and self .entry is not None :
0 commit comments