Skip to content
This repository was archived by the owner on Aug 10, 2024. It is now read-only.

Commit fdb1b1a

Browse files
committed
v0.4.0 Resolve #3, Fix #1, Fix #2
1 parent b8c4a05 commit fdb1b1a

File tree

4 files changed

+381
-257
lines changed

4 files changed

+381
-257
lines changed

src/class_form.py

Lines changed: 208 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1-
from queue import Empty
21
from math import ceil
2+
from typing import Optional
33
from logging import info, error
44
from datetime import timedelta
55
from 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
99
from clock import Clock
1010
from _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

1415
class 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

204275
if __name__ == "__main__":

0 commit comments

Comments
 (0)