-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathmain_qml.py
More file actions
176 lines (139 loc) · 6 KB
/
main_qml.py
File metadata and controls
176 lines (139 loc) · 6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""
Mouser — QML Entry Point
==============================
Launches the Qt Quick / QML UI with PySide6.
Replaces the old tkinter-based main.py.
Run with: python main_qml.py
"""
import time as _time
_t0 = _time.perf_counter() # ◄ startup clock
import sys
import os
import signal
# Ensure project root on path — works for both normal Python and PyInstaller
if getattr(sys, "frozen", False):
# PyInstaller 6.x: data files are in _internal/ next to the exe
ROOT = os.path.join(os.path.dirname(sys.executable), "_internal")
else:
ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT)
# Set Material theme before any Qt imports
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
os.environ["QT_QUICK_CONTROLS_MATERIAL_THEME"] = "Dark"
os.environ["QT_QUICK_CONTROLS_MATERIAL_ACCENT"] = "#00d4aa"
_t1 = _time.perf_counter()
from PySide6.QtWidgets import QApplication, QSystemTrayIcon, QMenu
from PySide6.QtGui import QIcon, QAction
from PySide6.QtCore import Qt, QUrl, QCoreApplication
from PySide6.QtQml import QQmlApplicationEngine
_t2 = _time.perf_counter()
# Ensure PySide6 QML plugins are found
import PySide6
_pyside_dir = os.path.dirname(PySide6.__file__)
os.environ.setdefault("QML2_IMPORT_PATH", os.path.join(_pyside_dir, "qml"))
os.environ.setdefault("QT_PLUGIN_PATH", os.path.join(_pyside_dir, "plugins"))
_t3 = _time.perf_counter()
from core.engine import Engine
from ui.backend import Backend
_t4 = _time.perf_counter()
def _print_startup_times():
print(f"[Startup] Env setup: {(_t1-_t0)*1000:7.1f} ms")
print(f"[Startup] PySide6 imports: {(_t2-_t1)*1000:7.1f} ms")
print(f"[Startup] Core imports: {(_t4-_t3)*1000:7.1f} ms")
print(f"[Startup] Total imports: {(_t4-_t0)*1000:7.1f} ms")
def _app_icon() -> QIcon:
"""Load the app icon from the pre-cropped .ico file."""
ico = os.path.join(ROOT, "images", "logo.ico")
return QIcon(ico)
def main():
_print_startup_times()
_t5 = _time.perf_counter()
QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_ShareOpenGLContexts)
app = QApplication(sys.argv)
app.setApplicationName("Mouser")
app.setOrganizationName("Mouser")
app.setWindowIcon(_app_icon())
# macOS: allow Ctrl+C in terminal to quit the app
signal.signal(signal.SIGINT, signal.SIG_DFL)
if sys.platform == "darwin":
# SIGUSR1 thread dump (useful for debugging on macOS)
import traceback
def _dump_threads(sig, frame):
import threading
for t in threading.enumerate():
print(f"\n--- {t.name} ---")
if t.ident:
traceback.print_stack(sys._current_frames().get(t.ident))
signal.signal(signal.SIGUSR1, _dump_threads)
_t6 = _time.perf_counter()
# ── Engine (created but started AFTER UI is visible) ───────
engine = Engine()
_t7 = _time.perf_counter()
# ── QML Backend ────────────────────────────────────────────
backend = Backend(engine)
# ── QML Engine ─────────────────────────────────────────────
qml_engine = QQmlApplicationEngine()
qml_engine.rootContext().setContextProperty("backend", backend)
qml_engine.rootContext().setContextProperty(
"applicationDirPath", ROOT.replace("\\", "/"))
qml_path = os.path.join(ROOT, "ui", "qml", "Main.qml")
qml_engine.load(QUrl.fromLocalFile(qml_path))
_t8 = _time.perf_counter()
if not qml_engine.rootObjects():
print("[Mouser] FATAL: Failed to load QML")
sys.exit(1)
root_window = qml_engine.rootObjects()[0]
print(f"[Startup] QApp create: {(_t6-_t5)*1000:7.1f} ms")
print(f"[Startup] Engine create: {(_t7-_t6)*1000:7.1f} ms")
print(f"[Startup] QML load: {(_t8-_t7)*1000:7.1f} ms")
print(f"[Startup] TOTAL to window: {(_t8-_t0)*1000:7.1f} ms")
# ── Start engine AFTER window is ready (deferred) ──────────
from PySide6.QtCore import QTimer
QTimer.singleShot(0, lambda: (
engine.start(),
print("[Mouser] Engine started — remapping is active"),
))
# ── System Tray ────────────────────────────────────────────
tray = QSystemTrayIcon(_app_icon(), app)
tray.setToolTip("Mouser — MX Master 3S")
tray_menu = QMenu()
open_action = QAction("Open Settings", tray_menu)
open_action.triggered.connect(lambda: (
root_window.show(),
root_window.raise_(),
root_window.requestActivate(),
))
tray_menu.addAction(open_action)
toggle_action = QAction("Disable Remapping", tray_menu)
def toggle_remapping():
enabled = not engine._enabled
engine.set_enabled(enabled)
toggle_action.setText(
"Disable Remapping" if enabled else "Enable Remapping")
toggle_action.triggered.connect(toggle_remapping)
tray_menu.addAction(toggle_action)
tray_menu.addSeparator()
quit_action = QAction("Quit Mouser", tray_menu)
def quit_app():
engine.hook.stop()
engine._app_detector.stop()
tray.hide()
app.quit()
quit_action.triggered.connect(quit_app)
tray_menu.addAction(quit_action)
tray.setContextMenu(tray_menu)
tray.activated.connect(lambda reason: (
root_window.show(),
root_window.raise_(),
root_window.requestActivate(),
) if reason == QSystemTrayIcon.ActivationReason.DoubleClick else None)
tray.show()
# ── Run ────────────────────────────────────────────────────
try:
sys.exit(app.exec())
finally:
engine.hook.stop()
engine._app_detector.stop()
print("[Mouser] Shut down cleanly")
if __name__ == "__main__":
main()