266 lines
9.7 KiB
Python
266 lines
9.7 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import logging.handlers
|
|
import sys
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtGui import QFont, QKeySequence, QShortcut
|
|
from PySide6.QtWidgets import (
|
|
QApplication,
|
|
QInputDialog,
|
|
QMainWindow,
|
|
QMenuBar,
|
|
QMessageBox,
|
|
QTabWidget,
|
|
QToolButton,
|
|
)
|
|
|
|
from ameba_control_panel import config
|
|
from ameba_control_panel.config import DeviceProfile
|
|
from ameba_control_panel import theme
|
|
from ameba_control_panel.controllers.device_tab_controller import DeviceTabController
|
|
from ameba_control_panel.services.session_store import SessionStore
|
|
from ameba_control_panel.services.settings_service import Settings
|
|
from ameba_control_panel.views.settings_dialog import SettingsDialog
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.setWindowTitle(f"{config.APP_NAME} v{config.APP_VERSION}")
|
|
self._tabs = QTabWidget()
|
|
self._tabs.setTabsClosable(True)
|
|
self._tabs.tabCloseRequested.connect(self._close_tab)
|
|
self._tabs.tabBarDoubleClicked.connect(self._rename_tab)
|
|
self.setCentralWidget(self._tabs)
|
|
|
|
self._session = SessionStore()
|
|
self._settings = Settings()
|
|
self._dut_controllers: list[DeviceTabController] = []
|
|
self._next_dut_num = 1
|
|
|
|
add_btn = QToolButton()
|
|
add_btn.setText("+")
|
|
add_btn.setToolTip("Add DUT tab (Ctrl+T)")
|
|
add_btn.clicked.connect(self._add_dut_tab_auto)
|
|
self._tabs.setCornerWidget(add_btn, Qt.TopLeftCorner)
|
|
|
|
self._current_palette = theme.LIGHT
|
|
|
|
self._build_menu_bar()
|
|
self._setup_shortcuts()
|
|
|
|
tab_list = self._session.get_tab_list()
|
|
if tab_list:
|
|
for entry in tab_list:
|
|
key = entry.get("key", "")
|
|
label = entry.get("label", "")
|
|
if key and label:
|
|
self._add_dut_tab(key, label)
|
|
else:
|
|
self._add_dut_tab("dut_1", "DUT 1")
|
|
|
|
def _build_menu_bar(self) -> None:
|
|
mb = self.menuBar()
|
|
|
|
view_menu = mb.addMenu("&View")
|
|
view_menu.addAction("New Tab", self._add_dut_tab_auto, QKeySequence("Ctrl+T"))
|
|
view_menu.addAction("Close Tab", self._close_current_tab, QKeySequence("Ctrl+W"))
|
|
view_menu.addSeparator()
|
|
view_menu.addAction("Clear Log", self._clear_current_log, QKeySequence("Ctrl+L"))
|
|
view_menu.addAction("Find", self._focus_find, QKeySequence("Ctrl+F"))
|
|
|
|
settings_menu = mb.addMenu("&Settings")
|
|
settings_menu.addAction("Preferences...", self._open_settings)
|
|
|
|
help_menu = mb.addMenu("&Help")
|
|
help_menu.addAction("About", self._show_about)
|
|
|
|
def _toggle_theme(self) -> None:
|
|
if self._current_palette.name == "light":
|
|
self._current_palette = theme.DARK
|
|
else:
|
|
self._current_palette = theme.LIGHT
|
|
QApplication.instance().setStyleSheet(theme.build_stylesheet(self._current_palette))
|
|
p = self._current_palette
|
|
for ctrl in self._dut_controllers:
|
|
ctrl.view.log_view.set_colors(p.log_rx, p.log_tx, p.log_info)
|
|
|
|
def _show_about(self) -> None:
|
|
QMessageBox.about(self, config.APP_NAME,
|
|
f"{config.APP_NAME}\n"
|
|
f"Version: {config.APP_VERSION}\n\n"
|
|
f"Author: Yiek Heng\n"
|
|
f"Email: wongyiekheng@realtek-sg.com\n\n"
|
|
f"Licensed under the Apache License, Version 2.0\n"
|
|
f"All rights reserved. For internal use only.")
|
|
|
|
def _setup_shortcuts(self) -> None:
|
|
QShortcut(QKeySequence("Ctrl+T"), self, activated=self._add_dut_tab_auto)
|
|
QShortcut(QKeySequence("Ctrl+W"), self, activated=self._close_current_tab)
|
|
QShortcut(QKeySequence("Ctrl+F"), self, activated=self._focus_find)
|
|
QShortcut(QKeySequence("Ctrl+L"), self, activated=self._clear_current_log)
|
|
QShortcut(QKeySequence("Ctrl+Return"), self, activated=self._send_current)
|
|
QShortcut(QKeySequence("Ctrl+Shift+F"), self, activated=self._flash_current)
|
|
QShortcut(QKeySequence("Ctrl+R"), self, activated=self._reset_current)
|
|
|
|
def _close_current_tab(self) -> None:
|
|
self._close_tab(self._tabs.currentIndex())
|
|
|
|
def _focus_find(self) -> None:
|
|
ctrl = self._current_controller()
|
|
if ctrl:
|
|
ctrl.view.find_input.setFocus()
|
|
ctrl.view.find_input.selectAll()
|
|
|
|
def _clear_current_log(self) -> None:
|
|
ctrl = self._current_controller()
|
|
if ctrl:
|
|
ctrl.log.clear()
|
|
|
|
def _send_current(self) -> None:
|
|
ctrl = self._current_controller()
|
|
if ctrl:
|
|
ctrl._send_from_input()
|
|
|
|
def _open_settings(self) -> None:
|
|
dlg = SettingsDialog(self._settings, self)
|
|
dlg.set_apply_callback(self._apply_settings)
|
|
dlg.exec()
|
|
self._apply_settings()
|
|
|
|
def _apply_settings(self) -> None:
|
|
font = QFont(self._settings.font_family)
|
|
font.setStyleHint(QFont.Monospace)
|
|
font.setPointSize(self._settings.font_size)
|
|
for ctrl in self._dut_controllers:
|
|
ctrl.view.log_view.setFont(font)
|
|
ctrl._port_timer.setInterval(self._settings.port_scan_interval_sec * 1000)
|
|
|
|
def _flash_current(self) -> None:
|
|
ctrl = self._current_controller()
|
|
if ctrl:
|
|
ctrl.flash.run_flash()
|
|
|
|
def _reset_current(self) -> None:
|
|
ctrl = self._current_controller()
|
|
if ctrl:
|
|
from ameba_control_panel.config import Mode
|
|
ctrl.flash.run_mode(Mode.RESET)
|
|
|
|
def _current_controller(self) -> DeviceTabController | None:
|
|
widget = self._tabs.currentWidget()
|
|
for ctrl in self._dut_controllers:
|
|
if ctrl.view is widget:
|
|
return ctrl
|
|
return None
|
|
|
|
def _add_dut_tab(self, key: str, label: str) -> DeviceTabController:
|
|
profile = DeviceProfile(key=key, label=label)
|
|
controller = DeviceTabController(profile)
|
|
self._dut_controllers.append(controller)
|
|
# Wire sidebar Settings/Theme buttons
|
|
controller.view.settings_btn.clicked.connect(self._open_settings)
|
|
controller.view.theme_btn.clicked.connect(self._toggle_theme)
|
|
self._tabs.addTab(controller.view, label)
|
|
try:
|
|
num = int(key.split("_")[-1])
|
|
if num >= self._next_dut_num:
|
|
self._next_dut_num = num + 1
|
|
except (ValueError, IndexError):
|
|
pass
|
|
self._persist_tab_list()
|
|
return controller
|
|
|
|
def _add_dut_tab_auto(self) -> None:
|
|
key = f"dut_{self._next_dut_num}"
|
|
label = f"DUT {self._next_dut_num}"
|
|
self._add_dut_tab(key, label)
|
|
self._tabs.setCurrentIndex(self._tabs.count() - 1)
|
|
|
|
def _rename_tab(self, index: int) -> None:
|
|
old_label = self._tabs.tabText(index)
|
|
new_label, ok = QInputDialog.getText(self, "Rename Tab", "Tab name:", text=old_label)
|
|
if not ok or not new_label.strip():
|
|
return
|
|
new_label = new_label.strip()
|
|
self._tabs.setTabText(index, new_label)
|
|
for ctrl in self._dut_controllers:
|
|
if ctrl.view is self._tabs.widget(index):
|
|
ctrl.profile = DeviceProfile(key=ctrl.profile.key, label=new_label)
|
|
break
|
|
self._persist_tab_list()
|
|
|
|
def _close_tab(self, index: int) -> None:
|
|
if len(self._dut_controllers) <= 1:
|
|
QMessageBox.information(self, "Close Tab", "Cannot close the last DUT tab.")
|
|
return
|
|
widget = self._tabs.widget(index)
|
|
for ctrl in self._dut_controllers:
|
|
if ctrl.view is widget:
|
|
ctrl.shutdown()
|
|
self._session.remove(ctrl.profile.key)
|
|
self._dut_controllers.remove(ctrl)
|
|
break
|
|
self._tabs.removeTab(index)
|
|
self._persist_tab_list()
|
|
|
|
def _persist_tab_list(self) -> None:
|
|
tabs = [{"key": c.profile.key, "label": c.profile.label} for c in self._dut_controllers]
|
|
self._session.set_tab_list(tabs)
|
|
|
|
def closeEvent(self, event) -> None: # noqa: N802
|
|
for c in self._dut_controllers:
|
|
c.shutdown()
|
|
self._session.save_now()
|
|
super().closeEvent(event)
|
|
|
|
|
|
def _setup_logging() -> None:
|
|
log_dir = config.app_data_dir()
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
handler = logging.handlers.RotatingFileHandler(
|
|
log_dir / "app.log", maxBytes=2_000_000, backupCount=3, encoding="utf-8",
|
|
)
|
|
handler.setFormatter(logging.Formatter(
|
|
"%(asctime)s %(name)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S",
|
|
))
|
|
root = logging.getLogger()
|
|
root.setLevel(logging.DEBUG)
|
|
root.addHandler(handler)
|
|
logging.getLogger(__name__).info("App started v%s", config.APP_VERSION)
|
|
|
|
|
|
def _setup_linux_platform() -> None:
|
|
"""Ensure correct Qt platform plugin on Linux."""
|
|
import os
|
|
if sys.platform != "linux":
|
|
return
|
|
# Disable xdg-desktop-portal and GVfs UDisks2 volume monitor to prevent
|
|
# file dialog crashes / hangs caused by D-Bus service timeouts.
|
|
os.environ.setdefault("QT_DISABLE_XDG_DESKTOP_PORTAL", "1")
|
|
os.environ.setdefault("GIO_USE_VOLUME_MONITOR", "unix")
|
|
if os.environ.get("QT_QPA_PLATFORM"):
|
|
return
|
|
if os.environ.get("WAYLAND_DISPLAY"):
|
|
os.environ["QT_QPA_PLATFORM"] = "wayland;xcb"
|
|
elif os.environ.get("DISPLAY"):
|
|
os.environ["QT_QPA_PLATFORM"] = "xcb"
|
|
|
|
|
|
def main() -> None:
|
|
_setup_linux_platform()
|
|
_setup_logging()
|
|
QApplication.setStyle("Fusion")
|
|
app = QApplication(sys.argv)
|
|
app.setStyleSheet(theme.build_stylesheet(theme.LIGHT))
|
|
window = MainWindow()
|
|
window.resize(1200, 800)
|
|
window.show()
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|