Show More
@@ -7,6 +7,7 b'' | |||||
7 | # Standard library imports |
|
7 | # Standard library imports | |
8 | from os.path import commonprefix |
|
8 | from os.path import commonprefix | |
9 | import re |
|
9 | import re | |
|
10 | import os | |||
10 | import sys |
|
11 | import sys | |
11 | from textwrap import dedent |
|
12 | from textwrap import dedent | |
12 |
|
13 | |||
@@ -160,10 +161,35 b' class ConsoleWidget(Configurable, QtGui.QWidget):' | |||||
160 | self._reading_callback = None |
|
161 | self._reading_callback = None | |
161 | self._tab_width = 8 |
|
162 | self._tab_width = 8 | |
162 | self._text_completing_pos = 0 |
|
163 | self._text_completing_pos = 0 | |
|
164 | self._filename = 'ipython.html' | |||
|
165 | self._png_mode=None | |||
163 |
|
166 | |||
164 | # Set a monospaced font. |
|
167 | # Set a monospaced font. | |
165 | self.reset_font() |
|
168 | self.reset_font() | |
166 |
|
169 | |||
|
170 | # Configure actions. | |||
|
171 | action = QtGui.QAction('Print', None) | |||
|
172 | action.setEnabled(True) | |||
|
173 | action.setShortcut(QtGui.QKeySequence.Print) | |||
|
174 | action.triggered.connect(self.print_) | |||
|
175 | self.addAction(action) | |||
|
176 | self._print_action = action | |||
|
177 | ||||
|
178 | action = QtGui.QAction('Save as HTML/XML', None) | |||
|
179 | action.setEnabled(self.can_export()) | |||
|
180 | action.setShortcut(QtGui.QKeySequence.Save) | |||
|
181 | action.triggered.connect(self.export) | |||
|
182 | self.addAction(action) | |||
|
183 | self._export_action = action | |||
|
184 | ||||
|
185 | action = QtGui.QAction('Select All', None) | |||
|
186 | action.setEnabled(True) | |||
|
187 | action.setShortcut(QtGui.QKeySequence.SelectAll) | |||
|
188 | action.triggered.connect(self.select_all) | |||
|
189 | self.addAction(action) | |||
|
190 | self._select_all_action = action | |||
|
191 | ||||
|
192 | ||||
167 | def eventFilter(self, obj, event): |
|
193 | def eventFilter(self, obj, event): | |
168 | """ Reimplemented to ensure a console-like behavior in the underlying |
|
194 | """ Reimplemented to ensure a console-like behavior in the underlying | |
169 | text widgets. |
|
195 | text widgets. | |
@@ -300,6 +326,12 b' class ConsoleWidget(Configurable, QtGui.QWidget):' | |||||
300 | return not QtGui.QApplication.clipboard().text().isEmpty() |
|
326 | return not QtGui.QApplication.clipboard().text().isEmpty() | |
301 | return False |
|
327 | return False | |
302 |
|
328 | |||
|
329 | def can_export(self): | |||
|
330 | """Returns whether we can export. Currently only rich widgets | |||
|
331 | can export html. | |||
|
332 | """ | |||
|
333 | return self.kind == "rich" | |||
|
334 | ||||
303 | def clear(self, keep_input=True): |
|
335 | def clear(self, keep_input=True): | |
304 | """ Clear the console. |
|
336 | """ Clear the console. | |
305 |
|
337 | |||
@@ -501,94 +533,152 b' class ConsoleWidget(Configurable, QtGui.QWidget):' | |||||
501 | def print_(self, printer = None): |
|
533 | def print_(self, printer = None): | |
502 | """ Print the contents of the ConsoleWidget to the specified QPrinter. |
|
534 | """ Print the contents of the ConsoleWidget to the specified QPrinter. | |
503 | """ |
|
535 | """ | |
504 |
if(printer |
|
536 | if (not printer): | |
505 | printer = QtGui.QPrinter() |
|
537 | printer = QtGui.QPrinter() | |
506 | if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted): |
|
538 | if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted): | |
507 | return |
|
539 | return | |
508 | self._control.print_(printer) |
|
540 | self._control.print_(printer) | |
509 |
|
541 | |||
510 |
def export |
|
542 | def export(self, parent = None): | |
511 | """ Export the contents of the ConsoleWidget as HTML with inline PNGs. |
|
543 | """Export HTML/XML in various modes from one Dialog.""" | |
512 | """ |
|
544 | parent = parent or None # sometimes parent is False | |
513 | self.export_html(parent, inline = True) |
|
545 | dialog = QtGui.QFileDialog(parent, 'Save Console as...') | |
514 |
|
546 | dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) | ||
515 | def export_html(self, parent = None, inline = False): |
|
547 | filters = [ | |
|
548 | 'HTML with PNG figures (*.html *.htm)', | |||
|
549 | 'XHTML with inline SVG figures (*.xhtml *.xml)' | |||
|
550 | ] | |||
|
551 | dialog.setNameFilters(filters) | |||
|
552 | if self._filename: | |||
|
553 | dialog.selectFile(self._filename) | |||
|
554 | root,ext = os.path.splitext(self._filename) | |||
|
555 | if ext.lower() in ('.xml', '.xhtml'): | |||
|
556 | dialog.selectNameFilter(filters[-1]) | |||
|
557 | if dialog.exec_(): | |||
|
558 | filename = str(dialog.selectedFiles()[0]) | |||
|
559 | self._filename = filename | |||
|
560 | choice = str(dialog.selectedNameFilter()) | |||
|
561 | ||||
|
562 | if choice.startswith('XHTML'): | |||
|
563 | exporter = self.export_xhtml | |||
|
564 | else: | |||
|
565 | exporter = self.export_html | |||
|
566 | ||||
|
567 | try: | |||
|
568 | return exporter(filename) | |||
|
569 | except Exception, e: | |||
|
570 | title = self.window().windowTitle() | |||
|
571 | msg = "Error while saving to: %s\n"%filename+str(e) | |||
|
572 | reply = QtGui.QMessageBox.warning(self, title, msg, | |||
|
573 | QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok) | |||
|
574 | return None | |||
|
575 | ||||
|
576 | def export_html(self, filename): | |||
516 | """ Export the contents of the ConsoleWidget as HTML. |
|
577 | """ Export the contents of the ConsoleWidget as HTML. | |
517 |
|
578 | |||
518 | Parameters: |
|
579 | Parameters: | |
519 | ----------- |
|
580 | ----------- | |
|
581 | filename : str | |||
|
582 | The file to be saved. | |||
520 | inline : bool, optional [default True] |
|
583 | inline : bool, optional [default True] | |
521 |
|
||||
522 | If True, include images as inline PNGs. Otherwise, |
|
584 | If True, include images as inline PNGs. Otherwise, | |
523 | include them as links to external PNG files, mimicking |
|
585 | include them as links to external PNG files, mimicking | |
524 |
|
|
586 | web browsers' "Web Page, Complete" behavior. | |
525 | """ |
|
587 | """ | |
526 | dialog = QtGui.QFileDialog(parent, 'Save HTML Document') |
|
588 | # N.B. this is overly restrictive, but Qt's output is | |
527 | dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) |
|
589 | # predictable... | |
528 | dialog.setDefaultSuffix('htm') |
|
590 | img_re = re.compile(r'<img src="(?P<name>[\d]+)" />') | |
529 | dialog.setNameFilter('HTML document (*.htm)') |
|
591 | html = self.fix_html_encoding( | |
530 | if dialog.exec_(): |
|
592 | str(self._control.toHtml().toUtf8())) | |
531 | filename = str(dialog.selectedFiles()[0]) |
|
593 | if self._png_mode: | |
532 | if(inline): |
|
594 | # preference saved, don't ask again | |
533 | path = None |
|
595 | if img_re.search(html): | |
|
596 | inline = (self._png_mode == 'inline') | |||
534 | else: |
|
597 | else: | |
535 | offset = filename.rfind(".") |
|
598 | inline = True | |
536 | if(offset > 0): |
|
599 | elif img_re.search(html): | |
537 | path = filename[:offset]+"_files" |
|
600 | # there are images | |
|
601 | widget = QtGui.QWidget() | |||
|
602 | layout = QtGui.QVBoxLayout(widget) | |||
|
603 | title = self.window().windowTitle() | |||
|
604 | msg = "Exporting HTML with PNGs" | |||
|
605 | info = "Would you like inline PNGs (single large html file) or "+\ | |||
|
606 | "external image files?" | |||
|
607 | checkbox = QtGui.QCheckBox("&Don't ask again") | |||
|
608 | checkbox.setShortcut('D') | |||
|
609 | ib = QtGui.QPushButton("&Inline", self) | |||
|
610 | ib.setShortcut('I') | |||
|
611 | eb = QtGui.QPushButton("&External", self) | |||
|
612 | eb.setShortcut('E') | |||
|
613 | box = QtGui.QMessageBox(QtGui.QMessageBox.Question, title, msg) | |||
|
614 | box.setInformativeText(info) | |||
|
615 | box.addButton(ib,QtGui.QMessageBox.NoRole) | |||
|
616 | box.addButton(eb,QtGui.QMessageBox.YesRole) | |||
|
617 | box.setDefaultButton(ib) | |||
|
618 | layout.setSpacing(0) | |||
|
619 | layout.addWidget(box) | |||
|
620 | layout.addWidget(checkbox) | |||
|
621 | widget.setLayout(layout) | |||
|
622 | widget.show() | |||
|
623 | reply = box.exec_() | |||
|
624 | inline = (reply == 0) | |||
|
625 | if checkbox.checkState(): | |||
|
626 | # don't ask anymore, always use this choice | |||
|
627 | if inline: | |||
|
628 | self._png_mode='inline' | |||
538 | else: |
|
629 | else: | |
539 | path = filename+"_files" |
|
630 | self._png_mode='external' | |
540 | import os |
|
631 | else: | |
541 |
|
|
632 | # no images | |
542 | os.mkdir(path) |
|
633 | inline = True | |
543 | except OSError: |
|
634 | ||
544 | # TODO: check that this is an "already exists" error |
|
635 | if inline: | |
545 | pass |
|
636 | path = None | |
546 |
|
637 | else: | ||
547 |
|
|
638 | root,ext = os.path.splitext(filename) | |
548 | try: |
|
639 | path = root+"_files" | |
549 | # N.B. this is overly restrictive, but Qt's output is |
|
640 | if os.path.isfile(path): | |
550 | # predictable... |
|
641 | raise OSError("%s exists, but is not a directory."%path) | |
551 | img_re = re.compile(r'<img src="(?P<name>[\d]+)" />') |
|
642 | ||
552 | html = self.fix_html_encoding( |
|
643 | f = open(filename, 'w') | |
553 | str(self._control.toHtml().toUtf8())) |
|
644 | try: | |
554 |
|
|
645 | f.write(img_re.sub( | |
555 |
|
|
646 | lambda x: self.image_tag(x, path = path, format = "png"), | |
556 |
|
|
647 | html)) | |
557 | finally: |
|
648 | except Exception, e: | |
558 |
|
|
649 | f.close() | |
559 |
re |
|
650 | raise e | |
560 |
|
|
651 | else: | |
|
652 | f.close() | |||
|
653 | return filename | |||
561 |
|
654 | |||
562 | def export_xhtml(self, parent = None): |
|
655 | ||
|
656 | def export_xhtml(self, filename): | |||
563 | """ Export the contents of the ConsoleWidget as XHTML with inline SVGs. |
|
657 | """ Export the contents of the ConsoleWidget as XHTML with inline SVGs. | |
564 | """ |
|
658 | """ | |
565 | dialog = QtGui.QFileDialog(parent, 'Save XHTML Document') |
|
659 | f = open(filename, 'w') | |
566 | dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) |
|
660 | try: | |
567 | dialog.setDefaultSuffix('xml') |
|
661 | # N.B. this is overly restrictive, but Qt's output is | |
568 | dialog.setNameFilter('XHTML document (*.xml)') |
|
662 | # predictable... | |
569 | if dialog.exec_(): |
|
663 | img_re = re.compile(r'<img src="(?P<name>[\d]+)" />') | |
570 | filename = str(dialog.selectedFiles()[0]) |
|
664 | html = str(self._control.toHtml().toUtf8()) | |
571 | f = open(filename, 'w') |
|
665 | # Hack to make xhtml header -- note that we are not doing | |
572 | try: |
|
666 | # any check for valid xml | |
573 | # N.B. this is overly restrictive, but Qt's output is |
|
667 | offset = html.find("<html>") | |
574 | # predictable... |
|
668 | assert(offset > -1) | |
575 | img_re = re.compile(r'<img src="(?P<name>[\d]+)" />') |
|
669 | html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+ | |
576 | html = str(self._control.toHtml().toUtf8()) |
|
670 | html[offset+6:]) | |
577 | # Hack to make xhtml header -- note that we are not doing |
|
671 | # And now declare UTF-8 encoding | |
578 | # any check for valid xml |
|
672 | html = self.fix_html_encoding(html) | |
579 | offset = html.find("<html>") |
|
673 | f.write(img_re.sub( | |
580 | assert(offset > -1) |
|
674 | lambda x: self.image_tag(x, path = None, format = "svg"), | |
581 | html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+ |
|
675 | html)) | |
582 | html[offset+6:]) |
|
676 | except Exception, e: | |
583 | # And now declare UTF-8 encoding |
|
677 | f.close() | |
584 | html = self.fix_html_encoding(html) |
|
678 | raise e | |
585 | f.write(img_re.sub( |
|
679 | else: | |
586 | lambda x: self.image_tag(x, path = None, format = "svg"), |
|
680 | f.close() | |
587 | html)) |
|
681 | return filename | |
588 | finally: |
|
|||
589 | f.close() |
|
|||
590 | return filename |
|
|||
591 | return None |
|
|||
592 |
|
682 | |||
593 | def fix_html_encoding(self, html): |
|
683 | def fix_html_encoding(self, html): | |
594 | """ Return html string, with a UTF-8 declaration added to <HEAD>. |
|
684 | """ Return html string, with a UTF-8 declaration added to <HEAD>. | |
@@ -854,7 +944,7 b' class ConsoleWidget(Configurable, QtGui.QWidget):' | |||||
854 | def _context_menu_make(self, pos): |
|
944 | def _context_menu_make(self, pos): | |
855 | """ Creates a context menu for the given QPoint (in widget coordinates). |
|
945 | """ Creates a context menu for the given QPoint (in widget coordinates). | |
856 | """ |
|
946 | """ | |
857 | menu = QtGui.QMenu() |
|
947 | menu = QtGui.QMenu(self) | |
858 |
|
948 | |||
859 | cut_action = menu.addAction('Cut', self.cut) |
|
949 | cut_action = menu.addAction('Cut', self.cut) | |
860 | cut_action.setEnabled(self.can_cut()) |
|
950 | cut_action.setEnabled(self.can_cut()) | |
@@ -869,23 +959,15 b' class ConsoleWidget(Configurable, QtGui.QWidget):' | |||||
869 | paste_action.setShortcut(QtGui.QKeySequence.Paste) |
|
959 | paste_action.setShortcut(QtGui.QKeySequence.Paste) | |
870 |
|
960 | |||
871 | menu.addSeparator() |
|
961 | menu.addSeparator() | |
872 |
menu.addAction( |
|
962 | menu.addAction(self._select_all_action) | |
873 |
|
963 | |||
874 | menu.addSeparator() |
|
964 | menu.addSeparator() | |
875 |
|
|
965 | menu.addAction(self._export_action) | |
876 | print_action.setEnabled(True) |
|
966 | menu.addAction(self._print_action) | |
877 | html_action = menu.addAction('Export HTML (external PNGs)', |
|
967 | ||
878 | self.export_html) |
|
|||
879 | html_action.setEnabled(True) |
|
|||
880 | html_inline_action = menu.addAction('Export HTML (inline PNGs)', |
|
|||
881 | self.export_html_inline) |
|
|||
882 | html_inline_action.setEnabled(True) |
|
|||
883 | xhtml_action = menu.addAction('Export XHTML (inline SVGs)', |
|
|||
884 | self.export_xhtml) |
|
|||
885 | xhtml_action.setEnabled(True) |
|
|||
886 | return menu |
|
968 | return menu | |
887 |
|
969 | |||
888 |
def _control_key_down(self, modifiers, include_command= |
|
970 | def _control_key_down(self, modifiers, include_command=False): | |
889 | """ Given a KeyboardModifiers flags object, return whether the Control |
|
971 | """ Given a KeyboardModifiers flags object, return whether the Control | |
890 | key is down. |
|
972 | key is down. | |
891 |
|
973 |
@@ -1,4 +1,6 b'' | |||||
1 | # System library imports |
|
1 | # System library imports | |
|
2 | import os | |||
|
3 | import re | |||
2 | from PyQt4 import QtCore, QtGui |
|
4 | from PyQt4 import QtCore, QtGui | |
3 |
|
5 | |||
4 | # Local imports |
|
6 | # Local imports | |
@@ -154,7 +156,9 b' class RichIPythonWidget(IPythonWidget):' | |||||
154 | return "<b>Couldn't find image %s</b>" % match.group("name") |
|
156 | return "<b>Couldn't find image %s</b>" % match.group("name") | |
155 |
|
157 | |||
156 | if(path is not None): |
|
158 | if(path is not None): | |
157 | relpath = path[path.rfind("/")+1:] |
|
159 | if not os.path.exists(path): | |
|
160 | os.mkdir(path) | |||
|
161 | relpath = os.path.basename(path) | |||
158 | if(image.save("%s/qt_img%s.png" % (path,match.group("name")), |
|
162 | if(image.save("%s/qt_img%s.png" % (path,match.group("name")), | |
159 | "PNG")): |
|
163 | "PNG")): | |
160 | return '<img src="%s/qt_img%s.png">' % (relpath, |
|
164 | return '<img src="%s/qt_img%s.png">' % (relpath, | |
@@ -167,7 +171,6 b' class RichIPythonWidget(IPythonWidget):' | |||||
167 | buffer_.open(QtCore.QIODevice.WriteOnly) |
|
171 | buffer_.open(QtCore.QIODevice.WriteOnly) | |
168 | image.save(buffer_, "PNG") |
|
172 | image.save(buffer_, "PNG") | |
169 | buffer_.close() |
|
173 | buffer_.close() | |
170 | import re |
|
|||
171 | return '<img src="data:image/png;base64,\n%s\n" />' % ( |
|
174 | return '<img src="data:image/png;base64,\n%s\n" />' % ( | |
172 | re.sub(r'(.{60})',r'\1\n',str(ba.toBase64()))) |
|
175 | re.sub(r'(.{60})',r'\1\n',str(ba.toBase64()))) | |
173 |
|
176 |
General Comments 0
You need to be logged in to leave comments.
Login now