diff --git a/app/init.py b/app/init.py index 12d6b66..88854cd 100644 --- a/app/init.py +++ b/app/init.py @@ -1,9 +1,14 @@ ''' app/init.py ''' import sys +from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QApplication from .ui.main_window import MainWindow from .utility.config import AppConfig +from . import resources +# This is necessary to set the taskbar icon +import ctypes +ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(u'n203.cpdctrl-gui.1') def run() -> int: """ @@ -13,6 +18,7 @@ def run() -> int: int: The exit status code. """ app: QApplication = QApplication(sys.argv) + app.setWindowIcon(QIcon(":/icons/cpdctrl-gui-logo")) AppConfig.initialize() window: MainWindow = MainWindow() @@ -21,3 +27,58 @@ def run() -> int: print("Saving configuration") AppConfig.finalize() return sys.exit(exitcode) + + +import sys +import traceback +import logging +from PyQt6 import QtCore, QtWidgets + +# This is taken from: +# https://timlehr.com/2018/01/python-exception-hooks-with-qt-message-box/index.html +# basic logger functionality +log = logging.getLogger(__name__) +handler = logging.StreamHandler(stream=sys.stdout) +log.addHandler(handler) + +def show_exception_box(log_msg): + """Checks if a QApplication instance is available and shows a messagebox with the exception message. + If unavailable (non-console application), log an additional notice. + """ + if QtWidgets.QApplication.instance() is not None: + errorbox = QtWidgets.QMessageBox() + errorbox.setText("Oops. An unexpected error occured:\n{0}".format(log_msg)) + errorbox.exec() + else: + log.debug("No QApplication instance available.") + +class UncaughtHook(QtCore.QObject): + _exception_caught = QtCore.pyqtSignal(object) + + def __init__(self, *args, **kwargs): + super(UncaughtHook, self).__init__(*args, **kwargs) + + # this registers the exception_hook() function as hook with the Python interpreter + sys.excepthook = self.exception_hook + + # connect signal to execute the message box function always on main thread + self._exception_caught.connect(show_exception_box) + + def exception_hook(self, exc_type, exc_value, exc_traceback): + """Function handling uncaught exceptions. + It is triggered each time an uncaught exception occurs. + """ + if issubclass(exc_type, KeyboardInterrupt): + # ignore keyboard interrupt to support console applications + sys.__excepthook__(exc_type, exc_value, exc_traceback) + else: + exc_info = (exc_type, exc_value, exc_traceback) + log_msg = '\n'.join([''.join(traceback.format_tb(exc_traceback)), + '{0}: {1}'.format(exc_type.__name__, exc_value)]) + log.critical("Uncaught exception:\n {0}".format(log_msg), exc_info=exc_info) + + # trigger message box show + self._exception_caught.emit(log_msg) + +# create a global instance of our class to register the hook +qt_exception_hook = UncaughtHook() diff --git a/app/ui/main_window.py b/app/ui/main_window.py index ef899ab..caf0c58 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -119,6 +119,7 @@ class MainWindow(QMainWindow): """ # return TreeView(self) + # LED DEVICE MANAGEMENT def leddev_connect(self, leddev_type, leddev_name): self.leddev = ledd.connect_device(leddev_type, leddev_name) AppConfig.MAIN_CFG.set("led_device_last.type", leddev_type) @@ -137,12 +138,13 @@ class MainWindow(QMainWindow): and then connects to the device. """ devices = ledd.list_devices() - device_dialog = ListChoice(devices) + device_dialog = ListChoice(devices, window_title="Select LED device") if device_dialog.exec() == QDialog.DialogCode.Accepted: leddev_type, leddev_name = device_dialog.get_selected() if self.verbose: print(f"Connecting to {leddev_type}:{leddev_name}") self.leddev_connect(leddev_type, leddev_name) + # VOLTAGE DEVICE MANAGEMENT def vmdev_connect(self, vmdev_type, vmdev_name): self.vmdev = vmd.connect_device(vmdev_type, vmdev_name) AppConfig.MAIN_CFG.set("voltage_measurement_device_last.type", vmdev_type) @@ -164,12 +166,13 @@ class MainWindow(QMainWindow): and then connects to the device. """ devices = vmd.list_devices() - device_dialog = ListChoice(devices) + device_dialog = ListChoice(devices, window_title="Select CPD Measurement Device") if device_dialog.exec() == QDialog.DialogCode.Accepted: vmdev_type, vmdev_name = device_dialog.get_selected() if self.verbose: print(f"Connecting to {vmdev_type}:{vmdev_name}") self.vmdev_connect(vmdev_type, vmdev_name) + # MEASUREMENT def measure_start(self): if self.vmdev is None: self.vmdev_connect_from_dialog() @@ -187,8 +190,8 @@ class MainWindow(QMainWindow): self.topbar.enable_button("meas_stop") self.w_plot.clear_data() - script = 100 measurement_name = "guitest" + script = self.w_measurement_settings.get_value("led_script") led_script = LedScript(script=script) flush_after = self.w_measurement_settings.get_value("flush_after") use_buffer = self.w_measurement_settings.get_value("use_buffer") @@ -294,4 +297,5 @@ class MainWindow(QMainWindow): """ def __del__(self): - self.measure_stop() + if self.measurement_is_running(): + self.measure_stop() diff --git a/app/ui/widgets/measurement_settings.py b/app/ui/widgets/measurement_settings.py index 4874bd3..431d870 100644 --- a/app/ui/widgets/measurement_settings.py +++ b/app/ui/widgets/measurement_settings.py @@ -1,5 +1,5 @@ from PyQt6.QtWidgets import QWidget, QRadioButton, QVBoxLayout, QHBoxLayout, QPushButton, QSpinBox, QFileDialog, QLabel -from PyQt6.QtWidgets import QFormLayout, QDoubleSpinBox, QCheckBox +from PyQt6.QtWidgets import QFormLayout, QDoubleSpinBox, QCheckBox, QLineEdit from ...utility.config import AppConfig @@ -15,55 +15,59 @@ class ScriptSelection(QWidget): self.radio_constant_value.toggled.connect(self.on_radio_button_toggled) # Load from file button - self.load_button = QPushButton("Load from file") - self.load_button.clicked.connect(self.load_file) + self.btn_load_script = QPushButton("Load from file") + self.btn_load_script.clicked.connect(self.load_file) + self.w_script_file = QLineEdit() + self.w_script_file.setEnabled(False) # QSpinBox for constant value - self.spin_box = QSpinBox() - self.spin_box.setRange(0, 100) + self.w_constant_value = QSpinBox() + self.w_constant_value.setRange(0, 100) - # Layout for radio buttons - radio_layout = QHBoxLayout() - radio_layout.addWidget(self.radio_script_file) - radio_layout.addWidget(self.radio_constant_value) + # Layouts + l_constant_value = QVBoxLayout() + l_constant_value.addWidget(self.radio_constant_value) + l_constant_value.addWidget(self.w_constant_value) - # Layout for load button and spin box - input_layout = QHBoxLayout() - input_layout.addWidget(self.load_button) - input_layout.addWidget(self.spin_box) + l_file = QVBoxLayout() + l_file.addWidget(self.radio_script_file) + l_file.addWidget(self.btn_load_script) + l_file.addWidget(self.w_script_file) # Add layouts to main layout - self.layout.addLayout(radio_layout) - self.layout.addLayout(input_layout) + self.layout.addLayout(l_constant_value) + self.layout.addLayout(l_file) self.setLayout(self.layout) # Initial state - self.radio_script_file.setChecked(True) + self.radio_constant_value.setChecked(True) self.on_radio_button_toggled() self.layout.addStretch(1) + self.file_path = None def on_radio_button_toggled(self): if self.radio_script_file.isChecked(): - self.load_button.setEnabled(True) - self.spin_box.setEnabled(False) + self.btn_load_script.setEnabled(True) + self.w_constant_value.setEnabled(False) else: - self.load_button.setEnabled(False) - self.spin_box.setEnabled(True) + self.btn_load_script.setEnabled(False) + self.w_constant_value.setEnabled(True) def load_file(self): # options = QFileDialog.Options() - file_name, _ = QFileDialog.getOpenFileName(self, "Open Script File", "", "All Files (*);;Text files (*.led)") - if file_name: - with open(file_name, 'r') as file: - self.file_content = file.read() + file_path, _ = QFileDialog.getOpenFileName(self, "Open Script File", "", "All Files (*);;Text files (*.led)") + if file_path: + self.file_path = file_path + self.w_script_file.setText(self.file_path) + def get_script(self): if self.radio_script_file.isChecked(): - return self.file_content + return self.file_path else: - return self.spin_box.value() + return int(self.w_constant_value.value()) class MeasurementSettings(QWidget): @@ -81,7 +85,9 @@ class MeasurementSettings(QWidget): self.l_form = QFormLayout() # - script - self.l_vbox.addWidget(ScriptSelection()) + self.l_vbox.addWidget(QLabel("LED Script")) + self.w_led_script = ScriptSelection() + self.l_vbox.addWidget(self.w_led_script) # key-value stuff in a form self.l_vbox.addLayout(self.l_form) self.ws_form = {} @@ -90,6 +96,7 @@ class MeasurementSettings(QWidget): self._add_form_field("stop_on_script_end", "Stop on Script End", False, QCheckBox(self), "Stop measurement when LED script ends") self._add_form_field("use_buffer", "Use Buffer", False, QCheckBox(self), "If available, use device buffer for more accurate measurement timings.\nLeads to a lower accuracy of LED update timings, up to 1*interval") self._add_form_field("flush_after", "Flush after", 0, QSpinBox(self), "Number of measurements to take before writing the data to an intermediate file") + self.l_vbox.addStretch(1) def _add_form_field(self, key: str, label: str, default_value, widget: QWidget, tooltip: str = None): if tooltip: widget.setToolTip(tooltip) @@ -131,6 +138,8 @@ class MeasurementSettings(QWidget): return self.ws_form[key].isChecked() else: raise ValueError(f"Unknown widget type: {type(self.ws_form[key])}") + elif key == "led_script": + return self.w_led_script.get_script() else: raise ValueError(f"Unknown key: {key}")