Source code for qudi.core.gui.gui

# -*- coding: utf-8 -*-
"""
This file contains the Qudi console app class.

Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this
distribution and on <https://github.com/Ulm-IQO/qudi-core/>

This file is part of qudi.

Qudi is free software: you can redistribute it and/or modify it under the terms of
the GNU Lesser General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.

Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with qudi.
If not, see <https://www.gnu.org/licenses/>.
"""

import os
import weakref
import platform
from PySide2 import QtCore, QtGui, QtWidgets
from qudi.core.gui.main_gui.main_gui import QudiMainGui
from qudi.core.modulemanager import ModuleManager
from qudi.util.paths import get_artwork_dir
from qudi.core.logger import get_logger

try:
    import pyqtgraph as pg
except ImportError:
    pg = None

logger = get_logger(__name__)


[docs] class SystemTrayIcon(QtWidgets.QSystemTrayIcon): """Tray icon class subclassing QSystemTrayIcon for custom functionality. """
[docs] def __init__(self): """Tray icon constructor. Adds all the appropriate menus and actions. """ super().__init__() self._actions = dict() self.setIcon(QtWidgets.QApplication.instance().windowIcon()) self.right_menu = QtWidgets.QMenu('Quit') self.left_menu = QtWidgets.QMenu('Manager') iconpath = os.path.join(get_artwork_dir(), 'icons') self.managericon = QtGui.QIcon() self.managericon.addFile(os.path.join(iconpath, 'go-home'), QtCore.QSize(16, 16)) self.managerAction = QtWidgets.QAction(self.managericon, 'Manager', self.left_menu) self.exiticon = QtGui.QIcon() self.exiticon.addFile(os.path.join(iconpath, 'application-exit'), QtCore.QSize(16, 16)) self.quitAction = QtWidgets.QAction(self.exiticon, 'Quit', self.right_menu) self.restarticon = QtGui.QIcon() self.restarticon.addFile(os.path.join(iconpath, 'view-refresh'), QtCore.QSize(16, 16)) self.restartAction = QtWidgets.QAction(self.restarticon, 'Restart', self.right_menu) self.left_menu.addAction(self.managerAction) self.left_menu.addSeparator() self.right_menu.addAction(self.quitAction) self.right_menu.addAction(self.restartAction) self.setContextMenu(self.right_menu) self.activated.connect(self.handle_activation)
@QtCore.Slot(QtWidgets.QSystemTrayIcon.ActivationReason) def handle_activation(self, reason): """Click handler. This method is called when the tray icon is left-clicked. It opens a menu at the position of the left click. @param reason: reason that caused the activation """ if reason == self.Trigger: self.left_menu.exec_(QtGui.QCursor.pos()) def add_action(self, label, callback, icon=None): if label in self._actions: raise ValueError(f'Action "{label}" already exists in system tray.') if not isinstance(icon, QtGui.QIcon): icon = QtGui.QIcon() iconpath = os.path.join(get_artwork_dir(), 'icons') icon.addFile(os.path.join(iconpath, 'go-next')) action = QtWidgets.QAction(label) action.setIcon(icon) action.triggered.connect(callback) self.left_menu.addAction(action) self._actions[label] = action def remove_action(self, label): action = self._actions.pop(label, None) if action is not None: action.triggered.disconnect() self.left_menu.removeAction(action)
[docs] class Gui(QtCore.QObject): """Set up all necessary GUI elements, like application icons, themes, etc. """ _instance = None _sigPopUpMessage = QtCore.Signal(str, str) _sigBalloonMessage = QtCore.Signal(str, str, object, object) def __new__(cls, *args, **kwargs): if cls._instance is None or cls._instance() is None: obj = super().__new__(cls, *args, **kwargs) cls._instance = weakref.ref(obj) return obj raise RuntimeError( 'Gui is a singleton. Please use Gui.instance() to get a reference to the already ' 'created instance.' )
[docs] def __init__(self, qudi_instance, stylesheet_path=None, theme=None, use_opengl=False): if theme is None: theme = 'qudiTheme' super().__init__() app = QtWidgets.QApplication.instance() if app is None: raise RuntimeError('No Qt GUI app running (no QApplication instance).') app.setQuitOnLastWindowClosed(False) self._init_app_icon() self.set_theme(theme) if stylesheet_path is not None: self.set_style_sheet(stylesheet_path) self.system_tray_icon = SystemTrayIcon() self._sigPopUpMessage.connect(self.pop_up_message, QtCore.Qt.QueuedConnection) self._sigBalloonMessage.connect(self.balloon_message, QtCore.Qt.QueuedConnection) self._configure_pyqtgraph(use_opengl) self.main_gui_module = QudiMainGui(qudi_main_weakref=weakref.ref(qudi_instance), name='qudi_main_gui') self.system_tray_icon.managerAction.triggered.connect(self.activate_main_gui, QtCore.Qt.QueuedConnection) self.system_tray_icon.quitAction.triggered.connect(qudi_instance.quit, QtCore.Qt.QueuedConnection) self.system_tray_icon.restartAction.triggered.connect(qudi_instance.restart, QtCore.Qt.QueuedConnection) qudi_instance.module_manager.sigModuleStateChanged.connect(self._tray_module_action_changed) self.show_system_tray_icon()
@classmethod def instance(cls): if cls._instance is None: return None return cls._instance() @staticmethod def _init_app_icon(): """Set up the Qudi application icon. """ app_icon = QtGui.QIcon(os.path.join(get_artwork_dir(), 'logo', 'logo-qudi.svg')) QtWidgets.QApplication.instance().setWindowIcon(app_icon) @staticmethod def _configure_pyqtgraph(use_opengl=False): # Configure pyqtgraph (if present) if pg is not None: # test setting background of pyqtgraph testwidget = QtWidgets.QWidget() testwidget.ensurePolished() bgcolor = testwidget.palette().color(QtGui.QPalette.Normal, testwidget.backgroundRole()) # set manually the background color in hex code according to our color scheme: pg.setConfigOption('background', bgcolor) # experimental opengl usage pg.setConfigOption('useOpenGL', use_opengl) @staticmethod def set_theme(theme): """ Set icon theme for qudi app. @param str theme: qudi theme name """ # Make icons work on non-X11 platforms, set custom theme # if not sys.platform.startswith('linux') and not sys.platform.startswith('freebsd'): # # To enable the use of custom action icons, for now the above if statement has been # removed and the QT theme is being set to our artwork/icons folder for # all OSs. themepaths = QtGui.QIcon.themeSearchPaths() themepaths.append(os.path.join(get_artwork_dir(), 'icons')) QtGui.QIcon.setThemeSearchPaths(themepaths) QtGui.QIcon.setThemeName(theme) @staticmethod def set_style_sheet(stylesheet_path): """ Set qss style sheet for application. @param str stylesheet_path: path to style sheet file """ try: if not os.path.exists(stylesheet_path): stylesheet_path = os.path.join(get_artwork_dir(), 'styles', stylesheet_path) with open(stylesheet_path, 'r') as stylesheetfile: stylesheet = stylesheetfile.read() if stylesheet_path.endswith('qdark.qss'): path = os.path.join(os.path.dirname(stylesheet_path), 'qdark').replace('\\', '/') stylesheet = stylesheet.replace('{qdark}', path) # see issue #12 on qdarkstyle github if platform.system().lower() == 'darwin' and stylesheet_path.endswith('qdark.qss'): mac_fix = ''' QDockWidget::title { background-color: #31363b; text-align: center; height: 12px; } ''' stylesheet += mac_fix QtWidgets.QApplication.instance().setStyleSheet(stylesheet) except: logger.exception('Exception while setting qudi stylesheet:') @staticmethod def close_windows(): """Close all application windows. """ QtWidgets.QApplication.instance().closeAllWindows() def activate_main_gui(self): if QtCore.QThread.currentThread() is not self.thread(): QtCore.QMetaObject.invokeMethod(self, 'activate_main_gui', QtCore.Qt.BlockingQueuedConnection) return if self.main_gui_module.module_state() != 'deactivated': self.main_gui_module.show() return logger.info('Activating main GUI module...') print('> Activating main GUI module...') self.main_gui_module.module_state.activate() QtWidgets.QApplication.instance().processEvents() def deactivate_main_gui(self): if QtCore.QThread.currentThread() is not self.thread(): QtCore.QMetaObject.invokeMethod(self, 'deactivate_main_gui', QtCore.Qt.BlockingQueuedConnection) return if self.main_gui_module.module_state() == 'deactivated': return self.main_gui_module.module_state.deactivate() QtWidgets.QApplication.instance().processEvents() def show_system_tray_icon(self): """Show system tray icon. """ self.system_tray_icon.show() def hide_system_tray_icon(self): """Hide system tray icon. """ self.system_tray_icon.hide() def close_system_tray_icon(self): """ Kill and delete system tray icon. Tray icon will be lost until Gui.__init__ is called again. """ self.hide_system_tray_icon() self.system_tray_icon.quitAction.triggered.disconnect() self.system_tray_icon.restartAction.triggered.disconnect() self.system_tray_icon.managerAction.triggered.disconnect() self.system_tray_icon = None def system_tray_notification_bubble(self, title, message, time=None, icon=None): """ Helper method to invoke balloon messages in the system tray by calling QSystemTrayIcon.showMessage. @param str title: The notification title of the balloon @param str message: The message to be shown in the balloon @param float time: optional, The lingering time of the balloon in seconds @param QIcon icon: optional, an icon to be used in the balloon. "None" will use OS default. """ if icon is None: icon = QtGui.QIcon() if time is None: time = 15 self.system_tray_icon.showMessage(title, message, icon, int(round(time * 1000))) def prompt_shutdown(self, modules_locked=True): """Display a dialog, asking the user to confirm shutdown. """ if modules_locked: msg = 'Some qudi modules are locked right now.\n' \ 'Do you really want to quit and force modules to deactivate?' else: msg = 'Do you really want to quit?' result = QtWidgets.QMessageBox.question(self.main_gui_module.mw, 'Qudi: Quit?', msg, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) return result == QtWidgets.QMessageBox.Yes def prompt_restart(self, modules_locked=True): """Display a dialog, asking the user to confirm restart. """ if modules_locked: msg = 'Some qudi modules are locked right now.\n' \ 'Do you really want to restart and force modules to deactivate?' else: msg = 'Do you really want to restart?' result = QtWidgets.QMessageBox.question(self.main_gui_module.mw, 'Qudi: Restart?', msg, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) return result == QtWidgets.QMessageBox.Yes @QtCore.Slot(str, str) def pop_up_message(self, title, message): """ Slot prompting a dialog window with a message and an OK button to dismiss it. @param str title: The window title of the dialog @param str message: The message to be shown in the dialog window """ if not isinstance(title, str): logger.error('pop-up message title must be str type') return if not isinstance(message, str): logger.error('pop-up message must be str type') return if self.thread() is not QtCore.QThread.currentThread(): self._sigPopUpMessage.emit(title, message) return QtWidgets.QMessageBox.information(None, title, message, QtWidgets.QMessageBox.Ok) return @QtCore.Slot(str, str, object, object) def balloon_message(self, title, message, time=None, icon=None): """ Slot prompting a balloon notification from the system tray icon. @param str title: The notification title of the balloon @param str message: The message to be shown in the balloon @param float time: optional, The lingering time of the balloon in seconds @param QIcon icon: optional, an icon to be used in the balloon. "None" will use OS default. """ if not self.system_tray_icon.supportsMessages(): logger.warning('{0}:\n{1}'.format(title, message)) return if self.thread() is not QtCore.QThread.currentThread(): self._sigBalloonMessage.emit(title, message, time, icon) return self.system_tray_notification_bubble(title, message, time=time, icon=icon) return @QtCore.Slot(str, str, str) def _tray_module_action_changed(self, base, module_name, state): if self.system_tray_icon and base == 'gui': if state == 'deactivated': self.system_tray_icon.remove_action(module_name) else: mod_manager = ModuleManager.instance() try: module_inst = mod_manager[module_name].instance except KeyError: return self.system_tray_icon.add_action(module_name, module_inst.show)