1- from queue import Empty
21from math import ceil
2+ from typing import Optional
33from logging import info , error
44from datetime import timedelta
55from traceback import format_exception
6- from tkinter .messagebox import showerror # type: ignore
6+ from tkinter .messagebox import showerror # type: ignore
77
8- from states import LessonState , MessageEnum , Message , State , PollEnum , PollResult
8+ from states import LessonState , State , StatePollEnum , StatePollResult
99from clock import Clock
1010from _logging import init_logger
11- from windows import MainWindow , SecondWindow
11+ from windows import MainWindow , SecondWindow , LessonsEditWindow , WeekdayEditWindow
12+ from windows import MainPollEnum , MainPollResult , SecondPollResult
1213
1314
1415class Calender :
1516 """Main program class."""
1617
18+ MAX_POINT_DISPLAY = 9.9
19+
1720 @classmethod
1821 def format_minutes (cls , minute : float , max_limit : float ) -> str :
1922 """
@@ -34,171 +37,239 @@ def format_minutes(cls, minute: float, max_limit: float) -> str:
3437 def minute (cls , delta : timedelta ) -> float :
3538 return delta .total_seconds () / 60
3639
40+ @classmethod
41+ def format_progress (cls , remaining : timedelta , total : timedelta , half_limit : bool ) -> str :
42+ total_min = cls .minute (total )
43+ remaining_min = cls .minute (remaining )
44+ max_limit = cls .MAX_POINT_DISPLAY
45+ if half_limit :
46+ max_limit = min (max_limit , total_min / 2 )
47+ return "{0}/{1:.0f}" .format (
48+ cls .format_minutes (remaining_min , max_limit ),
49+ total_min
50+ )
51+
3752 def __init__ (self ) -> None :
38- self .state = State ()
39- self .main_window = MainWindow (self .state )
40- self .second_window = SecondWindow (self .state )
53+ self .st = State ()
54+ self .main_window = MainWindow (self .st )
55+ self .second_window = SecondWindow (self .st )
4156 self .main_window .load ()
4257 self .running = True
4358
44- info ("Initialized\n {}" .format (repr (self .state )))
59+ info ("Initialized\n {}" .format (repr (self .st )))
4560
4661 def mainloop (self ) -> None :
47- clock = Clock (self .state .inspect_interval )
62+ clock = Clock (self .st .inspect_interval )
4863 while self .running :
4964 clock .wait ()
50- poll_results = self .state .poll_all ()
65+ poll_results = self .st .poll_all ()
5166 breaking = False
5267 for i , poll_result in enumerate (poll_results ):
53- if self .handle_poll (poll_result , i + 1 == len (poll_results )):
68+ if self .handle_state_poll (poll_result , i + 1 == len (poll_results )):
5469 breaking = True
5570 if breaking :
5671 info ("Breaking poll result. Continue." )
5772 if not breaking :
73+ main_poll_result = self .main_window .poll ()
74+ if main_poll_result is not None :
75+ self .handle_main_poll (main_poll_result )
76+ second_poll_result = self .second_window .poll ()
77+ if second_poll_result is not None :
78+ self .handle_second_poll (second_poll_result )
79+ if not self .running :
80+ break
5881 self .poll_update ()
5982 self .main_window .update ()
60- while True :
61- try :
62- event = self .state .queue .get (False )
63- info (f"Event captured: { event } " )
64- except Empty :
65- break
66- self .handle_event (event )
6783
6884 def set_windows_topmost (self , topmost : bool ) -> None :
6985 self .main_window .set_topmost (topmost )
7086 self .second_window .set_topmost (topmost )
7187
72- def handle_event (self , event : Message ) -> None :
73- """Handle the event sent by windows children."""
74- if event [0 ] == MessageEnum .ShutDown :
75- self .second_window .set_topmost (True )
76- self .second_window .animate ((1 , 1 , 1 , 1 ), 500 )
77- self .second_window .destroy ()
78- self .second_window .quit ()
79- self .running = False
80- elif event [0 ] == MessageEnum .Resize :
81- duration_ms = event [1 ]
82- w1 , h1 = self .main_window .winfo_width (), self .main_window .winfo_height ()
83- label = self .second_window .label
84- w2 = label .winfo_width () + 2 * self .state .layout .padding_x
85- h2 = label .winfo_height () + 2 * self .state .layout .padding_y
86- w_screen = self .main_window .winfo_screenwidth ()
87- gap = self .state .layout .windows_gap if len (label ['text' ]) else 0
88- remaining = max (w_screen - gap - w1 - w2 , 0 )
89- x1 = remaining // 2
90- x2 = w_screen - w2 - remaining // 2
91- self .second_window .animate (
92- (w2 , h2 , x2 , self .state .layout .margin_y ), duration_ms )
93- self .main_window .animate (
94- (w1 , h1 , x1 , self .state .layout .margin_y ), duration_ms )
95- elif event [0 ] == MessageEnum .HideTemporarily :
96- self .set_windows_topmost (False )
97- self .state .lesson_state = LessonState .AtClass
98- self .state .current_lesson -= 1
99- lesson = self .state .i_lesson (self .state .current_lesson )
100- lesson .delay = self .state .now + self .state .temporary_hide - \
101- lesson .finish
102- next_lesson = self .state .i_lesson_checked (
103- self .state .current_lesson + 1 )
104- if next_lesson is not None :
105- lesson .delay = min (
106- lesson .delay ,
107- next_lesson .prepare - lesson .finish
108- )
109- self .main_window .class_advance_label ['text' ] = "下课"
110- elif event [0 ] == MessageEnum .ClassAdvance :
111- if event [1 ] == 'on' :
112- self .set_windows_topmost (False )
113- self .state .lesson_state = LessonState .AtClass
114- if self .state .current_lesson > 0 :
115- self .main_window .class_labels [
116- self .state .current_lesson - 1 ]['fg' ] = self .state .color_theme .fg
117- self .main_window .class_labels [self .state .current_lesson ]['fg' ] = self .state .color_theme .hint
118- self .main_window .class_advance_label ['text' ] = "下课"
119- else :
120- self .state .current_lesson += 1
121- if self .state .current_lesson < len (self .state .lessons ):
122- self .state .lesson_state = LessonState .Break
123- else :
124- self .state .lesson_state = LessonState .AfterSchool
125- self .main_window .class_advance_label ['text' ] = "上课"
88+ def reload (self ) -> None :
89+ info (f"Select the timetable of weekday (0~7) { self .st .weekday } " )
90+ self .st .load_lessons ()
91+ self .main_window .load ()
12692
127- else :
128- assert False , event # unreachable
93+ def resize (self , duration_ms : int , w1 : Optional [int ] = None ) -> None :
94+ if w1 is None :
95+ w1 = self .main_window .winfo_width ()
96+ h1 = self .main_window .winfo_height ()
97+ label = self .second_window .label
98+ w2 = label .winfo_width () + 2 * self .st .layout .padding_x
99+ h2 = label .winfo_height () + 2 * self .st .layout .padding_y
100+ w_screen = self .main_window .winfo_screenwidth ()
101+ gap = self .st .layout .windows_gap if len (label ['text' ]) else 0
102+ remaining = max (w_screen - gap - w1 - w2 , 0 )
103+ x1 = remaining // 2
104+ x2 = w_screen - w2 - remaining // 2
105+ self .main_window .animate (
106+ (w1 , h1 , x1 , self .st .layout .margin_y ), duration_ms )
107+ self .second_window .animate (
108+ (w2 , h2 , x2 , self .st .layout .margin_y ), duration_ms )
109+
110+ def shutdown (self ) -> None :
111+ self .set_windows_topmost (True )
112+ self .main_window .destroy (True )
113+ self .second_window .destroy (True )
114+ self .running = False
115+
116+ def class_begin (self , index : int ) -> None :
117+ self .set_windows_topmost (False )
118+ self .main_window .class_advance_label ['text' ] = "下课"
119+ class_labels = self .main_window .class_labels
120+ if index > 0 :
121+ class_labels [index - 1
122+ ]['fg' ] = self .st .color_theme .fg
123+ class_labels [index ]['fg' ] = self .st .color_theme .hint
124+
125+ def class_finish (self ) -> None :
126+ self .set_windows_topmost (True )
127+ self .main_window .class_advance_label ['text' ] = "上课"
128+
129+ def class_prepare (self , index : int , bell : bool ) -> None :
130+ if bell :
131+ self .main_window .bell ()
132+ if index > 0 :
133+ label = self .main_window .class_labels [index - 1 ]
134+ label ['fg' ] = self .st .color_theme .fg
135+ label = self .main_window .class_labels [index ]
136+ label ['fg' ] = self .st .color_theme .hint
129137
130- def handle_poll (self , event : PollResult , update : bool ) -> bool :
138+ def adjust_color (self , pass_ratio : float ):
139+ pass_ratio = min (max (pass_ratio , 0 ), 1 )
140+ class_labels = self .main_window .class_labels
141+ i = self .st .current_index
142+ if i > 0 :
143+ class_labels [i - 1 ]['fg' ] = self .st .color_theme .gradient (
144+ (1 - pass_ratio ) / 1.5 )
145+ if i < len (self .st .lessons ):
146+ class_labels [i ]['fg' ] = self .st .color_theme .gradient (
147+ (1 + pass_ratio ) / 2 )
148+
149+ def handle_state_poll (self , event : StatePollResult , update : bool ) -> bool :
131150 """Handle the polling result."""
132- if event [0 ] == PollEnum .Reload :
133- info (f"Select the timetable of weekday { self .state .weekday } " )
134- self .state .load_lessons ()
135- self .main_window .load ()
151+ if event [0 ] == StatePollEnum .Reload :
152+ self .reload ()
136153 return True
137- elif event [0 ] == PollEnum .ClassBegin :
138- self .set_windows_topmost (False )
139- self .main_window .class_advance_label ['text' ] = "下课"
140- elif event [0 ] == PollEnum .ClassFinish :
141- self .set_windows_topmost (True )
142- self .main_window .class_advance_label ['text' ] = "上课"
143- elif event [0 ] == PollEnum .ClassPrepare :
144- if update :
145- self .main_window .bell ()
146- if event [1 ] > 0 :
147- label = self .main_window .class_labels [event [1 ] - 1 ]
148- label ['fg' ] = self .state .color_theme .fg
149- label = self .main_window .class_labels [event [1 ]]
150- label ['fg' ] = self .state .color_theme .hint
154+ elif event [0 ] == StatePollEnum .ClassBegin :
155+ self .class_begin (event [1 ])
156+ elif event [0 ] == StatePollEnum .ClassFinish :
157+ self .class_finish ()
158+ elif event [0 ] == StatePollEnum .ClassPrepare :
159+ self .class_prepare (event [1 ], update )
151160 else :
152- assert False , event
161+ assert False , f"unreachable { event } "
153162 return False
154163
164+ def handle_main_poll (self , poll_result : MainPollResult ) -> None :
165+ """Handle the event sent by main window."""
166+ info (f"Main poll captured: { poll_result } " )
167+
168+ if poll_result [0 ] == MainPollEnum .ShutDown :
169+ self .shutdown ()
170+ elif poll_result [0 ] == MainPollEnum .Resize :
171+ self .resize (2000 , poll_result [1 ])
172+ elif poll_result [0 ] == MainPollEnum .HideTemporarily :
173+ original_state = self .st .lesson_state
174+ self .st .lesson_state = LessonState .AtClass
175+ self .st .current_index -= 1
176+ lesson = self .st .current_lesson ()
177+ delay = self .st .now + self .st .temporary_hide - lesson .finish
178+ if original_state != LessonState .AfterSchool :
179+ delay = min (
180+ delay ,
181+ self .st .next_lesson ().prepare - lesson .finish
182+ )
183+ lesson .delay = delay
184+ self .class_begin (self .st .current_index )
185+ elif poll_result [0 ] == MainPollEnum .ClassAdvance :
186+ if poll_result [1 ] == 'on' :
187+ self .class_begin (self .st .current_index )
188+ self .st .lesson_state = LessonState .AtClass
189+ elif poll_result [1 ] == 'off' :
190+ self .st .current_index += 1
191+ if self .st .current_index < len (self .st .lessons ):
192+ self .st .lesson_state = LessonState .Break
193+ else :
194+ self .st .lesson_state = LessonState .AfterSchool
195+ self .class_finish ()
196+ else :
197+ assert False , f"unreachable { poll_result [1 ]} "
198+ elif poll_result [0 ] == MainPollEnum .ChangeClass :
199+ info ("Try changing new lessons." )
200+ new_lessons = LessonsEditWindow (self .st ).run ()
201+ info (f"Change classes to: { new_lessons } " )
202+ if new_lessons is not None :
203+ self .st .raw_schedule [self .st .weekday ] = new_lessons
204+ self .reload ()
205+ elif poll_result [0 ] == MainPollEnum .ChangeWeekday :
206+ info ("Try changing the weekday." )
207+ weekday = WeekdayEditWindow (self .st ).run ()
208+ info (f"Change weekday to: { weekday } " )
209+ if weekday is not None :
210+ self .st .weekday_map [self .st .now .weekday ()] = weekday
211+
212+ else :
213+ assert False , f"unreachable { poll_result } "
214+
215+ def handle_second_poll (self , poll_result : SecondPollResult ) -> None :
216+ info (f"Second poll captured: { poll_result } " )
217+
218+ if poll_result == SecondPollResult .ShutDown :
219+ self .shutdown ()
220+ elif poll_result == SecondPollResult .BigChangeResize :
221+ self .resize (2000 )
222+ elif poll_result == SecondPollResult .SmallChangeResize :
223+ self .resize (300 )
224+ else :
225+ assert poll_result is None , f"unreachable { poll_result } "
226+
155227 def poll_update (self ):
156228 """Polling, updating mainly the second window and resizing."""
157- MIN_POINT = 9.9
158- text = ""
159- if self .state .lesson_state == LessonState .AtClass :
160- text = "上课时间"
161- lesson = self .state .i_lesson (self .state .current_lesson )
162- if lesson .name in self .state .self_study_lessons :
163- total = self .minute (
164- lesson .finish + lesson .delay - lesson .start )
165- last = self .minute (lesson .finish - self .state .now )
166- text += " {0}/{1:.0f}" .format (
167- self .format_minutes (last , min (MIN_POINT , total / 2 )), total
229+ MAX_MIN_OUT_CLASS = 15
230+
231+ text_prefix = ""
232+ text_suffix = ""
233+ if self .st .lesson_state == LessonState .AtClass :
234+ lesson = self .st .current_lesson ()
235+ text_prefix = "上课时间"
236+ if lesson .name in self .st .self_study_lessons :
237+ text_suffix = self .format_progress (
238+ lesson .finish - self .st .now ,
239+ lesson .real_finish () - lesson .start ,
240+ True
168241 )
169- elif self .state .lesson_state == LessonState .Preparing :
170- lesson = self .state .i_lesson (self .state .current_lesson )
171- total = self .minute (self .state .preparation )
172- last = self .minute (lesson .start - self .state .now )
173- text = "预备铃 {0}/{1:.0f}" .format (
174- self .format_minutes (last , MIN_POINT ), total
175- )
176- elif self .state .lesson_state == LessonState .Break :
177- lesson = self .state .i_lesson (self .state .current_lesson )
178- total = self .minute (
179- lesson .prepare -
180- self .state .i_lesson (self .state .current_lesson - 1 ).finish )
181- last = self .minute (lesson .prepare - self .state .now )
182- text = "下课 {0}/{1:.0f}" .format (
183- self .format_minutes (last , min (MIN_POINT , total / 2 )), total
242+ elif self .st .lesson_state == LessonState .Preparing :
243+ lesson = self .st .current_lesson ()
244+ text_prefix = "预备铃"
245+ text_suffix = self .format_progress (
246+ lesson .start - self .st .now ,
247+ self .st .preparation ,
248+ False
184249 )
185- current_lesson = self .state .current_lesson
186- pass_ratio = max (min (last / total , 1 ), 0 )
187- if current_lesson > 0 :
188- self .main_window .class_labels [current_lesson ]['fg' ] = self .state .color_theme .gradient (
189- pass_ratio / 2 )
190- self .main_window .class_labels [
191- current_lesson - 1 ]['fg' ] = self .state .color_theme .gradient (1 - pass_ratio / 2 )
192- elif self .state .lesson_state == LessonState .BeforeSchool :
193- first_lesson = self .state .i_lesson (0 )
194- text = "{}" .format (self .format_minutes (self .minute (
195- first_lesson .prepare - self .state .now ), MIN_POINT ))
196- elif self .state .lesson_state == LessonState .AfterSchool :
197- self .main_window .class_labels [- 1 ]['fg' ] = self .state .color_theme .fg
198- last_lesson = self .state .i_lesson (- 1 )
199- text = "放学 {}" .format (self .format_minutes (
200- self .minute (self .state .now - last_lesson .finish ), MIN_POINT ))
201- self .second_window .set_text (text )
250+ elif self .st .lesson_state == LessonState .Break :
251+ lesson = self .st .current_lesson ()
252+ text_prefix = "下课"
253+ remaining = lesson .prepare - self .st .now
254+ total = lesson .prepare - self .st .last_lesson ().finish
255+ text_suffix = self .format_progress (remaining , total , True )
256+ self .adjust_color (1 - remaining / total )
257+ elif self .st .lesson_state == LessonState .BeforeSchool :
258+ text_prefix = ""
259+ remaining_min = self .minute (
260+ self .st .current_lesson ().prepare - self .st .now )
261+ text_suffix = self .format_minutes (
262+ remaining_min , self .MAX_POINT_DISPLAY )
263+ self .adjust_color (1 - remaining_min / MAX_MIN_OUT_CLASS )
264+ elif self .st .lesson_state == LessonState .AfterSchool :
265+ text_prefix = "放学"
266+ last_lesson = self .st .last_lesson ()
267+ past_min = self .minute (self .st .now - last_lesson .finish )
268+ text_suffix = self .format_minutes (
269+ past_min , self .MAX_POINT_DISPLAY )
270+ self .adjust_color (past_min / MAX_MIN_OUT_CLASS )
271+
272+ self .second_window .set_text ((text_prefix , text_suffix ))
202273
203274
204275if __name__ == "__main__" :
0 commit comments