summaryrefslogtreecommitdiff
path: root/silx/gui/qt
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/qt')
-rw-r--r--silx/gui/qt/__init__.py61
-rw-r--r--silx/gui/qt/_macosx.py68
-rw-r--r--silx/gui/qt/_pyside_dynamic.py158
-rw-r--r--silx/gui/qt/_pyside_missing.py274
-rw-r--r--silx/gui/qt/_qt.py229
-rw-r--r--silx/gui/qt/_utils.py44
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])