Skip to content

Commit adcc50a

Browse files
replace tkinter with pyqt6
1 parent a8bea41 commit adcc50a

27 files changed

Lines changed: 926 additions & 664 deletions
672 KB
Binary file not shown.

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ pyserial==3.5
22
requests==2.32.5
33
adafruit-circuitpython-pn532
44
RPi.GPIO; sys_platform == 'linux'
5+
pyqt6==6.11.0
6+
qtawesome

src/app_context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def __init__(self, sheets: ApiClient, traffic_light: TrafficLightApi):
1313
self.nav = None
1414
self.check_in = None
1515
self.account = None
16+
self.dispatcher = None # set by main.py after QApplication is created
1617
self._rfid_lock = threading.Lock()
1718
self._rfid: str = ""
1819

src/controllers/account_controller.py

Lines changed: 18 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,23 @@
1-
import tkinter
21
import logging
32

3+
from PyQt6.QtCore import QTimer
4+
from PyQt6.QtWidgets import QApplication
5+
46

57
class AccountController:
68
def __init__(self, ctx):
79
self.ctx = ctx
810

9-
def create_account_from_barcode(self, barcode):
10-
self._create(barcode=barcode)
11-
1211
def go_to_review_from_barcode(self, barcode):
13-
canvas = self.ctx.window.canvas
14-
loading = tkinter.Label(
15-
canvas,
16-
text="Looking up student...",
17-
bg="#153246", fg="white", font=("Arial", 25),
18-
)
19-
loading.place(relx=0.5, rely=0.87, anchor="center")
20-
self.ctx.window.update()
12+
self.ctx.nav.show_status("Looking up student...")
13+
QApplication.processEvents()
2114

2215
student = self.ctx.sheets.lookup_by_barcode(barcode)
23-
loading.destroy()
16+
self.ctx.nav.hide_status()
2417

2518
if student is None:
26-
error = tkinter.Label(
27-
canvas,
28-
text="Student not found. Please enter your details manually.",
29-
bg="#153246", fg="white", font=("Arial", 20),
30-
)
31-
error.place(relx=0.5, rely=0.87, anchor="center")
32-
error.after(3000, error.destroy)
19+
self.ctx.nav.show_status("Student not found. Please enter your details manually.")
20+
QTimer.singleShot(3000, self.ctx.nav.hide_status)
3321
return
3422

3523
self.ctx.nav.go_to_create_account_review(
@@ -39,30 +27,16 @@ def go_to_review_from_barcode(self, barcode):
3927
email=student["email"],
4028
)
4129

42-
def create_account_from_pid(self, pid):
43-
self._create(pid=pid)
44-
4530
def go_to_review_from_pid(self, pid):
46-
canvas = self.ctx.window.canvas
47-
loading = tkinter.Label(
48-
canvas,
49-
text="Looking up student...",
50-
bg="#153246", fg="white", font=("Arial", 25),
51-
)
52-
loading.place(relx=0.5, rely=0.87, anchor="center")
53-
self.ctx.window.update()
31+
self.ctx.nav.show_status("Looking up student...")
32+
QApplication.processEvents()
5433

5534
student = self.ctx.sheets.lookup_by_pid(pid)
56-
loading.destroy()
35+
self.ctx.nav.hide_status()
5736

5837
if student is None:
59-
error = tkinter.Label(
60-
canvas,
61-
text="Student not found. Please check your PID.",
62-
bg="#153246", fg="white", font=("Arial", 20),
63-
)
64-
error.place(relx=0.5, rely=0.87, anchor="center")
65-
error.after(3000, error.destroy)
38+
self.ctx.nav.show_status("Student not found. Please check your PID.")
39+
QTimer.singleShot(3000, self.ctx.nav.hide_status)
6640
return
6741

