def __init__(self, use_ansi=True, **kwargs): """ Initialize. :return: """ super(LoggingConfigurable, self).__init__(**kwargs) # Text interaction self.name_to_svg_map = {} self.svg_warning = QtCore.QSemaphore() self.highlighter = SelectiveHighlighter(self, lexer=self.lexer) self.use_ansi = use_ansi self.setMouseTracking(True) if hasattr(self, 'setAcceptRichText'): self.setAcceptRichText(False) self.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.setReadOnly(False) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) # ConsoleWidget # Hijack the document size change signal to prevent Qt from adjusting # the viewport's scrollbar. We are relying on an implementation detail # of Q(Plain)TextEdit here, which is potentially dangerous, but without # this functionality we cannot create a nice terminal interface. layout = self.document().documentLayout() layout.documentSizeChanged.disconnect() layout.documentSizeChanged.connect(self.adjust_scrollbars) self.ansi_processor = QtAnsiCodeProcessor() # JupyterWidget # Initialize widget styling. if self.style_sheet: self._style_sheet_changed() self._syntax_style_changed() else: self.set_default_style() self.increase_font_size = QtGui.QAction("Bigger Font", self, shortcut=QtGui.QKeySequence.ZoomIn, shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut, statusTip="Increase the font size by one point", triggered=self._increase_font_size) self.addAction(self.increase_font_size) self.increase_font_size = QtGui.QAction("Bigger Font", self, shortcut="Ctrl+=", shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut, statusTip="Increase the font size by one point", triggered=self._increase_font_size) self.addAction(self.increase_font_size) self.decrease_font_size = QtGui.QAction("Smaller Font", self, shortcut=QtGui.QKeySequence.ZoomOut, shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut, statusTip="Decrease the font size by one point", triggered=self._decrease_font_size) self.addAction(self.decrease_font_size) self.reset_font_size = QtGui.QAction("Normal Font", self, shortcut="Ctrl+0", shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut, statusTip="Restore the Normal font size", triggered=self.reset_font) self.addAction(self.reset_font_size) action = QtGui.QAction('Print', None) action.setEnabled(True) print_key = QtGui.QKeySequence(QtGui.QKeySequence.Print) if print_key.matches("Ctrl+P") and sys.platform != 'darwin': # Only override the default if there is a collision. # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX. print_key = "Ctrl+Shift+P" action.setShortcut(print_key) action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut) action.triggered.connect(self._print_doc) self.addAction(action) self.print_action = action self.html_exporter = HtmlExporter(self) if isinstance(self, QtGui.QTextEdit): self.html_exporter.image_tag = self.get_image_tag action = QtGui.QAction('Save as HTML/XML', None) action.setShortcut(QtGui.QKeySequence.Save) action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.html_exporter.export) self.addAction(action) self.export_action = action action = QtGui.QAction('Select All', None) action.setEnabled(True) select_all = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll) if select_all.matches("Ctrl+A") and sys.platform != 'darwin': # Only override the default if there is a collision. # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX. select_all = "Ctrl+Shift+A" action.setShortcut(select_all) action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.selectAll) self.addAction(action) self.select_all_action = action # Set a monospaced font. self.reset_font() # Context menu self.customContextMenuRequested.connect( self._custom_context_menu_requested)
class DocumentConfig(LoggingConfigurable): """ Mixin for configuring text properties of a subclass of an editor class, QTextEdit or QPlainTextEdit. It should be initialized after the editor class has been initialized. """ highlighter = Instance(SelectiveHighlighter, allow_none=True) font_family = Unicode( config=True, help="""The font family to use for the console. On OSX this defaults to Monaco, on Windows the default is Consolas with fallback of Courier, and on other platforms the default is Monospace. """) font_size = Integer( config=True, help="The font size. If unconfigured, Qt will be entrusted with the size of the font.") standard_tab_width = Integer(4, config=True, help="Number of spaces used for tab.") style_sheet = Unicode(config=False, help=""" A CSS stylesheet. The stylesheet can contain classes for: 1. Qt: QPlainTextEdit, QFrame, QWidget, etc 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter) 3. ChatConsole: .error, .in-prompt, .out-prompt, etc """) syntax_style = Unicode(config=False, help=""" If not empty, use this Pygments style for syntax highlighting. Otherwise, the style sheet is queried for Pygments style information. """) lexer_class = DottedObjectName(config=True, help="The pygments lexer class to use." ) is_complete_timeout = Float(0.25, config=True, help="Seconds to wait for is_complete replies from the kernel." ) lexer = Any() ansi_processor = None # QtAnsiCodeProcessor increase_font_size = None # action for increasing font size decrease_font_size = None # action for decreasing font size reset_font_size = None # action for resetting font size html_exporter = None print_action = None # action for printing export_action = None # action for exporting select_all_action = None # action for selecting all use_ansi = True # whether to use ansi codes in text name_to_svg_map = None # Dictionary for resolving document resource names to SVG data. # RichJupyterWidget: # Used to determine whether a given html export attempt has already # displayed a warning about being unable to convert a png to svg. svg_warning = None # QSemaphore(0) def __init__(self, use_ansi=True, **kwargs): """ Initialize. :return: """ super(LoggingConfigurable, self).__init__(**kwargs) # Text interaction self.name_to_svg_map = {} self.svg_warning = QtCore.QSemaphore() self.highlighter = SelectiveHighlighter(self, lexer=self.lexer) self.use_ansi = use_ansi self.setMouseTracking(True) if hasattr(self, 'setAcceptRichText'): self.setAcceptRichText(False) self.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.setReadOnly(False) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) # ConsoleWidget # Hijack the document size change signal to prevent Qt from adjusting # the viewport's scrollbar. We are relying on an implementation detail # of Q(Plain)TextEdit here, which is potentially dangerous, but without # this functionality we cannot create a nice terminal interface. layout = self.document().documentLayout() layout.documentSizeChanged.disconnect() layout.documentSizeChanged.connect(self.adjust_scrollbars) self.ansi_processor = QtAnsiCodeProcessor() # JupyterWidget # Initialize widget styling. if self.style_sheet: self._style_sheet_changed() self._syntax_style_changed() else: self.set_default_style() self.increase_font_size = QtGui.QAction("Bigger Font", self, shortcut=QtGui.QKeySequence.ZoomIn, shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut, statusTip="Increase the font size by one point", triggered=self._increase_font_size) self.addAction(self.increase_font_size) self.increase_font_size = QtGui.QAction("Bigger Font", self, shortcut="Ctrl+=", shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut, statusTip="Increase the font size by one point", triggered=self._increase_font_size) self.addAction(self.increase_font_size) self.decrease_font_size = QtGui.QAction("Smaller Font", self, shortcut=QtGui.QKeySequence.ZoomOut, shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut, statusTip="Decrease the font size by one point", triggered=self._decrease_font_size) self.addAction(self.decrease_font_size) self.reset_font_size = QtGui.QAction("Normal Font", self, shortcut="Ctrl+0", shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut, statusTip="Restore the Normal font size", triggered=self.reset_font) self.addAction(self.reset_font_size) action = QtGui.QAction('Print', None) action.setEnabled(True) print_key = QtGui.QKeySequence(QtGui.QKeySequence.Print) if print_key.matches("Ctrl+P") and sys.platform != 'darwin': # Only override the default if there is a collision. # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX. print_key = "Ctrl+Shift+P" action.setShortcut(print_key) action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut) action.triggered.connect(self._print_doc) self.addAction(action) self.print_action = action self.html_exporter = HtmlExporter(self) if isinstance(self, QtGui.QTextEdit): self.html_exporter.image_tag = self.get_image_tag action = QtGui.QAction('Save as HTML/XML', None) action.setShortcut(QtGui.QKeySequence.Save) action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.html_exporter.export) self.addAction(action) self.export_action = action action = QtGui.QAction('Select All', None) action.setEnabled(True) select_all = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll) if select_all.matches("Ctrl+A") and sys.platform != 'darwin': # Only override the default if there is a collision. # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX. select_all = "Ctrl+Shift+A" action.setShortcut(select_all) action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.selectAll) self.addAction(action) self.select_all_action = action # Set a monospaced font. self.reset_font() # Context menu self.customContextMenuRequested.connect( self._custom_context_menu_requested) # ConsoleWidget def adjust_scrollbars(self): """ Expands the vertical scrollbar beyond the range set by Qt. """ # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp # and qtextedit.cpp. document = self.document() scrollbar = self.verticalScrollBar() viewport_height = self.viewport().height() if isinstance(self, QtGui.QPlainTextEdit): maximum = max(0, document.lineCount() - 1) step = viewport_height / self.fontMetrics().lineSpacing() else: # QTextEdit does not do line-based layout and blocks will not in # general have the same height. Therefore it does not make sense to # attempt to scroll in line height increments. maximum = document.size().height() step = viewport_height diff = maximum - scrollbar.maximum() scrollbar.setRange(0, maximum) scrollbar.setPageStep(step) # Compensate for undesirable scrolling that occurs automatically due to # maximumBlockCount() text truncation. if diff < 0 and document.blockCount() == document.maximumBlockCount(): scrollbar.setValue(scrollbar.value() + diff) @staticmethod def _font_family_default(): if sys.platform == 'win32': # Consolas ships with Vista/Win7, fallback to Courier if needed return 'Consolas' elif sys.platform == 'darwin': # OSX always has Monaco, no need for a fallback return 'Monaco' else: # Monospace should always exist, no need for a fallback return 'Monospace' def reset_font(self): """ Sets the font to the default fixed-width font for this platform. """ fallback = self._font_family_default() font = get_font(self.font_family, fallback) if self.font_size: font.setPointSize(self.font_size) else: font.setPointSize(QtGui.qApp.font().pointSize()) font.setStyleHint(QtGui.QFont.TypeWriter) self.set_font(font) def get_font(self): """ The base font being used. """ return self.document().defaultFont() def set_font(self, font): """ Sets the base font for the ConsoleWidget to the specified QFont. """ font_metrics = QtGui.QFontMetrics(font) self.setTabStopWidth(self.tab_width * font_metrics.width(' ')) self.document().setDefaultFont(font) font = property(get_font, set_font) def change_font_size(self, delta): """Change the font size by the specified amount (in points). """ font = self.font size = max(font.pointSize() + delta, 1) # minimum 1 point font.setPointSize(size) self.set_font(font) def _increase_font_size(self): self.change_font_size(1) def _decrease_font_size(self): self.change_font_size(-1) def _get_tab_width(self): """ The width (in terms of space characters) for tab characters. """ return self.standard_tab_width def _set_tab_width(self, tab_width): """ Sets the width (in terms of space characters) for tab characters. """ font_metrics = QtGui.QFontMetrics(self.font) self.setTabStopWidth(tab_width * font_metrics.width(' ')) self.standard_tab_width = tab_width tab_width = property(_get_tab_width, _set_tab_width) #JupyterWidget def set_default_style(self, colors='lightbg'): """ Sets the widget style to the class defaults. Parameters ---------- colors : str, optional (default lightbg) Whether to use the default light background or dark background or B&W style. """ colors = colors.lower() if colors=='lightbg': self.style_sheet = styles.default_light_style_sheet self.syntax_style = styles.default_light_syntax_style elif colors=='linux': self.style_sheet = styles.default_dark_style_sheet self.syntax_style = styles.default_dark_syntax_style elif colors=='nocolor': self.style_sheet = styles.default_bw_style_sheet self.syntax_style = styles.default_bw_syntax_style else: raise KeyError("No such color scheme: %s"%colors) def set_frame_color(self, new_color): """ Set the frame color according. :param new_color color to set. :return: """ new_palette = self.palette() # new_palette.setColor(QtGui.QPalette.WindowText, new_color) new_palette.setColor(QtGui.QPalette.Window, new_color) self.setPalette(new_palette) # traitlets # JupyterWidget def _style_sheet_changed(self): """ Set the style sheets of the underlying widgets. """ self.setStyleSheet(self.style_sheet) self.document().setDefaultStyleSheet(self.style_sheet) bg_color = self.palette().window().color() self.ansi_processor.set_background_color(bg_color) # if self._page_control is not None: # self._page_control.document().setDefaultStyleSheet(self.style_sheet) # JupyterWidget def _syntax_style_changed(self): """ Set the style for the syntax highlighter. """ if self.highlighter is None: # ignore premature calls return if self.syntax_style: self.highlighter.set_style(self.syntax_style) else: self.highlighter.set_style_sheet(self.style_sheet) # FrontendWidget def _lexer_class_changed(self, name, old, new): lexer_class = import_item(new) self.lexer = lexer_class() # FrontendWidget def _lexer_class_default(self): if py3compat.PY3: return 'pygments.lexers.Python3Lexer' else: return 'pygments.lexers.PythonLexer' # FrontendWidget def _lexer_default(self): lexer_class = import_item(self.lexer_class) return lexer_class() # ConsoleWidget def copy_anchor(self, anchor): """ Copy anchor text to the clipboard """ QtGui.QApplication.clipboard().setText(anchor) # ConsoleWidget def open_anchor(self, anchor): """ Open selected anchor in the default webbrowser """ webbrowser.open(anchor) @property def end_cursor(self): """ Return a text cursor at the end of the document. :param target: target editor object to get the cursor. :return: text cursor at the end of target. """ cursor = self.textCursor() cursor.movePosition(QtGui.QTextCursor.End) return cursor def insert_qimage(self, image_format, cursor=None): cursor = cursor if cursor else self.textCursor() insert_qimage_format(cursor, image_format) # RichJupyterWidget def get_image_tag(self, match, path = None, format = "png"): """ Return (X)HTML mark-up for the image-tag given by match. Parameters ---------- match : re.SRE_Match A match to an HTML image tag as exported by Qt, with match.group("Name") containing the matched image ID. path : string|None, optional [default None] If not None, specifies a path to which supporting files may be written (e.g., for linked images). If None, all images are to be included inline. format : "png"|"svg"|"jpg", optional [default "png"] Format for returned or referenced images. """ if format in ("png","jpg"): try: image = self.get_image(match.group("name")) except KeyError: return "<b>Couldn't find image %s</b>" % match.group("name") if path is not None: ensure_dir_exists(path) rel_path = os.path.basename(path) if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format), "PNG"): return '<img src="%s/qt_img%s.%s">' % (rel_path, match.group("name"),format) else: return "<b>Couldn't save image!</b>" else: ba = QtCore.QByteArray() buffer_ = QtCore.QBuffer(ba) buffer_.open(QtCore.QIODevice.WriteOnly) image.save(buffer_, format.upper()) buffer_.close() return '<img src="data:image/%s;base64,\n%s\n" />' % ( format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64()))) elif format == "svg": try: svg = str(self.name_to_svg_map[match.group("name")]) except KeyError: if self.svg_warning.tryAcquire(): QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.', 'Cannot convert PNG images to SVG, export with PNG figures instead. ' 'If you want to export matplotlib figures as SVG, add ' 'to your ipython config:\n\n' '\tc.InlineBackend.figure_format = \'svg\'\n\n' 'And regenerate the figures.', QtGui.QMessageBox.Ok) return ("<b>Cannot convert PNG images to SVG.</b> " "You must export this session with PNG images. " "If you want to export matplotlib figures as SVG, add to your config " "<span>c.InlineBackend.figure_format = 'svg'</span> " "and regenerate the figures.") # Not currently checking path, because it's tricky to find a # cross-browser way to embed external SVG images (e.g., via # object or embed tags). # Chop stand-alone header from matplotlib SVG offset = svg.find("<svg") assert(offset > -1) return svg[offset:] else: return '<b>Unrecognized image format</b>' # adopted from ConsoleWidget def insert_ansi_text(self, text, ansi_codes=True, cursor=None): cursor = cursor if cursor else self.textCursor() if ansi_codes: for substring in self.ansi_processor.split_string(text): for act in self.ansi_processor.actions: # Unlike real terminal emulators, we don't distinguish # between the screen and the scrollback buffer. A screen # erase request clears everything. if act.action == 'erase' and act.area == 'screen': cursor.select(QtGui.QTextCursor.Document) cursor.removeSelectedText() # Simulate a form feed by scrolling just past the last line. elif act.action == 'scroll' and act.unit == 'page': cursor.insertText('\n') # cursor.endEditBlock() set_top_cursor(self, cursor) # cursor.joinPreviousEditBlock() cursor.deletePreviousChar() elif act.action == 'carriage-return': cursor.movePosition( cursor.StartOfLine, cursor.KeepAnchor) elif act.action == 'beep': QtGui.qApp.beep() elif act.action == 'backspace': if not cursor.atBlockStart(): cursor.movePosition( cursor.PreviousCharacter, cursor.KeepAnchor) elif act.action == 'newline': cursor.movePosition(cursor.EndOfLine) ansi_format = self.ansi_processor.get_format() selection = cursor.selectedText() if len(selection) == 0: cursor.insertText(substring, ansi_format) elif substring is not None: # BS and CR are treated as a change in print # position, rather than a backwards character # deletion for output equivalence with (I)Python # terminal. if len(substring) >= len(selection): cursor.insertText(substring, ansi_format) else: old_text = selection[len(substring):] cursor.insertText(substring + old_text, ansi_format) cursor.movePosition(cursor.PreviousCharacter, cursor.KeepAnchor, len(old_text)) else: cursor.insertText(text) # ConsoleWidget def insert_html(self, html, cursor=None): """ Inserts HTML using the specified cursor in such a way that future formatting is unaffected. :param html: :param cursor: :return: """ cursor = cursor if cursor else self.textCursor() cursor.insertHtml(html) # Remark from qtconsole.console_widget: # After inserting HTML, the text document "remembers" it's in "html # mode", which means that subsequent calls adding plain text will result # in unwanted formatting, lost tab characters, etc. The following code # hacks around this behavior, which I consider to be a bug in Qt, by # (crudely) resetting the document's style state. cursor.movePosition(QtGui.QTextCursor.Left, QtGui.QTextCursor.KeepAnchor) if cursor.selection().toPlainText() == ' ': cursor.removeSelectedText() else: cursor.movePosition(QtGui.QTextCursor.Right) cursor.insertText(' ', QtGui.QTextCharFormat()) # ConsoleWdiget @property def word_start_cursor(self): """ Start of the word to the left of the current text cursor. If a sequence of non-word characters precedes the first word, skip over them. (This emulates the behavior of bash, emacs, etc.) """ cursor = self.textCursor() position = cursor.position() position -= 1 while position >= 0 and not is_letter_or_number(self.document().characterAt(position)): position -= 1 while position >= 0 and is_letter_or_number(self.document().characterAt(position)): position -= 1 cursor.setPosition(position + 1) return cursor # ConsoleWidget @property def word_end_cursor(self): """ End of the word to the right the current text cursor. If a sequence of non-word characters precedes the first word, skip over them. (This emulates the behavior of bash, emacs, etc.) """ cursor = self.textCursor() position = cursor.position() cursor.movePosition(QtGui.QTextCursor.End) end = cursor.position() while position < end and not is_letter_or_number(self.document().characterAt(position)): position += 1 while position < end and is_letter_or_number(self.document().characterAt(position)): position += 1 cursor = self.textCursor() cursor.setPosition(position) return cursor # ConsoleWidget def can_copy(self): """ Returns whether text can be copied to the clipboard. """ return self.textCursor().hasSelection() # ConsoleWidget def can_cut(self): """ Returns whether text can be cut to the clipboard. """ if not self.isReadOnly(): return self.textCursor().hasSelection() # ConsoleWidget def can_paste(self): """ Returns whether text can be pasted from the clipboard. """ paste_able = False if not self.isReadOnly(): paste_able = bool(QtGui.QApplication.clipboard().text()) return paste_able # ConsoleWidget def export_html(self): """ Shows a dialog to export HTML/XML in various formats. """ self.svg_warning.tryAcquire() # svg_warning can be 0 or 1. self.svg_warning.release() # svg warning should be 1 before export. self.html_exporter.export() # ConsoleWidget def _print_doc(self, printer=None): """ Print the contents of the ConsoleWidget to the specified QPrinter. """ if not printer: printer = QtGui.QPrinter() if QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted: return self.print_(printer) # ConsoleWidget def _custom_context_menu_requested(self, pos): """ Shows a context menu at the given QPoint (in widget coordinates). """ char_format = self.cursorForPosition(pos).charFormat() name = char_format.stringProperty(QtGui.QTextFormat.ImageName) if name: menu = ImageContextMenu(self, pos, name) else: menu = TextContextMenu(self, pos) menu.exec_(self.mapToGlobal(pos))