# -*- coding: utf-8 -*- # This file is part of Argos. # # Argos is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Argos 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Argos. If not, see <http://www.gnu.org/licenses/>. """ Miscellaneous Qt routines. """ from __future__ import division, print_function import logging import os import sys import traceback from argos.external.six import PY3 from argos import info from argos.utils.cls import environment_var_to_bool logger = logging.getLogger(__name__) ######################### # Importing PyQt/PySide # ######################### # Argos requires PyQt5. However, if you install qtpy (the master branch or the next version > 1.1.2) # and set the ARGOS_USE_QTPY environment variable to "1", Argos will work with PyQt4 or PySide. # You then can force which Qt bindings are used by setting the QT_API environment variable to pyqt5, # pyqt4 or pyside. If the QT_API environment variable is not set, qtpy will autodetect the bindings. # # Note that PyQt4 and PySide or not officially supported! There are already enough dependencies # that can vary (Python 2 & 3, Windows & Linux & OS-X, etc) so I don't want to support even more # combinations. I keep PyQt4 and PySide working as long as it is practical but I don't do extensive # testing. If you encounter issues with PyQt4/PySide please report them and I'll see what I can do. # Note that pyside does currently not work in combination with Python3! USE_QTPY = environment_var_to_bool(os.environ.get('ARGOS_USE_QTPY', False)) if USE_QTPY: if info.DEBUGGING: logger.debug("ARGOS_USE_QTPY = {}, using qtpy to find Qt bindings".format(USE_QTPY)) import qtpy._version from qtpy import QtCore, QtGui, QtWidgets, QtSvg from qtpy.QtCore import Qt from qtpy.QtCore import Signal as QtSignal from qtpy.QtCore import Slot as QtSlot from qtpy import PYQT_VERSION from qtpy import QT_VERSION QT_API = qtpy.API QT_API_NAME = qtpy.API_NAME QTPY_VERSION = '.'.join(map(str, qtpy._version.version_info)) ABOUT_QT_BINDINGS = "{} (api {}, qtpy: {})".format(QT_API_NAME, QT_API, QTPY_VERSION) if qtpy._version.version_info <= (1, 1, 2): # At least commit e863f422c7ef78f66223adaa40d52cba4a3b2fce logger.warning("You need qtpy version > 1.1.2, got: {}".format(QTPY_VERSION)) # PySide in combination with Python-3 gives the following error: # TypeError: unhashable type: 'PgImagePlot2dCti' # I don't know a fix and as long as the future of PySide2 is unclear I won't spend time on it. if QT_API == 'pyside' and PY3: raise ImportError("PySide in combination with Python 3 is buggy in Argos and supported.") else: if info.DEBUGGING: logger.debug("ARGOS_USE_QTPY = {}, using PyQt5 directly".format(USE_QTPY)) from PyQt5 import QtCore, QtGui, QtWidgets, QtSvg from PyQt5.QtCore import Qt from PyQt5.QtCore import pyqtSignal as QtSignal from PyQt5.QtCore import pyqtSlot as QtSlot from PyQt5.Qt import PYQT_VERSION_STR as PYQT_VERSION from PyQt5.Qt import QT_VERSION_STR as QT_VERSION QT_API = '' QT_API_NAME = 'PyQt5' QTPY_VERSION = '' ABOUT_QT_BINDINGS = 'PyQt5' ################ # QApplication # ################ def initQCoreApplication(): """ Initializes the QtCore.QApplication instance. Creates one if it doesn't exist. Sets Argos specific attributes, such as the OrganizationName, so that the application persistent settings are read/written to the correct settings file/winreg. It is therefore important to call this function (or initQApplication) at startup. Returns the application. """ app = QtCore.QCoreApplication(sys.argv) initArgosApplicationSettings(app) return app def initQApplication(): """ Initializes the QtWidgets.QApplication instance. Creates one if it doesn't exist. Sets Argos specific attributes, such as the OrganizationName, so that the application persistent settings are read/written to the correct settings file/winreg. It is therefore important to call this function at startup. The ArgosApplication constructor does this. Returns the application. """ # PyQtGraph recommends raster graphics system for OS-X. if 'darwin' in sys.platform: graphicsSystem = "raster" # raster, native or opengl os.environ.setdefault('QT_GRAPHICSSYSTEM', graphicsSystem) logger.info("Setting QT_GRAPHICSSYSTEM to: {}".format(graphicsSystem)) app = QtWidgets.QApplication(sys.argv) initArgosApplicationSettings(app) return app def initArgosApplicationSettings(app): # TODO: this is Argos specific. Move somewhere else. """ Sets Argos specific attributes, such as the OrganizationName, so that the application persistent settings are read/written to the correct settings file/winreg. It is therefore important to call this function at startup. The ArgosApplication constructor does this. """ assert app, \ "app undefined. Call QtWidgets.QApplication.instance() or QtCor.QApplication.instance() first." logger.debug("Setting Argos QApplication settings.") app.setApplicationName(info.REPO_NAME) app.setApplicationVersion(info.VERSION) app.setOrganizationName(info.ORGANIZATION_NAME) app.setOrganizationDomain(info.ORGANIZATION_DOMAIN) ###################### # Exception Handling # ###################### class ResizeDetailsMessageBox(QtWidgets.QMessageBox): """ Message box that enlarges when the 'Show Details' button is clicked. Can be used to better view stack traces. I could't find how to make a resizeable message box but this it the next best thing. Taken from: http://stackoverflow.com/questions/2655354/how-to-allow-resizing-of-qmessagebox-in-pyqt4 """ def __init__(self, detailsBoxWidth=700, detailBoxHeight=300, *args, **kwargs): """ Constructor :param detailsBoxWidht: The width of the details text box (default=700) :param detailBoxHeight: The heights of the details text box (default=700) """ super(ResizeDetailsMessageBox, self).__init__(*args, **kwargs) self.detailsBoxWidth = detailsBoxWidth self.detailBoxHeight = detailBoxHeight def resizeEvent(self, event): """ Resizes the details box if present (i.e. when 'Show Details' button was clicked) """ result = super(ResizeDetailsMessageBox, self).resizeEvent(event) details_box = self.findChild(QtWidgets.QTextEdit) if details_box is not None: #details_box.setFixedSize(details_box.sizeHint()) details_box.setFixedSize(QtCore.QSize(self.detailsBoxWidth, self.detailBoxHeight)) return result def handleException(exc_type, exc_value, exc_traceback): traceback.format_exception(exc_type, exc_value, exc_traceback) logger.critical("Bug: uncaught {}".format(exc_type.__name__), exc_info=(exc_type, exc_value, exc_traceback)) if info.DEBUGGING: logger.info("Quitting application with exit code 1") sys.exit(1) else: # Constructing a QApplication in case this hasn't been done yet. if not QtWidgets.qApp: _app = QtWidgets.QApplication() msgBox = ResizeDetailsMessageBox() msgBox.setText("Bug: uncaught {}".format(exc_type.__name__)) msgBox.setInformativeText(str(exc_value)) lst = traceback.format_exception(exc_type, exc_value, exc_traceback) msgBox.setDetailedText("".join(lst)) msgBox.setIcon(QtWidgets.QMessageBox.Warning) msgBox.exec_() logger.info("Quitting application with exit code 1") sys.exit(1) ###################### # QSettings routines # ###################### def removeSettingsGroup(groupName, settings=None): """ Removes a group from the persistent settings """ logger.debug("Removing settings group: {}".format(groupName)) settings = QtCore.QSettings() if settings is None else settings settings.remove(groupName) def containsSettingsGroup(groupName, settings=None): """ Returns True if the settings contain a group with the name groupName. Works recursively when the groupName is a slash separated path. """ def _containsPath(path, settings): "Aux function for containsSettingsGroup. Does the actual recursive search." if len(path) == 0: return True else: head = path[0] tail = path[1:] if head not in settings.childGroups(): return False else: settings.beginGroup(head) try: return _containsPath(tail, settings) finally: settings.endGroup() # Body starts here path = os.path.split(groupName) logger.debug("Looking for path: {}".format(path)) settings = QtCore.QSettings() if settings is None else settings return _containsPath(path, settings) ###################### # Debugging routines # ###################### def printChildren(obj, indent=""): """ Recursively prints the children of a QObject. Useful for debugging. """ children=obj.children() if children==None: return for child in children: try: childName = child.objectName() except AttributeError: childName = "<no-name>" #print ("{}{:10s}: {}".format(indent, childName, child.__class__)) print ("{}{!r}: {}".format(indent, childName, child.__class__)) printChildren(child, indent + " ") def printAllWidgets(qApplication, ofType=None): """ Prints list of all widgets to stdout (for debugging) """ print ("Application's widgets {}".format(('of type: ' + str(ofType)) if ofType else '')) for widget in qApplication.allWidgets(): if ofType is None or isinstance(widget, ofType): print (" {!r}".format(widget)) ##################### # Unsorted routines # ##################### def widgetSubCheckBoxRect(widget, option): """ Returns the rectangle of a check box drawn as a sub element of widget """ opt = QtWidgets.QStyleOption() opt.initFrom(widget) style = widget.style() return style.subElementRect(QtWidgets.QStyle.SE_ViewItemCheckIndicator, opt, widget)