6842
self.ctx.nav.go_to_create_account_review(
@@ -79,14 +53,8 @@ def create_account_from_review(self, *, first_name, last_name, email, pid):
7953
self._create(first_name=first_name, last_name=last_name, email=email)
8054

8155
def _create(self, *, barcode=None, pid=None, first_name=None, last_name=None, email=None):
82-
canvas = self.ctx.window.canvas
83-
inProgress = tkinter.Label(
84-
canvas,
85-
text="Account creation in progress!",
86-
bg="#153246", fg="white", font=("Arial", 25),
87-
)
88-
inProgress.place(relx=0.5, rely=0.87, anchor="center")
89-
self.ctx.window.update()
56+
self.ctx.nav.show_status("Account creation in progress!")
57+
QApplication.processEvents()
9058

9159
result = self.ctx.sheets.create_account(
9260
self.ctx.rfid,
@@ -96,16 +64,11 @@ def _create(self, *, barcode=None, pid=None, first_name=None, last_name=None, em
9664
last_name=last_name,
9765
email=email,
9866
)
99-
inProgress.destroy()
67+
self.ctx.nav.hide_status()
10068

10169
if result is None:
102-
error = tkinter.Label(
103-
canvas,
104-
text="ERROR! Could not create account, please try manually.",
105-
bg="#153246", fg="white", font=("Arial", 20),
106-
)
107-
error.place(relx=0.5, rely=0.87, anchor="center")
108-
error.after(3000, lambda: error.destroy())
70+
self.ctx.nav.show_status("ERROR! Could not create account, please try manually.")
71+
QTimer.singleShot(3000, self.ctx.nav.hide_status)
10972
return
11073

11174
logging.info("Account creation succeeded")

src/controllers/barcode_scanner_controller.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,13 @@ def _run(self, scanner):
4747
curr_frame = self.ctx.nav.get_curr_frame()
4848

4949
if curr_frame == CheckInManual:
50-
self.ctx.window.after(0, lambda b=barcode: self.ctx.check_in.handle_by_pid(b))
50+
self.ctx.dispatcher.call.emit(
51+
lambda b=barcode: self.ctx.check_in.handle_by_pid(b)
52+
)
5153
elif curr_frame in (CreateAccountBarcode, CreateAccountManual):
52-
self.ctx.window.after(0, lambda b=barcode: self.ctx.account.go_to_review_from_barcode(b))
54+
self.ctx.dispatcher.call.emit(
55+
lambda b=barcode: self.ctx.account.go_to_review_from_barcode(b)
56+
)
5357
else:
5458
logging.debug("Barcode scanned on unhandled screen: %s", curr_frame)
5559
except Exception as e:

src/controllers/check_in_controller.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
2-
from tkinter import Label
2+
3+
from PyQt6.QtCore import QTimer
34

45
from screens.user_welcome import UserWelcome
56

@@ -9,12 +10,13 @@ def __init__(self, ctx):
910
self.ctx = ctx
1011

1112
def handle_by_uuid(self, tag):
12-
# Called from background thread — defer to main thread.
13-
self.ctx.window.after(
14-
0, lambda: self._run_check_in(tag, self.ctx.sheets.checkin_by_uuid)
13+
# Called from background thread — dispatch to main thread via signal.
14+
self.ctx.dispatcher.call.emit(
15+
lambda: self._run_check_in(tag, self.ctx.sheets.checkin_by_uuid)
1516
)
1617

1718
def handle_by_pid(self, pid):
19+
# Called on main thread (button click or barcode dispatcher).
1820
self._run_check_in(pid, self.ctx.sheets.checkin_by_pid)
1921

2022
def _run_check_in(self, identifier, check_fn, welcome_message="Welcome back"):
@@ -24,13 +26,8 @@ def _run_check_in(self, identifier, check_fn, welcome_message="Welcome back"):
2426
if status == "api_error":
2527
logging.error("API error during check-in")
2628
self.ctx.traffic_light.request_red()
27-
error_label = Label(
28-
self.ctx.window.canvas,
29-
text="System error, please let staff know.",
30-
bg="#153246", fg="white", font=("Arial", 25),
31-
)
32-
error_label.place(relx=0.5, rely=0.1, anchor="center")
33-
error_label.after(4000, error_label.destroy)
29+
self.ctx.nav.show_status("System error, please let staff know.")
30+
QTimer.singleShot(4000, self.ctx.nav.hide_status)
3431
return
3532

3633
if status == "no_account":

src/controllers/navigation_controller.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import uuid
22

3+
from PyQt6.QtCore import QTimer, Qt
4+
from PyQt6.QtWidgets import QLabel
5+
36
from screens.check_in_rfid import CheckInRFID
47
from screens.transition_screen import TransitionScreen
58
from screens.create_account_barcode import CreateAccountBarcode
@@ -16,6 +19,7 @@ class NavigationController:
1619
def __init__(self, window, ctx, dev_mode=False):
1720
self.ctx = ctx
1821
self._window = window
22+
self._stacked = window.stacked
1923
self._frames = {}
2024
self._curr = None
2125
self._frame_uuid = uuid.uuid4().hex
@@ -39,11 +43,27 @@ def __init__(self, window, ctx, dev_mode=False):
3943
QRCodes,
4044
UserWelcome,
4145
):
42-
self._frames[F] = F(window.canvas, self)
46+
frame = F(self)
47+
self._frames[F] = frame
48+
self._stacked.addWidget(frame)
49+
50+
# Status overlay — floats over the stacked widget at the bottom
51+
self._status_label = QLabel("", window.central)
52+
self._status_label.setGeometry(40, 628, 1200, 56)
53+
self._status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
54+
self._status_label.setStyleSheet(
55+
"color: #F5F0E6;"
56+
"font: bold 18pt Montserrat;"
57+
"background-color: rgba(0, 0, 0, 170);"
58+
"border-radius: 10px;"
59+
"border: none;"
60+
)
61+
self._status_label.hide()
62+
self._status_label.raise_()
4363

4464
if dev_mode:
4565
from screens.components.dev_overlay import DevOverlay
46-
self._dev_overlay = DevOverlay(window.canvas, self)
66+
self._dev_overlay = DevOverlay(window, self)
4767

4868
self.show_frame(CheckInRFID)
4969

@@ -53,17 +73,18 @@ def __init__(self, window, ctx, dev_mode=False):
5373

5474
def show_frame(self, screen_class):
5575
if self._curr is not None:
56-
self._frames[self._curr].hide()
76+
self._frames[self._curr].on_hide()
5777
self._curr = screen_class
5878
self._frame_uuid = uuid.uuid4().hex
59-
self._frames[screen_class].show()
79+
self._stacked.setCurrentWidget(self._frames[screen_class])
80+
self._frames[screen_class].on_show()
6081

6182
if self._dev_overlay is not None:
6283
self._dev_overlay.update(screen_class)
6384

6485
if screen_class in self._timeouts:
6586
uid = self._frame_uuid
66-
self._window.after(
87+
QTimer.singleShot(
6788
self._timeouts[screen_class],
6889
lambda: self._on_timeout(uid),
6990
)
@@ -75,19 +96,29 @@ def get_curr_frame(self):
7596
return self._curr
7697

7798
def after(self, ms, fn):
78-
self._window.after(ms, fn)
99+
QTimer.singleShot(ms, fn)
100+
101+
# ------------------------------------------------------------------
102+
# Status overlay
103+
# ------------------------------------------------------------------
104+
105+
def show_status(self, text):
106+
self._status_label.setText(text)
107+
self._status_label.show()
108+
self._status_label.raise_()
109+
110+
def hide_status(self):
111+
self._status_label.hide()
79112

80113
# ------------------------------------------------------------------
81114
# Stack-based flow
82115
# ------------------------------------------------------------------
83116

84117
def push(self, screen_class, on_done=None):
85-
"""Show screen_class and register a continuation to run when pop() is called."""
86118
self._on_done_stack.append(on_done)
87119
self.show_frame(screen_class)
88120

89121
def pop(self):
90-
"""Signal that the current screen is done; run the stored continuation."""
91122
cb = self._on_done_stack.pop() if self._on_done_stack else None
92123
if cb:
93124
cb()
@@ -131,13 +162,13 @@ def go_to_create_account(self, on_done):
131162
self.get_frame(TransitionScreen).display(
132163
"Looks like you don't have an account,\nlet's set one up!"
133164
)
134-
self._window.after(3000, lambda: self.push(CreateAccountBarcode, on_done=on_done))
165+
QTimer.singleShot(3000, lambda: self.push(CreateAccountBarcode, on_done=on_done))
135166

136167
def go_to_sign_waiver(self):
137168
self.get_frame(TransitionScreen).display(
138169
"Looks like you haven't signed\nthe waiver yet,\nlet's fix that!"
139170
)
140-
self._window.after(3000, lambda: self.show_frame(SignWaiver))
171+
QTimer.singleShot(3000, lambda: self.show_frame(SignWaiver))
141172

142173
# ------------------------------------------------------------------
143174
# Internal

src/controllers/rfid_reader_controller.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import time
22
import socket
33
import logging
4-
from tkinter import Label
54
from threading import Thread
5+
6+
from PyQt6.QtCore import QTimer
7+
68
from screens.create_account_manual import CreateAccountManual
79

810

@@ -44,16 +46,12 @@ def _run(self, reader):
4446
logging.info("ERROR wifi is not connected")
4547
if not self._no_wifi_shown:
4648
self._no_wifi_shown = True
47-
no_wifi = Label(
48-
self.ctx.window.canvas,
49-
text="ERROR! Connection cannot be established, please let staff know.",
50-
bg="#153246", fg="white", font=("Arial", 25),
51-
)
52-
no_wifi.place(relx=0.5, rely=0.1, anchor="center")
53-
no_wifi.after(4000, lambda: self._destroy_wifi_error(no_wifi))
49+
self.ctx.dispatcher.call.emit(self._show_wifi_error)
5450
continue
5551

56-
self.ctx.nav.get_frame(CreateAccountManual).clear_entries()
52+
self.ctx.dispatcher.call.emit(
53+
lambda: self.ctx.nav.get_frame(CreateAccountManual).clear_entries()
54+
)
5755
tag = reader.grab_rfid()
5856

5957
if " " in tag:
@@ -86,14 +84,20 @@ def _poll_traffic_light(self):
8684
last_color = color
8785
self.ctx.traffic_light.drive(color)
8886

87+
def _show_wifi_error(self):
88+
self.ctx.nav.show_status(
89+
"ERROR! Connection cannot be established, please let staff know."
90+
)
91+
QTimer.singleShot(4000, self._clear_wifi_error)
92+
93+
def _clear_wifi_error(self):
94+
self.ctx.nav.hide_status()
95+
self._no_wifi_shown = False
96+
8997
def _is_connected(self, host="8.8.8.8", port=53, timeout=3):
9098
try:
9199
socket.setdefaulttimeout(timeout)
92100
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
93101
return True
94102
except socket.error:
95103
return False
96-
97-
def _destroy_wifi_error(self, label):
98-
label.destroy()
99-
self._no_wifi_shown = False

src/dispatcher.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from PyQt6.QtCore import QObject, pyqtSignal
2+
3+
4+
class MainThreadDispatcher(QObject):
5+
call = pyqtSignal(object)
6+
7+
def __init__(self):
8+
super().__init__()
9+
self.call.connect(lambda fn: fn())

0 commit comments

Comments
 (0)