From 1f32be1bebef249777175aef600fa9889db35704 2010-08-13 21:42:39 From: epatters Date: 2010-08-13 21:42:39 Subject: [PATCH] * Added a custom context menu to the RichIPythonWidget which allows saving plot as an images or SVG documents. * Expanded generic SVG functionality and refactored it into its own module. --- diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index cd2e4f4..239bb2f 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -941,22 +941,16 @@ class ConsoleWidget(QtGui.QWidget): """ menu = QtGui.QMenu() - copy_action = QtGui.QAction('Copy', menu) - copy_action.triggered.connect(self.copy) + copy_action = menu.addAction('Copy', self.copy) copy_action.setEnabled(self._get_cursor().hasSelection()) copy_action.setShortcut(QtGui.QKeySequence.Copy) - menu.addAction(copy_action) - paste_action = QtGui.QAction('Paste', menu) - paste_action.triggered.connect(self.paste) + paste_action = menu.addAction('Paste', self.paste) paste_action.setEnabled(self.can_paste()) paste_action.setShortcut(QtGui.QKeySequence.Paste) - menu.addAction(paste_action) - menu.addSeparator() - select_all_action = QtGui.QAction('Select All', menu) - select_all_action.triggered.connect(self.select_all) - menu.addAction(select_all_action) + menu.addSeparator() + menu.addAction('Select All', self.select_all) menu.exec_(self._control.mapToGlobal(pos)) diff --git a/IPython/frontend/qt/console/rich_ipython_widget.py b/IPython/frontend/qt/console/rich_ipython_widget.py index 1867e9e..ff5a831 100644 --- a/IPython/frontend/qt/console/rich_ipython_widget.py +++ b/IPython/frontend/qt/console/rich_ipython_widget.py @@ -2,7 +2,7 @@ from PyQt4 import QtCore, QtGui # Local imports -from IPython.frontend.qt.util import image_from_svg +from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image from ipython_widget import IPythonWidget @@ -12,6 +12,9 @@ class RichIPythonWidget(IPythonWidget): text version. """ + # Protected class variables. + _svg_text_format_property = 1 + #--------------------------------------------------------------------------- # 'QObject' interface #--------------------------------------------------------------------------- @@ -20,6 +23,33 @@ class RichIPythonWidget(IPythonWidget): """ Create a RichIPythonWidget. """ super(RichIPythonWidget, self).__init__(kind='rich', parent=parent) + + #--------------------------------------------------------------------------- + # 'ConsoleWidget' protected interface + #--------------------------------------------------------------------------- + + def _show_context_menu(self, pos): + """ Reimplemented to show a custom context menu for images. + """ + format = self._control.cursorForPosition(pos).charFormat() + name = format.stringProperty(QtGui.QTextFormat.ImageName) + if name.isEmpty(): + super(RichIPythonWidget, self)._show_context_menu(pos) + else: + menu = QtGui.QMenu() + + menu.addAction('Copy Image', lambda: self._copy_image(name)) + menu.addAction('Save Image As...', lambda: self._save_image(name)) + menu.addSeparator() + + svg = format.stringProperty(self._svg_text_format_property) + if not svg.isEmpty(): + menu.addSeparator() + menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg)) + menu.addAction('Save SVG As...', + lambda: save_svg(svg, self._control)) + + menu.exec_(self._control.mapToGlobal(pos)) #--------------------------------------------------------------------------- # 'FrontendWidget' protected interface @@ -30,14 +60,59 @@ class RichIPythonWidget(IPythonWidget): """ plot_payload = payload.get('plot', None) if plot_payload and plot_payload['format'] == 'svg': + svg = plot_payload['data'] try: - image = image_from_svg(plot_payload['data']) + image = svg_to_image(svg) except ValueError: self._append_plain_text('Received invalid plot data.') else: + format = self._add_image(image) + format.setProperty(self._svg_text_format_property, svg) cursor = self._get_end_cursor() cursor.insertBlock() - cursor.insertImage(image) + cursor.insertImage(format) cursor.insertBlock() else: super(RichIPythonWidget, self)._handle_execute_payload(payload) + + #--------------------------------------------------------------------------- + # 'RichIPythonWidget' protected interface + #--------------------------------------------------------------------------- + + def _add_image(self, image): + """ Adds the specified QImage to the document and returns a + QTextImageFormat that references it. + """ + document = self._control.document() + name = QtCore.QString.number(image.cacheKey()) + document.addResource(QtGui.QTextDocument.ImageResource, + QtCore.QUrl(name), image) + format = QtGui.QTextImageFormat() + format.setName(name) + return format + + def _copy_image(self, name): + """ Copies the ImageResource with 'name' to the clipboard. + """ + image = self._get_image(name) + QtGui.QApplication.clipboard().setImage(image) + + def _get_image(self, name): + """ Returns the QImage stored as the ImageResource with 'name'. + """ + document = self._control.document() + variant = document.resource(QtGui.QTextDocument.ImageResource, + QtCore.QUrl(name)) + return variant.toPyObject() + + def _save_image(self, name, format='PNG'): + """ Shows a save dialog for the ImageResource with 'name'. + """ + dialog = QtGui.QFileDialog(self._control, 'Save Image') + dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + dialog.setDefaultSuffix(format.lower()) + dialog.setNameFilter('%s file (*.%s)' % (format, format.lower())) + if dialog.exec_(): + filename = dialog.selectedFiles()[0] + image = self._get_image(name) + image.save(filename, format) diff --git a/IPython/frontend/qt/svg.py b/IPython/frontend/qt/svg.py new file mode 100644 index 0000000..23bb01d --- /dev/null +++ b/IPython/frontend/qt/svg.py @@ -0,0 +1,89 @@ +""" Defines utility functions for working with SVG documents in Qt. +""" + +# System library imports. +from PyQt4 import QtCore, QtGui, QtSvg + + +def save_svg(string, parent=None): + """ Prompts the user to save an SVG document to disk. + + Parameters: + ----------- + string : str + A Python string or QString containing a SVG document. + + parent : QWidget, optional + The parent to use for the file dialog. + + Returns: + -------- + The name of the file to which the document was saved, or None if the save + was cancelled. + """ + dialog = QtGui.QFileDialog(parent, 'Save SVG Document') + dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + dialog.setDefaultSuffix('svg') + dialog.setNameFilter('SVG document (*.svg)') + if dialog.exec_(): + filename = dialog.selectedFiles()[0] + f = open(filename, 'w') + try: + f.write(string) + finally: + f.close() + return filename + return None + +def svg_to_clipboard(string): + """ Copy a SVG document to the clipboard. + + Parameters: + ----------- + string : str + A Python string or QString containing a SVG document. + """ + if isinstance(string, basestring): + bytes = QtCore.QByteArray(string) + else: + bytes = string.toAscii() + mime_data = QtCore.QMimeData() + mime_data.setData('image/svg+xml', bytes) + QtGui.QApplication.clipboard().setMimeData(mime_data) + +def svg_to_image(string, size=None): + """ Convert a SVG document to a QImage. + + Parameters: + ----------- + string : str + A Python string or QString containing a SVG document. + + size : QSize, optional + The size of the image that is produced. If not specified, the SVG + document's default size is used. + + Raises: + ------- + ValueError + If an invalid SVG string is provided. + + Returns: + -------- + A QImage of format QImage.Format_ARGB32. + """ + if isinstance(string, basestring): + bytes = QtCore.QByteArray.fromRawData(string) # shallow copy + else: + bytes = string.toAscii() + + renderer = QtSvg.QSvgRenderer(bytes) + if not renderer.isValid(): + raise ValueError('Invalid SVG data.') + + if size is None: + size = renderer.defaultSize() + image = QtGui.QImage(size, QtGui.QImage.Format_ARGB32) + painter = QtGui.QPainter(image) + renderer.render(painter) + return image diff --git a/IPython/frontend/qt/util.py b/IPython/frontend/qt/util.py index ec76f25..3ea578b 100644 --- a/IPython/frontend/qt/util.py +++ b/IPython/frontend/qt/util.py @@ -2,7 +2,7 @@ """ # System library imports. -from PyQt4 import QtCore, QtGui +from PyQt4 import QtCore # IPython imports. from IPython.utils.traitlets import HasTraits @@ -23,42 +23,3 @@ class MetaQObjectHasTraits(MetaQObject, MetaHasTraits): QObject. See QtKernelManager for an example. """ pass - -#----------------------------------------------------------------------------- -# Functions -#----------------------------------------------------------------------------- - -def image_from_svg(string, size=None): - """ Convert a SVG document to a QImage. - - Parameters: - ----------- - string : str - A Python string containing a SVG document. - - size : QSize, optional - The size of the image that is produced. If not specified, the SVG - document's default size is used. - - Raises: - ------- - ValueError - If an invalid SVG string is provided. - - Returns: - -------- - A QImage of format QImage.Format_ARGB32. - """ - from PyQt4 import QtSvg - - bytes = QtCore.QByteArray.fromRawData(string) # shallow copy - renderer = QtSvg.QSvgRenderer(bytes) - if not renderer.isValid(): - raise ValueError('Invalid SVG data.') - - if size is None: - size = renderer.defaultSize() - image = QtGui.QImage(size, QtGui.QImage.Format_ARGB32) - painter = QtGui.QPainter(image) - renderer.render(painter) - return image