summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/actions
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/actions')
-rw-r--r--silx/gui/plot3d/actions/Plot3DAction.py69
-rw-r--r--silx/gui/plot3d/actions/__init__.py33
-rw-r--r--silx/gui/plot3d/actions/io.py336
-rw-r--r--silx/gui/plot3d/actions/mode.py126
4 files changed, 564 insertions, 0 deletions
diff --git a/silx/gui/plot3d/actions/Plot3DAction.py b/silx/gui/plot3d/actions/Plot3DAction.py
new file mode 100644
index 0000000..a1faaea
--- /dev/null
+++ b/silx/gui/plot3d/actions/Plot3DAction.py
@@ -0,0 +1,69 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2016-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.
+#
+# ###########################################################################*/
+"""Base class for QAction attached to a Plot3DWidget."""
+
+from __future__ import absolute_import, division
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "06/09/2017"
+
+
+import logging
+import weakref
+
+from silx.gui import qt
+
+
+_logger = logging.getLogger(__name__)
+
+
+class Plot3DAction(qt.QAction):
+ """QAction associated to a Plot3DWidget
+
+ :param parent: See :class:`QAction`
+ :param Plot3DWidget plot3d: Plot3DWidget the action is associated with
+ """
+
+ def __init__(self, parent, plot3d=None):
+ super(Plot3DAction, self).__init__(parent)
+ self._plot3d = None
+ self.setPlot3DWidget(plot3d)
+
+ def setPlot3DWidget(self, widget):
+ """Set the Plot3DWidget this action is associated with
+
+ :param Plot3DWidget widget: The Plot3DWidget to use
+ """
+ self._plot3d = None if widget is None else weakref.ref(widget)
+
+ def getPlot3DWidget(self):
+ """Return the Plot3DWidget associated to this action.
+
+ If no widget is associated, it returns None.
+
+ :rtype: qt.QWidget
+ """
+ return None if self._plot3d is None else self._plot3d()
diff --git a/silx/gui/plot3d/actions/__init__.py b/silx/gui/plot3d/actions/__init__.py
new file mode 100644
index 0000000..ebc57d2
--- /dev/null
+++ b/silx/gui/plot3d/actions/__init__.py
@@ -0,0 +1,33 @@
+# 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.
+#
+# ###########################################################################*/
+"""This module provides QAction that can be attached to a plot3DWidget."""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "06/09/2017"
+
+from .Plot3DAction import Plot3DAction
+from . import io
+from . import mode
diff --git a/silx/gui/plot3d/actions/io.py b/silx/gui/plot3d/actions/io.py
new file mode 100644
index 0000000..18f91b4
--- /dev/null
+++ b/silx/gui/plot3d/actions/io.py
@@ -0,0 +1,336 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2016-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 Plot3DAction related to input/output.
+
+It provides QAction to copy, save (snapshot and video), print a Plot3DWidget.
+"""
+
+from __future__ import absolute_import, division
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "06/09/2017"
+
+
+import logging
+import os
+
+import numpy
+
+from silx.gui import qt
+from silx.gui.plot.actions.io import PrintAction as _PrintAction
+from silx.gui.icons import getQIcon
+from .Plot3DAction import Plot3DAction
+from ..utils import mng
+from ..._utils import convertQImageToArray
+
+
+_logger = logging.getLogger(__name__)
+
+
+class CopyAction(Plot3DAction):
+ """QAction to provide copy of a Plot3DWidget
+
+ :param parent: See :class:`QAction`
+ :param Plot3DWidget plot3d: Plot3DWidget the action is associated with
+ """
+
+ def __init__(self, parent, plot3d=None):
+ super(CopyAction, self).__init__(parent, plot3d)
+
+ self.setIcon(getQIcon('edit-copy'))
+ self.setText('Copy')
+ self.setToolTip('Copy a snapshot of the 3D scene to the clipboard')
+ self.setCheckable(False)
+ self.setShortcut(qt.QKeySequence.Copy)
+ self.setShortcutContext(qt.Qt.WidgetShortcut)
+ self.triggered[bool].connect(self._triggered)
+
+ def _triggered(self, checked=False):
+ plot3d = self.getPlot3DWidget()
+ if plot3d is None:
+ _logger.error('Cannot copy widget, no associated Plot3DWidget')
+ else:
+ image = plot3d.grabGL()
+ qt.QApplication.clipboard().setImage(image)
+
+
+class SaveAction(Plot3DAction):
+ """QAction to provide save snapshot of a Plot3DWidget
+
+ :param parent: See :class:`QAction`
+ :param Plot3DWidget plot3d: Plot3DWidget the action is associated with
+ """
+
+ def __init__(self, parent, plot3d=None):
+ super(SaveAction, self).__init__(parent, plot3d)
+
+ self.setIcon(getQIcon('document-save'))
+ self.setText('Save...')
+ self.setToolTip('Save a snapshot of the 3D scene')
+ self.setCheckable(False)
+ self.setShortcut(qt.QKeySequence.Save)
+ self.setShortcutContext(qt.Qt.WidgetShortcut)
+ self.triggered[bool].connect(self._triggered)
+
+ def _triggered(self, checked=False):
+ plot3d = self.getPlot3DWidget()
+ if plot3d is None:
+ _logger.error('Cannot save widget, no associated Plot3DWidget')
+ else:
+ dialog = qt.QFileDialog(self.parent())
+ dialog.setWindowTitle('Save snapshot as')
+ dialog.setModal(True)
+ dialog.setNameFilters(('Plot3D Snapshot PNG (*.png)',
+ 'Plot3D Snapshot JPEG (*.jpg)'))
+
+ dialog.setFileMode(qt.QFileDialog.AnyFile)
+ dialog.setAcceptMode(qt.QFileDialog.AcceptSave)
+
+ if not dialog.exec_():
+ return
+
+ nameFilter = dialog.selectedNameFilter()
+ filename = dialog.selectedFiles()[0]
+ dialog.close()
+
+ # Forces the filename extension to match the chosen filter
+ extension = nameFilter.split()[-1][2:-1]
+ if (len(filename) <= len(extension) or
+ filename[-len(extension):].lower() != extension.lower()):
+ filename += extension
+
+ image = plot3d.grabGL()
+ if not image.save(filename):
+ _logger.error('Failed to save image as %s', filename)
+ qt.QMessageBox.critical(
+ self.parent(),
+ 'Save snapshot as',
+ 'Failed to save snapshot')
+
+
+class PrintAction(Plot3DAction):
+ """QAction to provide printing of a Plot3DWidget
+
+ :param parent: See :class:`QAction`
+ :param Plot3DWidget plot3d: Plot3DWidget the action is associated with
+ """
+
+ def __init__(self, parent, plot3d=None):
+ super(PrintAction, self).__init__(parent, plot3d)
+
+ self.setIcon(getQIcon('document-print'))
+ self.setText('Print...')
+ self.setToolTip('Print a snapshot of the 3D scene')
+ self.setCheckable(False)
+ self.setShortcut(qt.QKeySequence.Print)
+ self.setShortcutContext(qt.Qt.WidgetShortcut)
+ self.triggered[bool].connect(self._triggered)
+
+ def getPrinter(self):
+ """Return the QPrinter instance used for printing.
+
+ :rtype: qt.QPrinter
+ """
+ # TODO This is a hack to sync with silx plot PrintAction
+ # This needs to be centralized
+ if _PrintAction._printer is None:
+ _PrintAction._printer = qt.QPrinter()
+ return _PrintAction._printer
+
+ def _triggered(self, checked=False):
+ plot3d = self.getPlot3DWidget()
+ if plot3d is None:
+ _logger.error('Cannot print widget, no associated Plot3DWidget')
+ else:
+ printer = self.getPrinter()
+ dialog = qt.QPrintDialog(printer, plot3d)
+ dialog.setWindowTitle('Print Plot3D snapshot')
+ if not dialog.exec_():
+ return
+
+ image = plot3d.grabGL()
+
+ # Draw pixmap with painter
+ painter = qt.QPainter()
+ if not painter.begin(printer):
+ return
+
+ if (printer.pageRect().width() < image.width() or
+ printer.pageRect().height() < image.height()):
+ # Downscale to page
+ xScale = printer.pageRect().width() / image.width()
+ yScale = printer.pageRect().height() / image.height()
+ scale = min(xScale, yScale)
+ else:
+ scale = 1.
+
+ rect = qt.QRectF(0,
+ 0,
+ scale * image.width(),
+ scale * image.height())
+ painter.drawImage(rect, image)
+ painter.end()
+
+
+class VideoAction(Plot3DAction):
+ """This action triggers the recording of a video of the scene.
+
+ The scene is rotated 360 degrees around a vertical axis.
+
+ :param parent: Action parent see :class:`QAction`.
+ """
+
+ PNG_SERIE_FILTER = 'Serie of PNG files (*.png)'
+ MNG_FILTER = 'Multiple-image Network Graphics file (*.mng)'
+
+ def __init__(self, parent, plot3d=None):
+ super(VideoAction, self).__init__(parent, plot3d)
+ self.setText('Record video..')
+ self.setIcon(getQIcon('camera'))
+ self.setToolTip(
+ 'Record a video of a 360 degrees rotation of the 3D scene.')
+ self.setCheckable(False)
+ self.triggered[bool].connect(self._triggered)
+
+ def _triggered(self, checked=False):
+ """Action triggered callback"""
+ plot3d = self.getPlot3DWidget()
+ if plot3d is None:
+ _logger.warning(
+ 'Ignoring action triggered without Plot3DWidget set')
+ return
+
+ dialog = qt.QFileDialog(parent=plot3d)
+ dialog.setWindowTitle('Save video as...')
+ dialog.setModal(True)
+ dialog.setNameFilters([self.PNG_SERIE_FILTER,
+ self.MNG_FILTER])
+ dialog.setFileMode(dialog.AnyFile)
+ dialog.setAcceptMode(dialog.AcceptSave)
+
+ if not dialog.exec_():
+ return
+
+ nameFilter = dialog.selectedNameFilter()
+ filename = dialog.selectedFiles()[0]
+
+ # Forces the filename extension to match the chosen filter
+ extension = nameFilter.split()[-1][2:-1]
+ if (len(filename) <= len(extension) or
+ filename[-len(extension):].lower() != extension.lower()):
+ filename += extension
+
+ nbFrames = int(4. * 25) # 4 seconds, 25 fps
+
+ if nameFilter == self.PNG_SERIE_FILTER:
+ self._saveAsPNGSerie(filename, nbFrames)
+ elif nameFilter == self.MNG_FILTER:
+ self._saveAsMNG(filename, nbFrames)
+ else:
+ _logger.error('Unsupported file filter: %s', nameFilter)
+
+ def _saveAsPNGSerie(self, filename, nbFrames):
+ """Save video as serie of PNG files.
+
+ It adds a counter to the provided filename before the extension.
+
+ :param str filename: filename to use as template
+ :param int nbFrames: Number of frames to generate
+ """
+ plot3d = self.getPlot3DWidget()
+ assert plot3d is not None
+
+ # Define filename template
+ nbDigits = int(numpy.log10(nbFrames)) + 1
+ indexFormat = '%%0%dd' % nbDigits
+ extensionIndex = filename.rfind('.')
+ filenameFormat = \
+ filename[:extensionIndex] + indexFormat + filename[extensionIndex:]
+
+ try:
+ for index, image in enumerate(self._video360(nbFrames)):
+ image.save(filenameFormat % index)
+ except GeneratorExit:
+ pass
+
+ def _saveAsMNG(self, filename, nbFrames):
+ """Save video as MNG file.
+
+ :param str filename: filename to use
+ :param int nbFrames: Number of frames to generate
+ """
+ plot3d = self.getPlot3DWidget()
+ assert plot3d is not None
+
+ frames = (convertQImageToArray(im) for im in self._video360(nbFrames))
+ try:
+ with open(filename, 'wb') as file_:
+ for chunk in mng.convert(frames, nb_images=nbFrames):
+ file_.write(chunk)
+ except GeneratorExit:
+ os.remove(filename) # Saving aborted, delete file
+
+ def _video360(self, nbFrames):
+ """Run the video and provides the images
+
+ :param int nbFrames: The number of frames to generate for
+ :return: Iterator of QImage of the video sequence
+ """
+ plot3d = self.getPlot3DWidget()
+ assert plot3d is not None
+
+ angleStep = 360. / nbFrames
+
+ # Create progress bar dialog
+ dialog = qt.QDialog(plot3d)
+ dialog.setWindowTitle('Record Video')
+ layout = qt.QVBoxLayout(dialog)
+ progress = qt.QProgressBar()
+ progress.setRange(0, nbFrames)
+ layout.addWidget(progress)
+
+ btnBox = qt.QDialogButtonBox(qt.QDialogButtonBox.Abort)
+ btnBox.rejected.connect(dialog.reject)
+ layout.addWidget(btnBox)
+
+ dialog.setModal(True)
+ dialog.show()
+
+ qapp = qt.QApplication.instance()
+
+ for frame in range(nbFrames):
+ progress.setValue(frame)
+ image = plot3d.grabGL()
+ yield image
+ plot3d.viewport.orbitCamera('left', angleStep)
+ qapp.processEvents()
+ if not dialog.isVisible():
+ break # It as been rejected by the abort button
+ else:
+ dialog.accept()
+
+ if dialog.result() == qt.QDialog.Rejected:
+ raise GeneratorExit('Aborted')
diff --git a/silx/gui/plot3d/actions/mode.py b/silx/gui/plot3d/actions/mode.py
new file mode 100644
index 0000000..a06b9a8
--- /dev/null
+++ b/silx/gui/plot3d/actions/mode.py
@@ -0,0 +1,126 @@
+# 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.
+#
+# ###########################################################################*/
+"""This module provides Plot3DAction related to interaction modes.
+
+It provides QAction to rotate or pan a Plot3DWidget.
+"""
+
+from __future__ import absolute_import, division
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "06/09/2017"
+
+
+import logging
+
+from silx.gui.icons import getQIcon
+from .Plot3DAction import Plot3DAction
+
+
+_logger = logging.getLogger(__name__)
+
+
+class InteractiveModeAction(Plot3DAction):
+ """Base class for QAction changing interactive mode of a Plot3DWidget
+
+ :param parent: See :class:`QAction`
+ :param str interaction: The interactive mode this action controls
+ :param Plot3DWidget plot3d: Plot3DWidget the action is associated with
+ """
+
+ def __init__(self, parent, interaction, plot3d=None):
+ self._interaction = interaction
+
+ super(InteractiveModeAction, self).__init__(parent, plot3d)
+ self.setCheckable(True)
+ self.triggered[bool].connect(self._triggered)
+
+ def _triggered(self, checked=False):
+ plot3d = self.getPlot3DWidget()
+ if plot3d is None:
+ _logger.error(
+ 'Cannot set %s interaction, no associated Plot3DWidget' %
+ self._interaction)
+ else:
+ plot3d.setInteractiveMode(self._interaction)
+ self.setChecked(True)
+
+ def setPlot3DWidget(self, widget):
+ # Disconnect from previous Plot3DWidget
+ plot3d = self.getPlot3DWidget()
+ if plot3d is not None:
+ plot3d.sigInteractiveModeChanged.disconnect(
+ self._interactiveModeChanged)
+
+ super(InteractiveModeAction, self).setPlot3DWidget(widget)
+
+ # Connect to new Plot3DWidget
+ if widget is None:
+ self.setChecked(False)
+ else:
+ self.setChecked(widget.getInteractiveMode() == self._interaction)
+ widget.sigInteractiveModeChanged.connect(
+ self._interactiveModeChanged)
+
+ # Reuse docstring from super class
+ setPlot3DWidget.__doc__ = Plot3DAction.setPlot3DWidget.__doc__
+
+ def _interactiveModeChanged(self):
+ plot3d = self.getPlot3DWidget()
+ if plot3d is None:
+ _logger.error('Received a signal while there is no widget')
+ else:
+ self.setChecked(plot3d.getInteractiveMode() == self._interaction)
+
+
+class RotateArcballAction(InteractiveModeAction):
+ """QAction to set arcball rotation interaction on a Plot3DWidget
+
+ :param parent: See :class:`QAction`
+ :param Plot3DWidget plot3d: Plot3DWidget the action is associated with
+ """
+
+ def __init__(self, parent, plot3d=None):
+ super(RotateArcballAction, self).__init__(parent, 'rotate', plot3d)
+
+ self.setIcon(getQIcon('rotate-3d'))
+ self.setText('Rotate')
+ self.setToolTip('Rotate the view')
+
+
+class PanAction(InteractiveModeAction):
+ """QAction to set pan interaction on a Plot3DWidget
+
+ :param parent: See :class:`QAction`
+ :param Plot3DWidget plot3d: Plot3DWidget the action is associated with
+ """
+
+ def __init__(self, parent, plot3d=None):
+ super(PanAction, self).__init__(parent, 'pan', plot3d)
+
+ self.setIcon(getQIcon('pan'))
+ self.setText('Pan')
+ self.setToolTip('Pan the view')