Skip to content

Commit 3cd7aef

Browse files
authored
Support Dialog Boxes (#457)
* Separated `Window` and `Pot`. (#456) * Supported setting debug window status. (#456) * Bug fixed: the callbacks attached to the variable that remain after the widget is destroyed cause errors that it cannot find the widget. (#456) * Supported warning popups. (#456) * Bug fixed. (#456) * Bug fixed: unmatched types. (#456) * Bug fixed: wrong vertical position. (#456)
1 parent 7104ff9 commit 3cd7aef

File tree

7 files changed

+112
-89
lines changed

7 files changed

+112
-89
lines changed

leads/data_persistence/analyzer/processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def shared_pre(row: dict[str, _Any], i: int) -> tuple[int, float, float]:
189189
return (dt := t - self._lap_start_time), (
190190
ds := mileage - self._lap_start_mileage), 3600000 * ds / dt if dt else 0
191191

192-
def shared_post(duration: float, distance: float, avg_speed: float) -> None:
192+
def shared_post(duration: int, distance: float, avg_speed: float) -> None:
193193
if self._max_lap_duration is None or duration > self._max_lap_duration:
194194
self._max_lap_duration = duration
195195
if self._max_lap_distance is None or distance > self._max_lap_distance:

leads_gui/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def _(cfg: Config) -> None:
3939
_set_on_register_config(_on_register_config)
4040

4141

42-
def initialize(window: Window,
42+
def initialize(window: Pot,
4343
render: _Callable[[ContextManager], None],
4444
leads: _LEADS[_Any]) -> ContextManager:
4545
main_controller = _get_controller(_MAIN_CONTROLLER)

leads_gui/performance_checker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def frame_rate(self) -> float:
2020
return 1 / _average(self._delay_seq)
2121

2222
def net_delay(self) -> float:
23-
return _average(self._net_delay_seq)
23+
return float(_average(self._net_delay_seq))
2424

2525
def record_frame(self, last_interval: float) -> None:
2626
# add .0000000001 to avoid zero division

leads_gui/prototype.py

Lines changed: 81 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import ABCMeta as _ABCMeta, abstractmethod as _abstractmethod
22
from json import dumps as _dumps
33
from time import time as _time
4-
from tkinter import Misc as _Misc, Event as _Event, PhotoImage as _PhotoImage
4+
from tkinter import Misc as _Misc, Event as _Event, PhotoImage as _PhotoImage, TclError as _TclError
55
from typing import Callable as _Callable, Self as _Self, TypeVar as _TypeVar, Generic as _Generic, Any as _Any, \
66
Literal as _Literal, override as _override
77

@@ -173,7 +173,10 @@ def attach(self, callback: _Callable[[], None]) -> None:
173173

174174
def unique(_, __, ___) -> None:
175175
if (v := self._variable.get()) != self._last_value:
176-
callback()
176+
try:
177+
callback()
178+
except _TclError:
179+
self.detach()
177180
self._last_value = v
178181

179182
self._trace_cb_name = self._variable.trace_add("write", unique)
@@ -214,7 +217,6 @@ def attempt(self) -> bool:
214217

215218
class _RuntimeData(object):
216219
def __init__(self) -> None:
217-
self.protected_pot: Window | None = None
218220
self.start_time: int = int(_time())
219221
self.comm: _Server | None = None
220222
self.comm_stream: _Server | None = None
@@ -247,66 +249,35 @@ def __new__(cls, *args, **kwargs) -> _RuntimeData:
247249
return super().__new__(cls, *args, **kwargs)
248250

249251

250-
T = _TypeVar("T", bound=RuntimeData)
251-
252-
253-
class Window(_Generic[T]):
252+
class Window(object):
254253
def __init__(self,
255-
width: int,
256-
height: int,
257-
refresh_rate: int,
258-
runtime_data: T,
259-
on_refresh: _Callable[[_Self], None] = lambda _: None,
254+
master: _Misc | None = None,
255+
width: int = 720,
256+
height: int = 480,
260257
title: str = "LEADS",
261258
fullscreen: bool = False,
262259
no_title_bar: bool = True,
263-
theme_mode: _Literal["system", "light", "dark"] = "system",
264-
display: int = 0) -> None:
265-
self._refresh_rate: int = refresh_rate
266-
self._runtime_data: T = runtime_data
267-
self._on_refresh: _Callable[[Window], None] = on_refresh
268-
self._frequency_generators: dict[str, FrequencyGenerator] = {}
269-
self._display: int = display
270-
271-
pot = runtime_data.protected_pot
272-
popup = False
273-
274-
if pot:
275-
self._master: _CTkToplevel = _CTkToplevel(pot._master)
276-
popup = display == pot._display
277-
if not popup:
278-
self._master.bind("<Leave>", lambda _: pot._master.focus_force())
279-
self.show()
260+
display: int = 0,
261+
popup: bool = False) -> None:
262+
self._pot_master: _Misc | None = master
263+
if master:
264+
self._master: _CTk | _CTkToplevel = _CTkToplevel(master)
265+
elif self.__class__ is not Pot:
266+
raise TypeError("Use `Pot` for root windows")
280267
else:
281-
self._master: _CTk = _CTk()
282-
runtime_data.protected_pot = self
268+
self._master: _CTk | _CTkToplevel = _CTk()
269+
popup = False
283270
screen = _get_monitors()[display]
284-
self._master.title(title)
285-
self._master.wm_iconbitmap()
286-
self._master.iconphoto(True, _PhotoImage(master=self._master, file=f"{_ASSETS_PATH}/logo.png"))
287-
self._master.overrideredirect(no_title_bar)
288-
_set_appearance_mode(theme_mode)
271+
self._screen_x: int = screen.x
272+
self._screen_y: int = screen.y
289273
self._screen_width: int = screen.width
290274
self._screen_height: int = screen.height
291275
self._width: int = self._screen_width if fullscreen else width
292276
self._height: int = self._screen_height if fullscreen else height
293-
294-
x, y = int((self._screen_width - self._width) * .5) + screen.x, int((self._screen_height - self._height) * .5)
295-
if popup:
296-
x = int((pot._width - self._width) * .5 + pot._master.winfo_rootx())
297-
y = int((pot._height - self._height) * .5 + pot._master.winfo_rooty())
298-
self._master.geometry(f"{self._width}x{self._height}+{x}+{y}")
299-
self._master.resizable(False, False)
300-
301-
self._active: bool = isinstance(self._master, _CTkToplevel)
302-
self._performance_checker: PerformanceChecker = PerformanceChecker()
303-
self._last_interval: float = 0
304-
305-
def root(self) -> _CTk:
306-
return self._master
307-
308-
def is_pot(self) -> bool:
309-
return isinstance(self._master, _CTk)
277+
self._title: str = title
278+
self._no_title_bar: bool = no_title_bar
279+
self._display: int = display
280+
self._popup: bool = popup
310281

311282
def screen_index(self) -> int:
312283
return self._display
@@ -323,6 +294,55 @@ def width(self) -> int:
323294
def height(self) -> int:
324295
return self._height
325296

297+
def root(self) -> _CTk:
298+
return self._master
299+
300+
def show(self) -> None:
301+
self._master.title(self._title)
302+
self._master.wm_iconbitmap()
303+
self._master.iconphoto(True, _PhotoImage(master=self._master, file=f"{_ASSETS_PATH}/logo.png"))
304+
self._master.overrideredirect(self._no_title_bar)
305+
x, y = (int((self._screen_width - self._width) * .5) + self._screen_x,
306+
int((self._screen_height - self._height) * .5))
307+
if self._popup:
308+
x = int((self._pot_master.winfo_width() - self._width) * .5 + self._pot_master.winfo_rootx())
309+
y = int((self._pot_master.winfo_height() - self._height) * .5 + self._pot_master.winfo_rooty())
310+
self._master.transient(self._pot_master)
311+
elif self._pot_master:
312+
y += self._pot_master.winfo_screenheight() - self._screen_height - self._screen_y
313+
self._master.geometry(f"{self._width}x{self._height}+{x}+{y}")
314+
self._master.resizable(False, False)
315+
316+
def kill(self) -> None:
317+
self._master.destroy()
318+
319+
320+
T = _TypeVar("T", bound=RuntimeData)
321+
322+
323+
class Pot(Window, _Generic[T]):
324+
def __init__(self,
325+
width: int,
326+
height: int,
327+
refresh_rate: int,
328+
runtime_data: T,
329+
on_refresh: _Callable[[_Self], None] = lambda _: None,
330+
title: str = "LEADS",
331+
fullscreen: bool = False,
332+
no_title_bar: bool = True,
333+
theme_mode: _Literal["system", "light", "dark"] = "system",
334+
display: int = 0) -> None:
335+
Window.__init__(self, None, width, height, title, fullscreen, no_title_bar, display)
336+
self._refresh_rate: int = refresh_rate
337+
self._runtime_data: T = runtime_data
338+
self._on_refresh: _Callable[[Pot], None] = on_refresh
339+
self._frequency_generators: dict[str, FrequencyGenerator] = {}
340+
_set_appearance_mode(theme_mode)
341+
342+
self._active: bool = isinstance(self._master, _CTkToplevel)
343+
self._performance_checker: PerformanceChecker = PerformanceChecker()
344+
self._last_interval: float = 0
345+
326346
def frame_rate(self) -> float:
327347
return self._performance_checker.frame_rate()
328348

@@ -359,16 +379,9 @@ def clear_frequency_generators(self) -> None:
359379
def active(self) -> bool:
360380
return self._active
361381

382+
@_override
362383
def show(self) -> None:
363-
try:
364-
if isinstance(self._master, _CTkToplevel):
365-
pot = self._runtime_data.protected_pot
366-
if self._display == pot._display:
367-
self._master.transient(pot._master)
368-
return
369-
finally:
370-
self._active = True
371-
384+
super().show()
372385
def wrapper(init: bool) -> None:
373386
if not init:
374387
self._on_refresh(self)
@@ -383,26 +396,24 @@ def wrapper(init: bool) -> None:
383396
self._master.after(int((ni := self._performance_checker.next_interval()) * 1000), wrapper, init)
384397
self._last_interval = ni
385398

399+
self._active = True
386400
self._master.after(1, wrapper, True)
387401
self._master.mainloop()
388402
self._active = False
389403

390-
def kill(self) -> None:
391-
self._master.destroy()
392-
393404

394405
class ContextManager(object):
395406
def __init__(self, *windows: Window) -> None:
396407
pot = None
397408
self._windows: dict[int, Window] = {}
398409
for window in windows:
399-
if window.is_pot():
410+
if isinstance(window, Pot):
400411
pot = window
401412
else:
402413
self.add_window(window)
403414
if not pot:
404415
raise LookupError("No root window")
405-
self._pot: Window = pot
416+
self._pot: Pot = pot
406417
self._widgets: dict[str, _Widget] = {}
407418

408419
def num_windows(self) -> int:
@@ -418,12 +429,13 @@ def _allocate_window(self) -> int:
418429

419430
def add_window(self, window: Window) -> int:
420431
self._windows[index := self._allocate_window()] = window
432+
window.show()
421433
return index
422434

423435
def remove_window(self, index: int) -> None:
424436
self._windows.pop(index).kill()
425437

426-
def index_of_window(self, window: Window) -> int:
438+
def index_of_window(self, window: Pot) -> int:
427439
for k, v in self._windows.items():
428440
if v == window:
429441
return k
@@ -466,7 +478,7 @@ def layout(self, layout: list[list[str | _Widget | None]], padding: float = .005
466478
widget.configure(width=screen_width)
467479
widget.grid(row=i, column=j * s, sticky="NSEW", columnspan=s, ipadx=p, ipady=p, padx=p, pady=p)
468480

469-
def window(self, index: int = -1) -> Window:
481+
def window(self, index: int = -1) -> Pot:
470482
return self._pot if index < 0 else self._windows[index]
471483

472484
def show(self) -> None:

leads_gui/speedometer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ def dynamic_renderer(self, canvas: CanvasBased) -> None:
6666
f"#{f"{int(0x4d + 0xb2 * p):02x}" * 3}"))
6767
canvas.collect("d0", canvas.create_arc(x - r, y - r, x + r, y + r, start=-30, extent=240, width=4,
6868
style=_ARC, outline=color))
69-
canvas.collect("d1", canvas.create_line(*(x, y) if self._style == 2 else (x - _cos(rad) * (r - 8),
69+
canvas.collect("d1", canvas.create_line((x, y) if self._style == 2 else (x - _cos(rad) * (r - 8),
7070
y - _sin(rad) * (r - 8)),
71-
x - _cos(rad) * (r + 8), y - _sin(rad) * (r + 8), width=4,
71+
(x - _cos(rad) * (r + 8), y - _sin(rad) * (r + 8)), width=4,
7272
fill=color))
7373
canvas.collect("d2", canvas.create_text(x, y * .95 if self._style == 1 else y + (r - font[1]) * .5,
7474
text=str(int(v)), fill=self._text_color, font=font))

leads_vec/benchmark.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from cv2 import VideoCapture, imencode, IMWRITE_JPEG_QUALITY, CAP_PROP_FPS
99

1010
from leads import L, require_config
11-
from leads_gui import RuntimeData, Window, ContextManager, Speedometer
11+
from leads_gui import RuntimeData, Pot, ContextManager, Speedometer
1212

1313

1414
def video_tester(container: Callable[[], None]) -> float:
@@ -61,7 +61,7 @@ def __init__(self) -> None:
6161
self.t: float = time()
6262
self.speed: DoubleVar | None = None
6363

64-
def on_refresh(self, window: Window) -> None:
64+
def on_refresh(self, window: Pot) -> None:
6565
self.speed.set((d := time() - self.t) * 20)
6666
if d > 10:
6767
window.kill()
@@ -72,7 +72,7 @@ def main() -> int:
7272
L.info("GUI test starting, this takes about 10 seconds")
7373
rd = RuntimeData()
7474
callbacks = Callbacks()
75-
w = Window(800, 256, 30, rd, callbacks.on_refresh, "Benchmark", no_title_bar=False)
75+
w = Pot(800, 256, 30, rd, callbacks.on_refresh, "Benchmark", no_title_bar=False)
7676
callbacks.speed = DoubleVar(w.root())
7777
uim = ContextManager(w)
7878
uim.layout([[CTkLabel(w.root(), text="Do NOT close the window", height=240),

leads_vec/cli.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
FRONT_VIEW_CAMERA, LEFT_VIEW_CAMERA, RIGHT_VIEW_CAMERA, SuspensionExitEvent
1616
from leads.comm import Callback, Service, start_server, create_server, my_ip_addresses, ConnectionBase
1717
from leads_audio import DIRECTION_INDICATOR_ON, DIRECTION_INDICATOR_OFF, WARNING, CONFIRM
18-
from leads_gui import RuntimeData, Window, GForceVar, FrequencyGenerator, Left, Color, Right, ContextManager, \
18+
from leads_gui import RuntimeData, Window, Pot, GForceVar, FrequencyGenerator, Left, Color, Right, ContextManager, \
1919
Typography, Speedometer, ProxyCanvas, SpeedTrendMeter, GForceMeter, Stopwatch, Hazard, initialize, Battery, Brake, \
2020
ESC, Satellite, Motor, Speed, Photo, Light, ImageVariable
2121
from leads_vec.__version__ import __version__
@@ -118,7 +118,7 @@ def on_receive(self, service: Service, msg: bytes) -> None:
118118
def add_secondary_window(context_manager: ContextManager, display: int, var_lap_times: _StringVar,
119119
var_speed: _DoubleVar, var_speed_trend: _DoubleVar) -> None:
120120
pot = context_manager.window()
121-
w = Window(0, 0, pot.refresh_rate(), pot.runtime_data(), fullscreen=True, display=display)
121+
w = Window(pot.root(), 0, 0, fullscreen=True, display=display)
122122
window_index = context_manager.add_window(w)
123123
num_widgets = int(w.width() / w.height())
124124
fonts = (("Arial", int(w.width() * .2)), ("Arial", int(w.width() * .1)), ("Arial", int(w.width() * .025)))
@@ -135,14 +135,22 @@ def toggle_debug_window(context_manager: ContextManager, var_debug: _StringVar)
135135
pot = context_manager.window()
136136
rd = pot.runtime_data()
137137
if rd.debug_window_index < 0:
138-
w = Window(pot.width(), pot.height(), pot.refresh_rate(), rd)
138+
w = Window(pot.root(), pot.width(), pot.height(), popup=True)
139139
rd.debug_window_index = context_manager.add_window(w)
140-
context_manager.layout([[Typography(w.root(), width=pot.width(), height=pot.height(), variable=var_debug,
141-
font=("Arial", int(pot.height() * .022)))]], 0,
142-
rd.debug_window_index)
143-
return
144-
context_manager.remove_window(rd.debug_window_index)
145-
rd.debug_window_index = -1
140+
context_manager.layout([
141+
[Typography(w.root(), width=pot.width(), height=pot.height() * .9, variable=var_debug,
142+
font=("Arial", int(pot.height() * .022)))],
143+
[_Button(w.root(), pot.width(), int(pot.height() * .1), text="CLOSE",
144+
command=lambda: toggle_debug_window(context_manager, var_debug))]
145+
], 0, rd.debug_window_index)
146+
else:
147+
context_manager.remove_window(rd.debug_window_index)
148+
rd.debug_window_index = -1
149+
150+
151+
def set_debug_window(context_manager: ContextManager, var_debug: _StringVar, status: bool) -> None:
152+
if status ^ context_manager.window().runtime_data().debug_window_index < 0:
153+
toggle_debug_window(context_manager, var_debug)
146154

147155

148156
def main() -> int:
@@ -153,8 +161,8 @@ def main() -> int:
153161
ctx.plugin(SystemLiteral.ABS, ABS())
154162
ctx.plugin(SystemLiteral.EBI, EBI())
155163
ctx.plugin(SystemLiteral.ATBS, ATBS())
156-
w = Window(cfg.width, cfg.height, cfg.refresh_rate, CustomRuntimeData(), fullscreen=cfg.fullscreen,
157-
no_title_bar=cfg.no_title_bar, theme_mode=cfg.theme_mode)
164+
w = Pot(cfg.width, cfg.height, cfg.refresh_rate, CustomRuntimeData(), fullscreen=cfg.fullscreen,
165+
no_title_bar=cfg.no_title_bar, theme_mode=cfg.theme_mode)
158166
root = w.root()
159167
root.configure(cursor="dot")
160168
var_lap_times = _StringVar(root, "")
@@ -262,7 +270,10 @@ def switch_esc_mode(mode: str) -> None:
262270
class IdleUpdate(FrequencyGenerator):
263271
@_override
264272
def do(self) -> None:
265-
cpu_temp = get_device("cpu").read()["temp"] if has_device("cpu") else "?"
273+
cpu_temp = get_device("cpu").read()["temp"] if has_device("cpu") else 0
274+
if cpu_temp > 90:
275+
L.warn("! CPU OVERHEATING, PULL OVER RIGHT NOW !")
276+
set_debug_window(uim, var_debug, True)
266277
var_info.set(f"VeC {__version__.upper()}\n\n"
267278
f"{_datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n"
268279
f"{format_duration(duration := _time() - w.runtime_data().start_time)} {cpu_temp} °C\n"

0 commit comments

Comments
 (0)