diff options
Diffstat (limited to 'silx/gui/qt')
-rw-r--r-- | silx/gui/qt/__init__.py | 61 | ||||
-rw-r--r-- | silx/gui/qt/_macosx.py | 68 | ||||
-rw-r--r-- | silx/gui/qt/_pyside_dynamic.py | 158 | ||||
-rw-r--r-- | silx/gui/qt/_pyside_missing.py | 274 | ||||
-rw-r--r-- | silx/gui/qt/_qt.py | 229 | ||||
-rw-r--r-- | silx/gui/qt/_utils.py | 44 |
6 files changed, 834 insertions, 0 deletions
diff --git a/silx/gui/qt/__init__.py b/silx/gui/qt/__init__.py new file mode 100644 index 0000000..44daa94 --- /dev/null +++ b/silx/gui/qt/__init__.py @@ -0,0 +1,61 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2004-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Common wrapper over Python Qt bindings: + +- `PyQt5 <http://pyqt.sourceforge.net/Docs/PyQt5/>`_, +- `PyQt4 <http://pyqt.sourceforge.net/Docs/PyQt4/>`_ or +- `PySide <http://www.pyside.org>`_. + +If a Qt binding is already loaded, it will use it, otherwise the different +Qt bindings are tried in this order: PyQt4, PySide, PyQt5. + +The name of the loaded Qt binding is stored in the BINDING variable. + +This module provides a flat namespace over Qt bindings by importing +all symbols from **QtCore** and **QtGui** packages and if available +from **QtOpenGL** and **QtSvg** packages. +For **PyQt5**, it also imports all symbols from **QtWidgets** and +**QtPrintSupport** packages. + +Example of using :mod:`silx.gui.qt` module: + +>>> from silx.gui import qt +>>> app = qt.QApplication([]) +>>> widget = qt.QWidget() + +For an alternative solution providing a structured namespace, +see `qtpy <https://pypi.python.org/pypi/QtPy/>`_ which +provides the namespace of PyQt5 over PyQt4 and PySide. +""" + +import sys +from ._qt import * # noqa +from ._utils import * # noqa + + +if sys.platform == "darwin": + if BINDING in ["PySide", "PyQt4"]: + from . import _macosx + _macosx.patch_QUrl_toLocalFile() diff --git a/silx/gui/qt/_macosx.py b/silx/gui/qt/_macosx.py new file mode 100644 index 0000000..07f3143 --- /dev/null +++ b/silx/gui/qt/_macosx.py @@ -0,0 +1,68 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2004-2016 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +""" +Patches for Mac OS X +""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "30/11/2016" + + +def patch_QUrl_toLocalFile(): + """Apply a monkey-patch on qt.QUrl to allow to reach filename when the URL + come from a MIME data from a file drop. Without, `QUrl.toLocalName` with + some version of Mac OS X returns a path which looks like + `/.file/id=180.112`. + + Qt5 is or will be patch, but Qt4 and PySide are not. + + This fix uses the file URL and use an subprocess with an + AppleScript. The script convert the URI into a posix path. + The interpreter (osascript) is available on default OS X installs. + + See https://bugreports.qt.io/browse/QTBUG-40449 + """ + from ._qt import QUrl + import subprocess + + def QUrl_toLocalFile(self): + path = QUrl._oldToLocalFile(self) + if not path.startswith("/.file/id="): + return path + + url = self.toString() + script = 'get posix path of my posix file \"%s\" -- kthxbai' % url + try: + p = subprocess.Popen(["osascript", "-e", script], stdout=subprocess.PIPE) + out, _err = p.communicate() + if p.returncode == 0: + return out.strip() + except OSError: + pass + return path + + QUrl._oldToLocalFile = QUrl.toLocalFile + QUrl.toLocalFile = QUrl_toLocalFile diff --git a/silx/gui/qt/_pyside_dynamic.py b/silx/gui/qt/_pyside_dynamic.py new file mode 100644 index 0000000..a9246b9 --- /dev/null +++ b/silx/gui/qt/_pyside_dynamic.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- + +# Taken from: https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 + +# Copyright (c) 2011 Sebastian Wiesner <lunaryorn@gmail.com> +# Modifications by Charl Botha <cpbotha@vxlabs.com> +# * customWidgets support (registerCustomWidget() causes segfault in +# pyside 1.1.2 on Ubuntu 12.04 x86_64) +# * workingDirectory support in loadUi + +# found this here: +# https://github.com/lunaryorn/snippets/blob/master/qt4/designer/pyside_dynamic.py + +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +""" + How to load a user interface dynamically with PySide. + + .. moduleauthor:: Sebastian Wiesner <lunaryorn@gmail.com> +""" + +from __future__ import (print_function, division, unicode_literals, + absolute_import) + +import logging + +from PySide.QtCore import QMetaObject +from PySide.QtUiTools import QUiLoader +from PySide.QtGui import QMainWindow + + +_logger = logging.getLogger(__name__) + + +class UiLoader(QUiLoader): + """ + Subclass :class:`~PySide.QtUiTools.QUiLoader` to create the user interface + in a base instance. + + Unlike :class:`~PySide.QtUiTools.QUiLoader` itself this class does not + create a new instance of the top-level widget, but creates the user + interface in an existing instance of the top-level class. + + This mimics the behaviour of :func:`PyQt4.uic.loadUi`. + """ + + def __init__(self, baseinstance, customWidgets=None): + """ + Create a loader for the given ``baseinstance``. + + The user interface is created in ``baseinstance``, which must be an + instance of the top-level class in the user interface to load, or a + subclass thereof. + + ``customWidgets`` is a dictionary mapping from class name to class + object for widgets that you've promoted in the Qt Designer + interface. Usually, this should be done by calling + registerCustomWidget on the QUiLoader, but + with PySide 1.1.2 on Ubuntu 12.04 x86_64 this causes a segfault. + + ``parent`` is the parent object of this loader. + """ + + QUiLoader.__init__(self, baseinstance) + self.baseinstance = baseinstance + self.customWidgets = customWidgets + + def createWidget(self, class_name, parent=None, name=''): + """ + Function that is called for each widget defined in ui file, + overridden here to populate baseinstance instead. + """ + + if parent is None and self.baseinstance: + # supposed to create the top-level widget, return the base instance + # instead + return self.baseinstance + + else: + if class_name in self.availableWidgets(): + # create a new widget for child widgets + widget = QUiLoader.createWidget(self, class_name, parent, name) + + else: + # if not in the list of availableWidgets, + # must be a custom widget + # this will raise KeyError if the user has not supplied the + # relevant class_name in the dictionary, or TypeError, if + # customWidgets is None + try: + widget = self.customWidgets[class_name](parent) + + except (TypeError, KeyError): + raise Exception('No custom widget ' + class_name + + ' found in customWidgets param of' + + 'UiLoader __init__.') + + if self.baseinstance: + # set an attribute for the new child widget on the base + # instance, just like PyQt4.uic.loadUi does. + setattr(self.baseinstance, name, widget) + + # this outputs the various widget names, e.g. + # sampleGraphicsView, dockWidget, samplesTableView etc. + # print(name) + + return widget + + +def loadUi(uifile, baseinstance=None, package=None, resource_suffix=None): + """ + Dynamically load a user interface from the given ``uifile``. + + ``uifile`` is a string containing a file name of the UI file to load. + + If ``baseinstance`` is ``None``, the a new instance of the top-level widget + will be created. Otherwise, the user interface is created within the given + ``baseinstance``. In this case ``baseinstance`` must be an instance of the + top-level widget class in the UI file to load, or a subclass thereof. In + other words, if you've created a ``QMainWindow`` interface in the designer, + ``baseinstance`` must be a ``QMainWindow`` or a subclass thereof, too. You + cannot load a ``QMainWindow`` UI file with a plain + :class:`~PySide.QtGui.QWidget` as ``baseinstance``. + + :method:`~PySide.QtCore.QMetaObject.connectSlotsByName()` is called on the + created user interface, so you can implemented your slots according to its + conventions in your widget class. + + Return ``baseinstance``, if ``baseinstance`` is not ``None``. Otherwise + return the newly created instance of the user interface. + """ + if package is not None: + _logger.warning( + "loadUi package parameter not implemented with PySide") + if resource_suffix is not None: + _logger.warning( + "loadUi resource_suffix parameter not implemented with PySide") + + loader = UiLoader(baseinstance) + widget = loader.load(uifile) + QMetaObject.connectSlotsByName(widget) + return widget diff --git a/silx/gui/qt/_pyside_missing.py b/silx/gui/qt/_pyside_missing.py new file mode 100644 index 0000000..a7e2781 --- /dev/null +++ b/silx/gui/qt/_pyside_missing.py @@ -0,0 +1,274 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +""" +Python implementation of classes which are not provided by default by PySide. +""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "17/01/2017" + + +from PySide.QtGui import QAbstractProxyModel +from PySide.QtCore import QModelIndex +from PySide.QtCore import Qt +from PySide.QtGui import QItemSelection +from PySide.QtGui import QItemSelectionRange + + +class QIdentityProxyModel(QAbstractProxyModel): + """Python translation of the source code of Qt c++ file""" + + def __init__(self, parent=None): + super(QIdentityProxyModel, self).__init__(parent) + self.__ignoreNextLayoutAboutToBeChanged = False + self.__ignoreNextLayoutChanged = False + self.__persistentIndexes = [] + + def columnCount(self, parent): + parent = self.mapToSource(parent) + return self.sourceModel().columnCount(parent) + + def dropMimeData(self, data, action, row, column, parent): + parent = self.mapToSource(parent) + return self.sourceModel().dropMimeData(data, action, row, column, parent) + + def index(self, row, column, parent=QModelIndex()): + parent = self.mapToSource(parent) + i = self.sourceModel().index(row, column, parent) + return self.mapFromSource(i) + + def insertColumns(self, column, count, parent=QModelIndex()): + parent = self.mapToSource(parent) + return self.sourceModel().insertColumns(column, count, parent) + + def insertRows(self, row, count, parent=QModelIndex()): + parent = self.mapToSource(parent) + return self.sourceModel().insertRows(row, count, parent) + + def mapFromSource(self, sourceIndex): + if self.sourceModel() is None or not sourceIndex.isValid(): + return QModelIndex() + index = self.createIndex(sourceIndex.row(), sourceIndex.column(), sourceIndex.internalPointer()) + return index + + def mapSelectionFromSource(self, sourceSelection): + proxySelection = QItemSelection() + if self.sourceModel() is None: + return proxySelection + + cursor = sourceSelection.constBegin() + end = sourceSelection.constEnd() + while cursor != end: + topLeft = self.mapFromSource(cursor.topLeft()) + bottomRight = self.mapFromSource(cursor.bottomRight()) + proxyRange = QItemSelectionRange(topLeft, bottomRight) + proxySelection.append(proxyRange) + cursor += 1 + return proxySelection + + def mapSelectionToSource(self, proxySelection): + sourceSelection = QItemSelection() + if self.sourceModel() is None: + return sourceSelection + + cursor = proxySelection.constBegin() + end = proxySelection.constEnd() + while cursor != end: + topLeft = self.mapToSource(cursor.topLeft()) + bottomRight = self.mapToSource(cursor.bottomRight()) + sourceRange = QItemSelectionRange(topLeft, bottomRight) + sourceSelection.append(sourceRange) + cursor += 1 + return sourceSelection + + def mapToSource(self, proxyIndex): + if self.sourceModel() is None or not proxyIndex.isValid(): + return QModelIndex() + return self.sourceModel().createIndex(proxyIndex.row(), proxyIndex.column(), proxyIndex.internalPointer()) + + def match(self, start, role, value, hits=1, flags=Qt.MatchFlags(Qt.MatchStartsWith | Qt.MatchWrap)): + if self.sourceModel() is None: + return [] + + start = self.mapToSource(start) + sourceList = self.sourceModel().match(start, role, value, hits, flags) + proxyList = [] + for cursor in sourceList: + proxyList.append(self.mapFromSource(cursor)) + return proxyList + + def parent(self, child): + sourceIndex = self.mapToSource(child) + sourceParent = sourceIndex.parent() + index = self.mapFromSource(sourceParent) + return index + + def removeColumns(self, column, count, parent=QModelIndex()): + parent = self.mapToSource(parent) + return self.sourceModel().removeColumns(column, count, parent) + + def removeRows(self, row, count, parent=QModelIndex()): + parent = self.mapToSource(parent) + return self.sourceModel().removeRows(row, count, parent) + + def rowCount(self, parent=QModelIndex()): + parent = self.mapToSource(parent) + return self.sourceModel().rowCount(parent) + + def setSourceModel(self, newSourceModel): + """Bind and unbind the source model events""" + self.beginResetModel() + + sourceModel = self.sourceModel() + if sourceModel is not None: + sourceModel.rowsAboutToBeInserted.disconnect(self.__rowsAboutToBeInserted) + sourceModel.rowsInserted.disconnect(self.__rowsInserted) + sourceModel.rowsAboutToBeRemoved.disconnect(self.__rowsAboutToBeRemoved) + sourceModel.rowsRemoved.disconnect(self.__rowsRemoved) + sourceModel.rowsAboutToBeMoved.disconnect(self.__rowsAboutToBeMoved) + sourceModel.rowsMoved.disconnect(self.__rowsMoved) + sourceModel.columnsAboutToBeInserted.disconnect(self.__columnsAboutToBeInserted) + sourceModel.columnsInserted.disconnect(self.__columnsInserted) + sourceModel.columnsAboutToBeRemoved.disconnect(self.__columnsAboutToBeRemoved) + sourceModel.columnsRemoved.disconnect(self.__columnsRemoved) + sourceModel.columnsAboutToBeMoved.disconnect(self.__columnsAboutToBeMoved) + sourceModel.columnsMoved.disconnect(self.__columnsMoved) + sourceModel.modelAboutToBeReset.disconnect(self.__modelAboutToBeReset) + sourceModel.modelReset.disconnect(self.__modelReset) + sourceModel.dataChanged.disconnect(self.__dataChanged) + sourceModel.headerDataChanged.disconnect(self.__headerDataChanged) + sourceModel.layoutAboutToBeChanged.disconnect(self.__layoutAboutToBeChanged) + sourceModel.layoutChanged.disconnect(self.__layoutChanged) + + super(QIdentityProxyModel, self).setSourceModel(newSourceModel) + + sourceModel = self.sourceModel() + if sourceModel is not None: + sourceModel.rowsAboutToBeInserted.connect(self.__rowsAboutToBeInserted) + sourceModel.rowsInserted.connect(self.__rowsInserted) + sourceModel.rowsAboutToBeRemoved.connect(self.__rowsAboutToBeRemoved) + sourceModel.rowsRemoved.connect(self.__rowsRemoved) + sourceModel.rowsAboutToBeMoved.connect(self.__rowsAboutToBeMoved) + sourceModel.rowsMoved.connect(self.__rowsMoved) + sourceModel.columnsAboutToBeInserted.connect(self.__columnsAboutToBeInserted) + sourceModel.columnsInserted.connect(self.__columnsInserted) + sourceModel.columnsAboutToBeRemoved.connect(self.__columnsAboutToBeRemoved) + sourceModel.columnsRemoved.connect(self.__columnsRemoved) + sourceModel.columnsAboutToBeMoved.connect(self.__columnsAboutToBeMoved) + sourceModel.columnsMoved.connect(self.__columnsMoved) + sourceModel.modelAboutToBeReset.connect(self.__modelAboutToBeReset) + sourceModel.modelReset.connect(self.__modelReset) + sourceModel.dataChanged.connect(self.__dataChanged) + sourceModel.headerDataChanged.connect(self.__headerDataChanged) + sourceModel.layoutAboutToBeChanged.connect(self.__layoutAboutToBeChanged) + sourceModel.layoutChanged.connect(self.__layoutChanged) + + self.endResetModel() + + def __columnsAboutToBeInserted(self, parent, start, end): + parent = self.mapFromSource(parent) + self.beginInsertColumns(parent, start, end) + + def __columnsAboutToBeMoved(self, sourceParent, sourceStart, sourceEnd, destParent, dest): + sourceParent = self.mapFromSource(sourceParent) + destParent = self.mapFromSource(destParent) + self.beginMoveColumns(sourceParent, sourceStart, sourceEnd, destParent, dest) + + def __columnsAboutToBeRemoved(self, parent, start, end): + parent = self.mapFromSource(parent) + self.beginRemoveColumns(parent, start, end) + + def __columnsInserted(self, parent, start, end): + self.endInsertColumns() + + def __columnsMoved(self, sourceParent, sourceStart, sourceEnd, destParent, dest): + self.endMoveColumns() + + def __columnsRemoved(self, parent, start, end): + self.endRemoveColumns() + + def __dataChanged(self, topLeft, bottomRight): + topLeft = self.mapFromSource(topLeft) + bottomRight = self.mapFromSource(bottomRight) + self.dataChanged(topLeft, bottomRight) + + def __headerDataChanged(self, orientation, first, last): + self.headerDataChanged(orientation, first, last) + + def __layoutAboutToBeChanged(self): + """Store persistent indexes""" + if self.__ignoreNextLayoutAboutToBeChanged: + return + + for proxyPersistentIndex in self.persistentIndexList(): + self.__proxyIndexes.append() + sourcePersistentIndex = self.mapToSource(proxyPersistentIndex) + mapping = proxyPersistentIndex, sourcePersistentIndex + self.__persistentIndexes.append(mapping) + + self.layoutAboutToBeChanged() + + def __layoutChanged(self): + """Restore persistent indexes""" + if self.__ignoreNextLayoutChanged: + return + + for mapping in self.__persistentIndexes: + proxyIndex, sourcePersistentIndex = mapping + sourcePersistentIndex = self.mapFromSource(sourcePersistentIndex) + self.changePersistentIndex(proxyIndex, sourcePersistentIndex) + + self.__persistentIndexes = [] + + self.layoutChanged() + + def __modelAboutToBeReset(self): + self.beginResetModel() + + def __modelReset(self): + self.endResetModel() + + def __rowsAboutToBeInserted(self, parent, start, end): + parent = self.mapFromSource(parent) + self.beginInsertRows(parent, start, end) + + def __rowsAboutToBeMoved(self, sourceParent, sourceStart, sourceEnd, destParent, dest): + sourceParent = self.mapFromSource(sourceParent) + destParent = self.mapFromSource(destParent) + self.beginMoveRows(sourceParent, sourceStart, sourceEnd, destParent, dest) + + def __rowsAboutToBeRemoved(self, parent, start, end): + parent = self.mapFromSource(parent) + self.beginRemoveRows(parent, start, end) + + def __rowsInserted(self, parent, start, end): + self.endInsertRows() + + def __rowsMoved(self, sourceParent, sourceStart, sourceEnd, destParent, dest): + self.endMoveRows() + + def __rowsRemoved(self, parent, start, end): + self.endRemoveRows() diff --git a/silx/gui/qt/_qt.py b/silx/gui/qt/_qt.py new file mode 100644 index 0000000..0962c21 --- /dev/null +++ b/silx/gui/qt/_qt.py @@ -0,0 +1,229 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2004-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Common wrapper over Python Qt bindings: + +- `PyQt5 <http://pyqt.sourceforge.net/Docs/PyQt5/>`_, +- `PyQt4 <http://pyqt.sourceforge.net/Docs/PyQt4/>`_ or +- `PySide <http://www.pyside.org>`_. + +If a Qt binding is already loaded, it will use it, otherwise the different +Qt bindings are tried in this order: PyQt4, PySide, PyQt5. + +The name of the loaded Qt binding is stored in the BINDING variable. + +For an alternative solution providing a structured namespace, +see `qtpy <https://pypi.python.org/pypi/QtPy/>`_ which +provides the namespace of PyQt5 over PyQt4 and PySide. +""" + +__authors__ = ["V.A. Sole - ESRF Data Analysis"] +__license__ = "MIT" +__date__ = "17/01/2017" + + +import logging +import sys +import traceback + + +_logger = logging.getLogger(__name__) + + +BINDING = None +"""The name of the Qt binding in use: 'PyQt5', 'PyQt4' or 'PySide'.""" + +QtBinding = None # noqa +"""The Qt binding module in use: PyQt5, PyQt4 or PySide.""" + +HAS_SVG = False +"""True if Qt provides support for Scalable Vector Graphics (QtSVG).""" + +HAS_OPENGL = False +"""True if Qt provides support for OpenGL (QtOpenGL).""" + +# First check for an already loaded wrapper +if 'PySide.QtCore' in sys.modules: + BINDING = 'PySide' + +elif 'PyQt5.QtCore' in sys.modules: + BINDING = 'PyQt5' + +elif 'PyQt4.QtCore' in sys.modules: + BINDING = 'PyQt4' + +else: # Then try Qt bindings + try: + import PyQt4 # noqa + except ImportError: + try: + import PySide # noqa + except ImportError: + try: + import PyQt5 # noqa + except ImportError: + raise ImportError( + 'No Qt wrapper found. Install PyQt4, PyQt5 or PySide.') + else: + BINDING = 'PyQt5' + else: + BINDING = 'PySide' + else: + BINDING = 'PyQt4' + + +if BINDING == 'PyQt4': + _logger.debug('Using PyQt4 bindings') + + if sys.version < "3.0.0": + try: + import sip + + sip.setapi("QString", 2) + sip.setapi("QVariant", 2) + except: + _logger.warning("Cannot set sip API") + + import PyQt4 as QtBinding # noqa + + from PyQt4.QtCore import * # noqa + from PyQt4.QtGui import * # noqa + + try: + from PyQt4.QtOpenGL import * # noqa + except ImportError: + _logger.info("PyQt4.QtOpenGL not available") + HAS_OPENGL = False + else: + HAS_OPENGL = True + + try: + from PyQt4.QtSvg import * # noqa + except ImportError: + _logger.info("PyQt4.QtSvg not available") + HAS_SVG = False + else: + HAS_SVG = True + + from PyQt4.uic import loadUi # noqa + + Signal = pyqtSignal + + Property = pyqtProperty + + Slot = pyqtSlot + +elif BINDING == 'PySide': + _logger.debug('Using PySide bindings') + + import PySide as QtBinding # noqa + + from PySide.QtCore import * # noqa + from PySide.QtGui import * # noqa + + try: + from PySide.QtOpenGL import * # noqa + except ImportError: + _logger.info("PySide.QtOpenGL not available") + HAS_OPENGL = False + else: + HAS_OPENGL = True + + try: + from PySide.QtSvg import * # noqa + except ImportError: + _logger.info("PySide.QtSvg not available") + HAS_SVG = False + else: + HAS_SVG = True + + pyqtSignal = Signal + + # Import loadUi wrapper for PySide + from ._pyside_dynamic import loadUi # noqa + + # Import missing classes + if not hasattr(locals(), "QIdentityProxyModel"): + from ._pyside_missing import QIdentityProxyModel # noqa + +elif BINDING == 'PyQt5': + _logger.debug('Using PyQt5 bindings') + + import PyQt5 as QtBinding # noqa + + from PyQt5.QtCore import * # noqa + from PyQt5.QtGui import * # noqa + from PyQt5.QtWidgets import * # noqa + from PyQt5.QtPrintSupport import * # noqa + + try: + from PyQt5.QtOpenGL import * # noqa + except ImportError: + _logger.info("PySide.QtOpenGL not available") + HAS_OPENGL = False + else: + HAS_OPENGL = True + + try: + from PyQt5.QtSvg import * # noqa + except ImportError: + _logger.info("PyQt5.QtSvg not available") + HAS_SVG = False + else: + HAS_SVG = True + + from PyQt5.uic import loadUi # noqa + + Signal = pyqtSignal + + Property = pyqtProperty + + Slot = pyqtSlot + +else: + raise ImportError('No Qt wrapper found. Install PyQt4, PyQt5 or PySide') + +# provide a exception handler but not implement it by default +def exceptionHandler(type_, value, trace): + """ + This exception handler prevents quitting to the command line when there is + an unhandled exception while processing a Qt signal. + + The script/application willing to use it should implement code similar to: + + .. code-block:: python + + if __name__ == "__main__": + sys.excepthook = qt.exceptionHandler + + """ + _logger.error("%s %s %s", type_, value, ''.join(traceback.format_tb(trace))) + msg = QMessageBox() + msg.setWindowTitle("Unhandled exception") + msg.setIcon(QMessageBox.Critical) + msg.setInformativeText("%s %s\nPlease report details" % (type_, value)) + msg.setDetailedText(("%s " % value) + ''.join(traceback.format_tb(trace))) + msg.raise_() + msg.exec_() + diff --git a/silx/gui/qt/_utils.py b/silx/gui/qt/_utils.py new file mode 100644 index 0000000..0aa3ef1 --- /dev/null +++ b/silx/gui/qt/_utils.py @@ -0,0 +1,44 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2004-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""This module provides convenient functions related to Qt. +""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "30/11/2016" + +import sys +from ._qt import BINDING, QImageReader + + +def supportedImageFormats(): + """Return a set of string of file format extensions supported by the + Qt runtime.""" + if sys.version_info[0] < 3 or BINDING == 'PySide': + convert = str + else: + convert = lambda data: str(data, 'ascii') + formats = QImageReader.supportedImageFormats() + return set([convert(data) for data in formats]) |