diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index d4f939e..354d080 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -7,6 +7,7 @@ # Standard library imports from os.path import commonprefix import re +import os import sys from textwrap import dedent @@ -160,10 +161,35 @@ class ConsoleWidget(Configurable, QtGui.QWidget): self._reading_callback = None self._tab_width = 8 self._text_completing_pos = 0 + self._filename = 'ipython.html' + self._png_mode=None # Set a monospaced font. self.reset_font() + # Configure actions. + action = QtGui.QAction('Print', None) + action.setEnabled(True) + action.setShortcut(QtGui.QKeySequence.Print) + action.triggered.connect(self.print_) + self.addAction(action) + self._print_action = action + + action = QtGui.QAction('Save as HTML/XML', None) + action.setEnabled(self.can_export()) + action.setShortcut(QtGui.QKeySequence.Save) + action.triggered.connect(self.export) + self.addAction(action) + self._export_action = action + + action = QtGui.QAction('Select All', None) + action.setEnabled(True) + action.setShortcut(QtGui.QKeySequence.SelectAll) + action.triggered.connect(self.select_all) + self.addAction(action) + self._select_all_action = action + + def eventFilter(self, obj, event): """ Reimplemented to ensure a console-like behavior in the underlying text widgets. @@ -300,6 +326,12 @@ class ConsoleWidget(Configurable, QtGui.QWidget): return not QtGui.QApplication.clipboard().text().isEmpty() return False + def can_export(self): + """Returns whether we can export. Currently only rich widgets + can export html. + """ + return self.kind == "rich" + def clear(self, keep_input=True): """ Clear the console. @@ -501,94 +533,152 @@ class ConsoleWidget(Configurable, QtGui.QWidget): def print_(self, printer = None): """ Print the contents of the ConsoleWidget to the specified QPrinter. """ - if(printer is None): + if (not printer): printer = QtGui.QPrinter() if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted): return self._control.print_(printer) - def export_html_inline(self, parent = None): - """ Export the contents of the ConsoleWidget as HTML with inline PNGs. - """ - self.export_html(parent, inline = True) - - def export_html(self, parent = None, inline = False): + def export(self, parent = None): + """Export HTML/XML in various modes from one Dialog.""" + parent = parent or None # sometimes parent is False + dialog = QtGui.QFileDialog(parent, 'Save Console as...') + dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + filters = [ + 'HTML with PNG figures (*.html *.htm)', + 'XHTML with inline SVG figures (*.xhtml *.xml)' + ] + dialog.setNameFilters(filters) + if self._filename: + dialog.selectFile(self._filename) + root,ext = os.path.splitext(self._filename) + if ext.lower() in ('.xml', '.xhtml'): + dialog.selectNameFilter(filters[-1]) + if dialog.exec_(): + filename = str(dialog.selectedFiles()[0]) + self._filename = filename + choice = str(dialog.selectedNameFilter()) + + if choice.startswith('XHTML'): + exporter = self.export_xhtml + else: + exporter = self.export_html + + try: + return exporter(filename) + except Exception, e: + title = self.window().windowTitle() + msg = "Error while saving to: %s\n"%filename+str(e) + reply = QtGui.QMessageBox.warning(self, title, msg, + QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok) + return None + + def export_html(self, filename): """ Export the contents of the ConsoleWidget as HTML. Parameters: ----------- + filename : str + The file to be saved. inline : bool, optional [default True] - If True, include images as inline PNGs. Otherwise, include them as links to external PNG files, mimicking - Firefox's "Web Page, complete" behavior. - """ - dialog = QtGui.QFileDialog(parent, 'Save HTML Document') - dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - dialog.setDefaultSuffix('htm') - dialog.setNameFilter('HTML document (*.htm)') - if dialog.exec_(): - filename = str(dialog.selectedFiles()[0]) - if(inline): - path = None + web browsers' "Web Page, Complete" behavior. + """ + # N.B. this is overly restrictive, but Qt's output is + # predictable... + img_re = re.compile(r'') + html = self.fix_html_encoding( + str(self._control.toHtml().toUtf8())) + if self._png_mode: + # preference saved, don't ask again + if img_re.search(html): + inline = (self._png_mode == 'inline') else: - offset = filename.rfind(".") - if(offset > 0): - path = filename[:offset]+"_files" + inline = True + elif img_re.search(html): + # there are images + widget = QtGui.QWidget() + layout = QtGui.QVBoxLayout(widget) + title = self.window().windowTitle() + msg = "Exporting HTML with PNGs" + info = "Would you like inline PNGs (single large html file) or "+\ + "external image files?" + checkbox = QtGui.QCheckBox("&Don't ask again") + checkbox.setShortcut('D') + ib = QtGui.QPushButton("&Inline", self) + ib.setShortcut('I') + eb = QtGui.QPushButton("&External", self) + eb.setShortcut('E') + box = QtGui.QMessageBox(QtGui.QMessageBox.Question, title, msg) + box.setInformativeText(info) + box.addButton(ib,QtGui.QMessageBox.NoRole) + box.addButton(eb,QtGui.QMessageBox.YesRole) + box.setDefaultButton(ib) + layout.setSpacing(0) + layout.addWidget(box) + layout.addWidget(checkbox) + widget.setLayout(layout) + widget.show() + reply = box.exec_() + inline = (reply == 0) + if checkbox.checkState(): + # don't ask anymore, always use this choice + if inline: + self._png_mode='inline' else: - path = filename+"_files" - import os - try: - os.mkdir(path) - except OSError: - # TODO: check that this is an "already exists" error - pass - - f = open(filename, 'w') - try: - # N.B. this is overly restrictive, but Qt's output is - # predictable... - img_re = re.compile(r'') - html = self.fix_html_encoding( - str(self._control.toHtml().toUtf8())) - f.write(img_re.sub( - lambda x: self.image_tag(x, path = path, format = "png"), - html)) - finally: - f.close() - return filename - return None + self._png_mode='external' + else: + # no images + inline = True + + if inline: + path = None + else: + root,ext = os.path.splitext(filename) + path = root+"_files" + if os.path.isfile(path): + raise OSError("%s exists, but is not a directory."%path) + + f = open(filename, 'w') + try: + f.write(img_re.sub( + lambda x: self.image_tag(x, path = path, format = "png"), + html)) + except Exception, e: + f.close() + raise e + else: + f.close() + return filename - def export_xhtml(self, parent = None): + + def export_xhtml(self, filename): """ Export the contents of the ConsoleWidget as XHTML with inline SVGs. """ - dialog = QtGui.QFileDialog(parent, 'Save XHTML Document') - dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - dialog.setDefaultSuffix('xml') - dialog.setNameFilter('XHTML document (*.xml)') - if dialog.exec_(): - filename = str(dialog.selectedFiles()[0]) - f = open(filename, 'w') - try: - # N.B. this is overly restrictive, but Qt's output is - # predictable... - img_re = re.compile(r'') - html = str(self._control.toHtml().toUtf8()) - # Hack to make xhtml header -- note that we are not doing - # any check for valid xml - offset = html.find("") - assert(offset > -1) - html = ('\n'+ - html[offset+6:]) - # And now declare UTF-8 encoding - html = self.fix_html_encoding(html) - f.write(img_re.sub( - lambda x: self.image_tag(x, path = None, format = "svg"), - html)) - finally: - f.close() - return filename - return None + f = open(filename, 'w') + try: + # N.B. this is overly restrictive, but Qt's output is + # predictable... + img_re = re.compile(r'') + html = str(self._control.toHtml().toUtf8()) + # Hack to make xhtml header -- note that we are not doing + # any check for valid xml + offset = html.find("") + assert(offset > -1) + html = ('\n'+ + html[offset+6:]) + # And now declare UTF-8 encoding + html = self.fix_html_encoding(html) + f.write(img_re.sub( + lambda x: self.image_tag(x, path = None, format = "svg"), + html)) + except Exception, e: + f.close() + raise e + else: + f.close() + return filename def fix_html_encoding(self, html): """ Return html string, with a UTF-8 declaration added to . @@ -854,7 +944,7 @@ class ConsoleWidget(Configurable, QtGui.QWidget): def _context_menu_make(self, pos): """ Creates a context menu for the given QPoint (in widget coordinates). """ - menu = QtGui.QMenu() + menu = QtGui.QMenu(self) cut_action = menu.addAction('Cut', self.cut) cut_action.setEnabled(self.can_cut()) @@ -869,23 +959,15 @@ class ConsoleWidget(Configurable, QtGui.QWidget): paste_action.setShortcut(QtGui.QKeySequence.Paste) menu.addSeparator() - menu.addAction('Select All', self.select_all) + menu.addAction(self._select_all_action) menu.addSeparator() - print_action = menu.addAction('Print', self.print_) - print_action.setEnabled(True) - html_action = menu.addAction('Export HTML (external PNGs)', - self.export_html) - html_action.setEnabled(True) - html_inline_action = menu.addAction('Export HTML (inline PNGs)', - self.export_html_inline) - html_inline_action.setEnabled(True) - xhtml_action = menu.addAction('Export XHTML (inline SVGs)', - self.export_xhtml) - xhtml_action.setEnabled(True) + menu.addAction(self._export_action) + menu.addAction(self._print_action) + return menu - def _control_key_down(self, modifiers, include_command=True): + def _control_key_down(self, modifiers, include_command=False): """ Given a KeyboardModifiers flags object, return whether the Control key is down. diff --git a/IPython/frontend/qt/console/rich_ipython_widget.py b/IPython/frontend/qt/console/rich_ipython_widget.py index 0b6e69d..98d0cb1 100644 --- a/IPython/frontend/qt/console/rich_ipython_widget.py +++ b/IPython/frontend/qt/console/rich_ipython_widget.py @@ -1,4 +1,6 @@ # System library imports +import os +import re from PyQt4 import QtCore, QtGui # Local imports @@ -154,7 +156,9 @@ class RichIPythonWidget(IPythonWidget): return "Couldn't find image %s" % match.group("name") if(path is not None): - relpath = path[path.rfind("/")+1:] + if not os.path.exists(path): + os.mkdir(path) + relpath = os.path.basename(path) if(image.save("%s/qt_img%s.png" % (path,match.group("name")), "PNG")): return '' % (relpath, @@ -167,7 +171,6 @@ class RichIPythonWidget(IPythonWidget): buffer_.open(QtCore.QIODevice.WriteOnly) image.save(buffer_, "PNG") buffer_.close() - import re return '' % ( re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))