diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index 7308116..075ef99 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -9,7 +9,7 @@ from ansi_code_processor import QtAnsiCodeProcessor from completion_widget import CompletionWidget -class ConsoleWidget(QtGui.QPlainTextEdit): +class ConsoleWidget(QtGui.QWidget): """ Base class for console-type widgets. This class is mainly concerned with dealing with the prompt, keeping the cursor inside the editing line, and handling ANSI escape sequences. @@ -29,6 +29,11 @@ class ConsoleWidget(QtGui.QPlainTextEdit): # priority (when it has focus) over, e.g., window-level menu shortcuts. override_shortcuts = False + # Signals that indicate ConsoleWidget state. + copy_available = QtCore.pyqtSignal(bool) + redo_available = QtCore.pyqtSignal(bool) + undo_available = QtCore.pyqtSignal(bool) + # Protected class variables. _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left, QtCore.Qt.Key_F : QtCore.Qt.Key_Right, @@ -44,13 +49,28 @@ class ConsoleWidget(QtGui.QPlainTextEdit): # 'QObject' interface #--------------------------------------------------------------------------- - def __init__(self, parent=None): - QtGui.QPlainTextEdit.__init__(self, parent) + def __init__(self, kind='plain', parent=None): + """ Create a ConsoleWidget. + + Parameters + ---------- + kind : str, optional [default 'plain'] + The type of text widget to use. Valid values are 'plain', which + specifies a QPlainTextEdit, and 'rich', which specifies an + QTextEdit. + + parent : QWidget, optional [default None] + The parent for this widget. + """ + super(ConsoleWidget, self).__init__(parent) + + # Create the underlying text widget. + self._control = self._create_control(kind) # Initialize protected variables. Some variables contain useful state # information for subclasses; they should be considered read-only. self._ansi_processor = QtAnsiCodeProcessor() - self._completion_widget = CompletionWidget(self) + self._completion_widget = CompletionWidget(self._control) self._continuation_prompt = '> ' self._continuation_prompt_html = None self._executing = False @@ -64,249 +84,51 @@ class ConsoleWidget(QtGui.QPlainTextEdit): # Set a monospaced font. self.reset_font() - # Define a custom context menu. - self._context_menu = QtGui.QMenu(self) - - copy_action = QtGui.QAction('Copy', self) - copy_action.triggered.connect(self.copy) - self.copyAvailable.connect(copy_action.setEnabled) - self._context_menu.addAction(copy_action) - - self._paste_action = QtGui.QAction('Paste', self) - self._paste_action.triggered.connect(self.paste) - self._context_menu.addAction(self._paste_action) - self._context_menu.addSeparator() - - select_all_action = QtGui.QAction('Select All', self) - select_all_action.triggered.connect(self.selectAll) - self._context_menu.addAction(select_all_action) - - def event(self, event): - """ Reimplemented to override shortcuts, if necessary. - """ - # On Mac OS, it is always unnecessary to override shortcuts, hence the - # check below. Users should just use the Control key instead of the - # Command key. - if self.override_shortcuts and \ - sys.platform != 'darwin' and \ - event.type() == QtCore.QEvent.ShortcutOverride and \ - self._control_down(event.modifiers()) and \ - event.key() in self._shortcuts: - event.accept() - return True - else: - return QtGui.QPlainTextEdit.event(self, event) - - #--------------------------------------------------------------------------- - # 'QWidget' interface - #--------------------------------------------------------------------------- - - def contextMenuEvent(self, event): - """ Reimplemented to create a menu without destructive actions like - 'Cut' and 'Delete'. - """ - clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty() - self._paste_action.setEnabled(not clipboard_empty) - - self._context_menu.exec_(event.globalPos()) - - def dragMoveEvent(self, event): - """ Reimplemented to disable moving text by drag and drop. - """ - event.ignore() - - def keyPressEvent(self, event): - """ Reimplemented to create a console-like interface. + def eventFilter(self, obj, event): + """ Reimplemented to ensure a console-like behavior in the underlying + text widget. """ - intercepted = False - cursor = self.textCursor() - position = cursor.position() - key = event.key() - ctrl_down = self._control_down(event.modifiers()) - alt_down = event.modifiers() & QtCore.Qt.AltModifier - shift_down = event.modifiers() & QtCore.Qt.ShiftModifier - - # Even though we have reimplemented 'paste', the C++ level slot is still - # called by Qt. So we intercept the key press here. - if event.matches(QtGui.QKeySequence.Paste): - self.paste() - intercepted = True - - elif ctrl_down: - if key in self._ctrl_down_remap: - ctrl_down = False - key = self._ctrl_down_remap[key] - event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key, - QtCore.Qt.NoModifier) - - elif key == QtCore.Qt.Key_K: - if self._in_buffer(position): - cursor.movePosition(QtGui.QTextCursor.EndOfLine, - QtGui.QTextCursor.KeepAnchor) - cursor.removeSelectedText() - intercepted = True - - elif key == QtCore.Qt.Key_X: - intercepted = True - - elif key == QtCore.Qt.Key_Y: - self.paste() - intercepted = True - - elif alt_down: - if key == QtCore.Qt.Key_B: - self.setTextCursor(self._get_word_start_cursor(position)) - intercepted = True - - elif key == QtCore.Qt.Key_F: - self.setTextCursor(self._get_word_end_cursor(position)) - intercepted = True - - elif key == QtCore.Qt.Key_Backspace: - cursor = self._get_word_start_cursor(position) - cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) - cursor.removeSelectedText() - intercepted = True - - elif key == QtCore.Qt.Key_D: - cursor = self._get_word_end_cursor(position) - cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) - cursor.removeSelectedText() - intercepted = True - - if self._completion_widget.isVisible(): - self._completion_widget.keyPressEvent(event) - intercepted = event.isAccepted() - - else: - if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): - if self._reading: - self.appendPlainText('\n') - self._reading = False - if self._reading_callback: - self._reading_callback() - elif not self._executing: - self.execute(interactive=True) - intercepted = True - - elif key == QtCore.Qt.Key_Up: - if self._reading or not self._up_pressed(): - intercepted = True - else: - prompt_line = self._get_prompt_cursor().blockNumber() - intercepted = cursor.blockNumber() <= prompt_line - - elif key == QtCore.Qt.Key_Down: - if self._reading or not self._down_pressed(): - intercepted = True - else: - end_line = self._get_end_cursor().blockNumber() - intercepted = cursor.blockNumber() == end_line - - elif key == QtCore.Qt.Key_Tab: - if self._reading: - intercepted = False - else: - intercepted = not self._tab_pressed() - - elif key == QtCore.Qt.Key_Left: - intercepted = not self._in_buffer(position - 1) + if obj == self._control: + etype = event.type() - elif key == QtCore.Qt.Key_Home: - cursor.movePosition(QtGui.QTextCursor.StartOfLine) - start_line = cursor.blockNumber() - if start_line == self._get_prompt_cursor().blockNumber(): - start_pos = self._prompt_pos - else: - start_pos = cursor.position() - start_pos += len(self._continuation_prompt) - if shift_down and self._in_buffer(position): - self._set_selection(position, start_pos) - else: - self._set_position(start_pos) - intercepted = True + # Disable moving text by drag and drop. + if etype == QtCore.QEvent.DragMove: + return True - elif key == QtCore.Qt.Key_Backspace and not alt_down: + elif etype == QtCore.QEvent.KeyPress: + return self._event_filter_keypress(event) - # Line deletion (remove continuation prompt) - len_prompt = len(self._continuation_prompt) - if not self._reading and \ - cursor.columnNumber() == len_prompt and \ - position != self._prompt_pos: - cursor.setPosition(position - len_prompt, - QtGui.QTextCursor.KeepAnchor) - cursor.removeSelectedText() + # On Mac OS, it is always unnecessary to override shortcuts, hence + # the check below. Users should just use the Control key instead of + # the Command key. + elif etype == QtCore.QEvent.ShortcutOverride: + if sys.platform != 'darwin' and \ + self._control_key_down(event.modifiers()) and \ + event.key() in self._shortcuts: + event.accept() + return False - # Regular backwards deletion - else: - anchor = cursor.anchor() - if anchor == position: - intercepted = not self._in_buffer(position - 1) - else: - intercepted = not self._in_buffer(min(anchor, position)) - - elif key == QtCore.Qt.Key_Delete: - anchor = cursor.anchor() - intercepted = not self._in_buffer(min(anchor, position)) - - # Don't move cursor if control is down to allow copy-paste using - # the keyboard in any part of the buffer. - if not ctrl_down: - self._keep_cursor_in_buffer() - - if not intercepted: - QtGui.QPlainTextEdit.keyPressEvent(self, event) - - #-------------------------------------------------------------------------- - # 'QPlainTextEdit' interface - #-------------------------------------------------------------------------- + return super(ConsoleWidget, self).eventFilter(obj, event) - def appendHtml(self, html): - """ Reimplemented to not append HTML as a new paragraph, which doesn't - make sense for a console widget. - """ - cursor = self._get_end_cursor() - self._insert_html(cursor, html) - - def appendPlainText(self, text): - """ Reimplemented to not append text as a new paragraph, which doesn't - make sense for a console widget. Also, if enabled, handle ANSI - codes. - """ - cursor = self._get_end_cursor() - if self.ansi_codes: - for substring in self._ansi_processor.split_string(text): - format = self._ansi_processor.get_format() - cursor.insertText(substring, format) - else: - cursor.insertText(text) + #--------------------------------------------------------------------------- + # 'ConsoleWidget' public interface + #--------------------------------------------------------------------------- def clear(self, keep_input=False): - """ Reimplemented to write a new prompt. If 'keep_input' is set, + """ Clear the console, then write a new prompt. If 'keep_input' is set, restores the old input buffer when the new prompt is written. """ - QtGui.QPlainTextEdit.clear(self) + self._control.clear() if keep_input: input_buffer = self.input_buffer self._show_prompt() if keep_input: self.input_buffer = input_buffer - def paste(self): - """ Reimplemented to ensure that text is pasted in the editing region. + def copy(self): + """ Copy the current selected text to the clipboard. """ - self._keep_cursor_in_buffer() - QtGui.QPlainTextEdit.paste(self) - - def print_(self, printer): - """ Reimplemented to work around a bug in PyQt: the C++ level 'print_' - slot has the wrong signature. - """ - QtGui.QPlainTextEdit.print_(self, printer) - - #--------------------------------------------------------------------------- - # 'ConsoleWidget' public interface - #--------------------------------------------------------------------------- + self._control.copy() def execute(self, source=None, hidden=False, interactive=False): """ Executes source or the input buffer, possibly prompting for more @@ -346,7 +168,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): if source is not None: self.input_buffer = source - self.appendPlainText('\n') + self._append_plain_text('\n') self._executing_input_buffer = self.input_buffer self._executing = True self._prompt_finished() @@ -358,7 +180,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): # The maximum block count is only in effect during execution. # This ensures that _prompt_pos does not become invalid due to # text truncation. - self.setMaximumBlockCount(self.buffer_size) + self._control.document().setMaximumBlockCount(self.buffer_size) self._execute(real_source, hidden) elif hidden: raise RuntimeError('Incomplete noninteractive input: "%s"' % source) @@ -393,14 +215,14 @@ class ConsoleWidget(QtGui.QPlainTextEdit): # Insert new text with continuation prompts. lines = string.splitlines(True) if lines: - self.appendPlainText(lines[0]) + self._append_plain_text(lines[0]) for i in xrange(1, len(lines)): if self._continuation_prompt_html is None: - self.appendPlainText(self._continuation_prompt) + self._append_plain_text(self._continuation_prompt) else: - self.appendHtml(self._continuation_prompt_html) - self.appendPlainText(lines[i]) - self.moveCursor(QtGui.QTextCursor.End) + self._append_html(self._continuation_prompt_html) + self._append_plain_text(lines[i]) + self._control.moveCursor(QtGui.QTextCursor.End) input_buffer = property(_get_input_buffer, _set_input_buffer) @@ -410,7 +232,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): """ if self._executing: return None - cursor = self.textCursor() + cursor = self._control.textCursor() if cursor.position() >= self._prompt_pos: text = self._get_block_plain_text(cursor.block()) if cursor.blockNumber() == self._get_prompt_cursor().blockNumber(): @@ -425,19 +247,36 @@ class ConsoleWidget(QtGui.QPlainTextEdit): def _get_font(self): """ The base font being used by the ConsoleWidget. """ - return self.document().defaultFont() + return self._control.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._control.setTabStopWidth(self.tab_width * font_metrics.width(' ')) self._completion_widget.setFont(font) - self.document().setDefaultFont(font) + self._control.document().setDefaultFont(font) font = property(_get_font, _set_font) + def paste(self): + """ Paste the contents of the clipboard into the input region. + """ + self._keep_cursor_in_buffer() + self._control.paste() + + def print_(self, printer): + """ Print the contents of the ConsoleWidget to the specified QPrinter. + """ + self._control.print_(printer) + + def redo(self): + """ Redo the last operation. If there is no operation to redo, nothing + happens. + """ + self._control.redo() + def reset_font(self): """ Sets the font to the default fixed-width font for this platform. """ @@ -451,6 +290,11 @@ class ConsoleWidget(QtGui.QPlainTextEdit): font.setStyleHint(QtGui.QFont.TypeWriter) self._set_font(font) + def select_all(self): + """ Selects all the text in the buffer. + """ + self._control.selectAll() + def _get_tab_width(self): """ The width (in terms of space characters) for tab characters. """ @@ -460,12 +304,18 @@ class ConsoleWidget(QtGui.QPlainTextEdit): """ 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._control.setTabStopWidth(tab_width * font_metrics.width(' ')) self._tab_width = tab_width tab_width = property(_get_tab_width, _set_tab_width) - + + def undo(self): + """ Undo the last operation. If there is no operation to undo, nothing + happens. + """ + self._control.undo() + #--------------------------------------------------------------------------- # 'ConsoleWidget' abstract interface #--------------------------------------------------------------------------- @@ -515,28 +365,60 @@ class ConsoleWidget(QtGui.QPlainTextEdit): # 'ConsoleWidget' protected interface #-------------------------------------------------------------------------- + def _append_html(self, html): + """ Appends html at the end of the console buffer. + """ + cursor = self._get_end_cursor() + self._insert_html(cursor, html) + def _append_html_fetching_plain_text(self, html): """ Appends 'html', then returns the plain text version of it. """ anchor = self._get_end_cursor().position() - self.appendHtml(html) + self._append_html(html) cursor = self._get_end_cursor() cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor) return str(cursor.selection().toPlainText()) + def _append_plain_text(self, text): + """ Appends plain text at the end of the console buffer, processing + ANSI codes if enabled. + """ + cursor = self._get_end_cursor() + if self.ansi_codes: + for substring in self._ansi_processor.split_string(text): + format = self._ansi_processor.get_format() + cursor.insertText(substring, format) + else: + cursor.insertText(text) + def _append_plain_text_keeping_prompt(self, text): """ Writes 'text' after the current prompt, then restores the old prompt with its old input buffer. """ input_buffer = self.input_buffer - self.appendPlainText('\n') + self._append_plain_text('\n') self._prompt_finished() - self.appendPlainText(text) + self._append_plain_text(text) self._show_prompt() self.input_buffer = input_buffer - def _control_down(self, modifiers): + def _complete_with_items(self, cursor, items): + """ Performs completion with 'items' at the specified cursor location. + """ + if len(items) == 1: + cursor.setPosition(self._control.textCursor().position(), + QtGui.QTextCursor.KeepAnchor) + cursor.insertText(items[0]) + elif len(items) > 1: + if self.gui_completion: + self._completion_widget.show_items(cursor, items) + else: + text = self._format_as_columns(items) + self._append_plain_text_keeping_prompt(text) + + def _control_key_down(self, modifiers): """ Given a KeyboardModifiers flags object, return whether the Control key is down (on Mac OS, treat the Command key as a synonym for Control). @@ -549,20 +431,175 @@ class ConsoleWidget(QtGui.QPlainTextEdit): down = down ^ bool(modifiers & QtCore.Qt.MetaModifier) return down - - def _complete_with_items(self, cursor, items): - """ Performs completion with 'items' at the specified cursor location. + + def _create_control(self, kind): + """ Creates and sets the underlying text widget. """ - if len(items) == 1: - cursor.setPosition(self.textCursor().position(), - QtGui.QTextCursor.KeepAnchor) - cursor.insertText(items[0]) - elif len(items) > 1: - if self.gui_completion: - self._completion_widget.show_items(cursor, items) - else: - text = self._format_as_columns(items) - self._append_plain_text_keeping_prompt(text) + layout = QtGui.QVBoxLayout(self) + layout.setMargin(0) + if kind == 'plain': + control = QtGui.QPlainTextEdit() + elif kind == 'rich': + control = QtGui.QTextEdit() + else: + raise ValueError("Kind %s unknown." % repr(kind)) + layout.addWidget(control) + + control.installEventFilter(self) + control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + control.customContextMenuRequested.connect(self._show_context_menu) + control.copyAvailable.connect(self.copy_available) + control.redoAvailable.connect(self.redo_available) + control.undoAvailable.connect(self.undo_available) + + return control + + def _event_filter_keypress(self, event): + """ Filter key events for the underlying text widget to create a + console-like interface. + """ + key = event.key() + ctrl_down = self._control_key_down(event.modifiers()) + + # If the key is remapped, return immediately. + if ctrl_down and key in self._ctrl_down_remap: + new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, + self._ctrl_down_remap[key], + QtCore.Qt.NoModifier) + QtGui.qApp.sendEvent(self._control, new_event) + return True + + # If the completion widget accepts the key press, return immediately. + if self._completion_widget.isVisible(): + self._completion_widget.keyPressEvent(event) + if event.isAccepted(): + return True + + # Otherwise, proceed normally and do not return early. + intercepted = False + cursor = self._control.textCursor() + position = cursor.position() + alt_down = event.modifiers() & QtCore.Qt.AltModifier + shift_down = event.modifiers() & QtCore.Qt.ShiftModifier + + if event.matches(QtGui.QKeySequence.Paste): + # Call our paste instead of the underlying text widget's. + self.paste() + intercepted = True + + elif ctrl_down: + if key == QtCore.Qt.Key_K: + if self._in_buffer(position): + cursor.movePosition(QtGui.QTextCursor.EndOfLine, + QtGui.QTextCursor.KeepAnchor) + cursor.removeSelectedText() + intercepted = True + + elif key == QtCore.Qt.Key_X: + intercepted = True + + elif key == QtCore.Qt.Key_Y: + self.paste() + intercepted = True + + elif alt_down: + if key == QtCore.Qt.Key_B: + self._set_cursor(self._get_word_start_cursor(position)) + intercepted = True + + elif key == QtCore.Qt.Key_F: + self._set_cursor(self._get_word_end_cursor(position)) + intercepted = True + + elif key == QtCore.Qt.Key_Backspace: + cursor = self._get_word_start_cursor(position) + cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) + cursor.removeSelectedText() + intercepted = True + + elif key == QtCore.Qt.Key_D: + cursor = self._get_word_end_cursor(position) + cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) + cursor.removeSelectedText() + intercepted = True + + else: + if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): + if self._reading: + self._append_plain_text('\n') + self._reading = False + if self._reading_callback: + self._reading_callback() + elif not self._executing: + self.execute(interactive=True) + intercepted = True + + elif key == QtCore.Qt.Key_Up: + if self._reading or not self._up_pressed(): + intercepted = True + else: + prompt_line = self._get_prompt_cursor().blockNumber() + intercepted = cursor.blockNumber() <= prompt_line + + elif key == QtCore.Qt.Key_Down: + if self._reading or not self._down_pressed(): + intercepted = True + else: + end_line = self._get_end_cursor().blockNumber() + intercepted = cursor.blockNumber() == end_line + + elif key == QtCore.Qt.Key_Tab: + if self._reading: + intercepted = False + else: + intercepted = not self._tab_pressed() + + elif key == QtCore.Qt.Key_Left: + intercepted = not self._in_buffer(position - 1) + + elif key == QtCore.Qt.Key_Home: + cursor.movePosition(QtGui.QTextCursor.StartOfLine) + start_line = cursor.blockNumber() + if start_line == self._get_prompt_cursor().blockNumber(): + start_pos = self._prompt_pos + else: + start_pos = cursor.position() + start_pos += len(self._continuation_prompt) + if shift_down and self._in_buffer(position): + self._set_selection(position, start_pos) + else: + self._set_position(start_pos) + intercepted = True + + elif key == QtCore.Qt.Key_Backspace and not alt_down: + + # Line deletion (remove continuation prompt) + len_prompt = len(self._continuation_prompt) + if not self._reading and \ + cursor.columnNumber() == len_prompt and \ + position != self._prompt_pos: + cursor.setPosition(position - len_prompt, + QtGui.QTextCursor.KeepAnchor) + cursor.removeSelectedText() + + # Regular backwards deletion + else: + anchor = cursor.anchor() + if anchor == position: + intercepted = not self._in_buffer(position - 1) + else: + intercepted = not self._in_buffer(min(anchor, position)) + + elif key == QtCore.Qt.Key_Delete: + anchor = cursor.anchor() + intercepted = not self._in_buffer(min(anchor, position)) + + # Don't move the cursor if control is down to allow copy-paste using + # the keyboard in any part of the buffer. + if not ctrl_down: + self._keep_cursor_in_buffer() + + return intercepted def _format_as_columns(self, items, separator=' '): """ Transform a list of strings into a single string with columns. @@ -639,18 +676,23 @@ class ConsoleWidget(QtGui.QPlainTextEdit): cursor.movePosition(QtGui.QTextCursor.EndOfBlock, QtGui.QTextCursor.KeepAnchor) return str(cursor.selection().toPlainText()) + + def _get_cursor(self): + """ Convenience method that returns a cursor for the current position. + """ + return self._control.textCursor() def _get_end_cursor(self): """ Convenience method that returns a cursor for the last character. """ - cursor = self.textCursor() + cursor = self._control.textCursor() cursor.movePosition(QtGui.QTextCursor.End) return cursor def _get_prompt_cursor(self): """ Convenience method that returns a cursor for the prompt position. """ - cursor = self.textCursor() + cursor = self._control.textCursor() cursor.setPosition(self._prompt_pos) return cursor @@ -658,7 +700,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): """ Convenience method that returns a cursor with text selected between the positions 'start' and 'end'. """ - cursor = self.textCursor() + cursor = self._control.textCursor() cursor.setPosition(start) cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor) return cursor @@ -668,7 +710,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): sequence of non-word characters precedes the first word, skip over them. (This emulates the behavior of bash, emacs, etc.) """ - document = self.document() + document = self._control.document() position -= 1 while self._in_buffer(position) and \ not document.characterAt(position).isLetterOrNumber(): @@ -676,7 +718,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): while self._in_buffer(position) and \ document.characterAt(position).isLetterOrNumber(): position -= 1 - cursor = self.textCursor() + cursor = self._control.textCursor() cursor.setPosition(position + 1) return cursor @@ -685,7 +727,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): sequence of non-word characters precedes the first word, skip over them. (This emulates the behavior of bash, emacs, etc.) """ - document = self.document() + document = self._control.document() end = self._get_end_cursor().position() while position < end and \ not document.characterAt(position).isLetterOrNumber(): @@ -693,7 +735,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): while position < end and \ document.characterAt(position).isLetterOrNumber(): position += 1 - cursor = self.textCursor() + cursor = self._control.textCursor() cursor.setPosition(position) return cursor @@ -718,17 +760,33 @@ class ConsoleWidget(QtGui.QPlainTextEdit): cursor.movePosition(QtGui.QTextCursor.Right) cursor.insertText(' ', QtGui.QTextCharFormat()) + def _in_buffer(self, position): + """ Returns whether the given position is inside the editing region. + """ + return position >= self._prompt_pos + + def _keep_cursor_in_buffer(self): + """ Ensures that the cursor is inside the editing region. Returns + whether the cursor was moved. + """ + cursor = self._control.textCursor() + if cursor.position() < self._prompt_pos: + cursor.movePosition(QtGui.QTextCursor.End) + self._control.setTextCursor(cursor) + return True + else: + return False + def _prompt_started(self): """ Called immediately after a new prompt is displayed. """ # Temporarily disable the maximum block count to permit undo/redo and # to ensure that the prompt position does not change due to truncation. - self.setMaximumBlockCount(0) - self.setUndoRedoEnabled(True) + self._control.document().setMaximumBlockCount(0) + self._control.setUndoRedoEnabled(True) - self.setReadOnly(False) - self.moveCursor(QtGui.QTextCursor.End) - self.centerCursor() + self._control.setReadOnly(False) + self._control.moveCursor(QtGui.QTextCursor.End) self._executing = False self._prompt_started_hook() @@ -737,8 +795,8 @@ class ConsoleWidget(QtGui.QPlainTextEdit): """ Called immediately after a prompt is finished, i.e. when some input will be processed and a new prompt displayed. """ - self.setUndoRedoEnabled(False) - self.setReadOnly(True) + self._control.setUndoRedoEnabled(False) + self._control.setReadOnly(True) self._prompt_finished_hook() def _readline(self, prompt='', callback=None): @@ -783,7 +841,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): def _reset(self): """ Clears the console and resets internal state variables. """ - QtGui.QPlainTextEdit.clear(self) + self._control.clear() self._executing = self._reading = False def _set_continuation_prompt(self, prompt, html=False): @@ -804,18 +862,47 @@ class ConsoleWidget(QtGui.QPlainTextEdit): else: self._continuation_prompt = prompt self._continuation_prompt_html = None + + def _set_cursor(self, cursor): + """ Convenience method to set the current cursor. + """ + self._control.setTextCursor(cursor) def _set_position(self, position): """ Convenience method to set the position of the cursor. """ - cursor = self.textCursor() + cursor = self._control.textCursor() cursor.setPosition(position) - self.setTextCursor(cursor) + self._control.setTextCursor(cursor) def _set_selection(self, start, end): """ Convenience method to set the current selected text. """ - self.setTextCursor(self._get_selection_cursor(start, end)) + self._control.setTextCursor(self._get_selection_cursor(start, end)) + + def _show_context_menu(self, pos): + """ Shows a context menu at the given QPoint (in widget coordinates). + """ + menu = QtGui.QMenu() + + copy_action = QtGui.QAction('Copy', menu) + copy_action.triggered.connect(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.setEnabled(self._control.canPaste()) + 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.exec_(self._control.mapToGlobal(pos)) def _show_prompt(self, prompt=None, html=False, newline=True): """ Writes a new prompt at the end of the buffer. @@ -841,20 +928,20 @@ class ConsoleWidget(QtGui.QPlainTextEdit): cursor.movePosition(QtGui.QTextCursor.Left, QtGui.QTextCursor.KeepAnchor) if str(cursor.selection().toPlainText()) != '\n': - self.appendPlainText('\n') + self._append_plain_text('\n') # Write the prompt. if prompt is None: if self._prompt_html is None: - self.appendPlainText(self._prompt) + self._append_plain_text(self._prompt) else: - self.appendHtml(self._prompt_html) + self._append_html(self._prompt_html) else: if html: self._prompt = self._append_html_fetching_plain_text(prompt) self._prompt_html = prompt else: - self.appendPlainText(prompt) + self._append_plain_text(prompt) self._prompt = prompt self._prompt_html = None @@ -865,30 +952,13 @@ class ConsoleWidget(QtGui.QPlainTextEdit): """ Writes a new continuation prompt at the end of the buffer. """ if self._continuation_prompt_html is None: - self.appendPlainText(self._continuation_prompt) + self._append_plain_text(self._continuation_prompt) else: self._continuation_prompt = self._append_html_fetching_plain_text( self._continuation_prompt_html) self._prompt_started() - def _in_buffer(self, position): - """ Returns whether the given position is inside the editing region. - """ - return position >= self._prompt_pos - - def _keep_cursor_in_buffer(self): - """ Ensures that the cursor is inside the editing region. Returns - whether the cursor was moved. - """ - cursor = self.textCursor() - if cursor.position() < self._prompt_pos: - cursor.movePosition(QtGui.QTextCursor.End) - self.setTextCursor(cursor) - return True - else: - return False - class HistoryConsoleWidget(ConsoleWidget): """ A ConsoleWidget that keeps a history of the commands that have been @@ -896,12 +966,11 @@ class HistoryConsoleWidget(ConsoleWidget): """ #--------------------------------------------------------------------------- - # 'QObject' interface + # 'object' interface #--------------------------------------------------------------------------- - def __init__(self, parent=None): - super(HistoryConsoleWidget, self).__init__(parent) - + def __init__(self, *args, **kw): + super(HistoryConsoleWidget, self).__init__(*args, **kw) self._history = [] self._history_index = 0 @@ -933,13 +1002,13 @@ class HistoryConsoleWidget(ConsoleWidget): processing the event. """ prompt_cursor = self._get_prompt_cursor() - if self.textCursor().blockNumber() == prompt_cursor.blockNumber(): + if self._get_cursor().blockNumber() == prompt_cursor.blockNumber(): self.history_previous() # Go to the first line of prompt for seemless history scrolling. cursor = self._get_prompt_cursor() cursor.movePosition(QtGui.QTextCursor.EndOfLine) - self.setTextCursor(cursor) + self._set_cursor(cursor) return False return True @@ -949,7 +1018,7 @@ class HistoryConsoleWidget(ConsoleWidget): processing the event. """ end_cursor = self._get_end_cursor() - if self.textCursor().blockNumber() == end_cursor.blockNumber(): + if self._get_cursor().blockNumber() == end_cursor.blockNumber(): self.history_next() return False return True diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index 9387046..873a1f0 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -21,7 +21,7 @@ class FrontendHighlighter(PygmentsHighlighter): """ def __init__(self, frontend): - super(FrontendHighlighter, self).__init__(frontend.document()) + super(FrontendHighlighter, self).__init__(frontend._control.document()) self._current_offset = 0 self._frontend = frontend self.highlighting_on = False @@ -68,14 +68,14 @@ class FrontendWidget(HistoryConsoleWidget): executed = QtCore.pyqtSignal(object) #--------------------------------------------------------------------------- - # 'QObject' interface + # 'object' interface #--------------------------------------------------------------------------- - def __init__(self, parent=None): - super(FrontendWidget, self).__init__(parent) + def __init__(self, *args, **kw): + super(FrontendWidget, self).__init__(*args, **kw) # FrontendWidget protected variables. - self._call_tip_widget = CallTipWidget(self) + self._call_tip_widget = CallTipWidget(self._control) self._completion_lexer = CompletionLexer(PythonLexer()) self._hidden = True self._highlighter = FrontendHighlighter(self) @@ -86,7 +86,9 @@ class FrontendWidget(HistoryConsoleWidget): self.tab_width = 4 self._set_continuation_prompt('... ') - self.document().contentsChange.connect(self._document_contents_change) + # Connect signal handlers. + document = self._control.document() + document.contentsChange.connect(self._document_contents_change) #--------------------------------------------------------------------------- # 'QWidget' interface @@ -140,8 +142,8 @@ class FrontendWidget(HistoryConsoleWidget): if self._get_prompt_cursor().blockNumber() != \ self._get_end_cursor().blockNumber(): spaces = self._input_splitter.indent_spaces - self.appendPlainText('\t' * (spaces / self.tab_width)) - self.appendPlainText(' ' * (spaces % self.tab_width)) + self._append_plain_text('\t' * (spaces / self.tab_width)) + self._append_plain_text(' ' * (spaces % self.tab_width)) def _prompt_finished_hook(self): """ Called immediately after a prompt is finished, i.e. when some input @@ -155,7 +157,7 @@ class FrontendWidget(HistoryConsoleWidget): processing the event. """ self._keep_cursor_in_buffer() - cursor = self.textCursor() + cursor = self._get_cursor() return not self._complete() #--------------------------------------------------------------------------- @@ -232,9 +234,9 @@ class FrontendWidget(HistoryConsoleWidget): """ Shows a call tip, if appropriate, at the current cursor location. """ # Decide if it makes sense to show a call tip - cursor = self.textCursor() + cursor = self._get_cursor() cursor.movePosition(QtGui.QTextCursor.Left) - document = self.document() + document = self._control.document() if document.characterAt(cursor.position()).toAscii() != '(': return False context = self._get_context(cursor) @@ -244,7 +246,7 @@ class FrontendWidget(HistoryConsoleWidget): # Send the metadata request to the kernel name = '.'.join(context) self._calltip_id = self.kernel_manager.xreq_channel.object_info(name) - self._calltip_pos = self.textCursor().position() + self._calltip_pos = self._get_cursor().position() return True def _complete(self): @@ -259,7 +261,7 @@ class FrontendWidget(HistoryConsoleWidget): text = '.'.join(context) self._complete_id = self.kernel_manager.xreq_channel.complete( text, self.input_buffer_cursor_line, self.input_buffer) - self._complete_pos = self.textCursor().position() + self._complete_pos = self._get_cursor().position() return True def _get_banner(self): @@ -273,7 +275,7 @@ class FrontendWidget(HistoryConsoleWidget): """ Gets the context at the current cursor location. """ if cursor is None: - cursor = self.textCursor() + cursor = self._get_cursor() cursor.movePosition(QtGui.QTextCursor.StartOfLine, QtGui.QTextCursor.KeepAnchor) text = str(cursor.selection().toPlainText()) @@ -285,8 +287,8 @@ class FrontendWidget(HistoryConsoleWidget): if self.kernel_manager.has_kernel: self.kernel_manager.signal_kernel(signal.SIGINT) else: - self.appendPlainText('Kernel process is either remote or ' - 'unspecified. Cannot interrupt.\n') + self._append_plain_text('Kernel process is either remote or ' + 'unspecified. Cannot interrupt.\n') def _show_interpreter_prompt(self): """ Shows a prompt for the interpreter. @@ -299,7 +301,7 @@ class FrontendWidget(HistoryConsoleWidget): """ Called when the kernel manager has started listening. """ self._reset() - self.appendPlainText(self._get_banner()) + self._append_plain_text(self._get_banner()) self._show_interpreter_prompt() def _stopped_channels(self): @@ -315,8 +317,8 @@ class FrontendWidget(HistoryConsoleWidget): # Calculate where the cursor should be *after* the change: position += added - document = self.document() - if position == self.textCursor().position(): + document = self._control.document() + if position == self._get_cursor().position(): self._call_tip() def _handle_req(self, req): @@ -336,11 +338,11 @@ class FrontendWidget(HistoryConsoleWidget): handler(omsg) def _handle_pyout(self, omsg): - self.appendPlainText(omsg['content']['data'] + '\n') + self._append_plain_text(omsg['content']['data'] + '\n') def _handle_stream(self, omsg): - self.appendPlainText(omsg['content']['data']) - self.moveCursor(QtGui.QTextCursor.End) + self._append_plain_text(omsg['content']['data']) + self._control.moveCursor(QtGui.QTextCursor.End) def _handle_execute_reply(self, reply): if self._hidden: @@ -355,7 +357,7 @@ class FrontendWidget(HistoryConsoleWidget): self._handle_execute_error(reply) elif status == 'aborted': text = "ERROR: ABORTED\n" - self.appendPlainText(text) + self._append_plain_text(text) self._hidden = True self._show_interpreter_prompt() self.executed.emit(reply) @@ -363,10 +365,10 @@ class FrontendWidget(HistoryConsoleWidget): def _handle_execute_error(self, reply): content = reply['content'] traceback = ''.join(content['traceback']) - self.appendPlainText(traceback) + self._append_plain_text(traceback) def _handle_complete_reply(self, rep): - cursor = self.textCursor() + cursor = self._get_cursor() if rep['parent_header']['msg_id'] == self._complete_id and \ cursor.position() == self._complete_pos: text = '.'.join(self._get_context()) @@ -374,7 +376,7 @@ class FrontendWidget(HistoryConsoleWidget): self._complete_with_items(cursor, rep['content']['matches']) def _handle_object_info_reply(self, rep): - cursor = self.textCursor() + cursor = self._get_cursor() if rep['parent_header']['msg_id'] == self._calltip_id and \ cursor.position() == self._calltip_pos: doc = rep['content']['docstring'] diff --git a/IPython/frontend/qt/console/ipython_widget.py b/IPython/frontend/qt/console/ipython_widget.py index 5db2a3d..4f0bd72 100644 --- a/IPython/frontend/qt/console/ipython_widget.py +++ b/IPython/frontend/qt/console/ipython_widget.py @@ -35,11 +35,11 @@ class IPythonWidget(FrontendWidget): out_prompt = 'Out[%i]: ' #--------------------------------------------------------------------------- - # 'QObject' interface + # 'object' interface #--------------------------------------------------------------------------- - def __init__(self, parent=None): - super(IPythonWidget, self).__init__(parent) + def __init__(self, *args, **kw): + super(IPythonWidget, self).__init__(*args, **kw) # Initialize protected variables. self._previous_prompt_blocks = [] @@ -108,15 +108,15 @@ class IPythonWidget(FrontendWidget): ename_styled = '%s' % ename traceback = traceback.replace(ename, ename_styled) - self.appendHtml(traceback) + self._append_html(traceback) def _handle_pyout(self, omsg): """ Reimplemented for IPython-style "display hook". """ - self.appendHtml(self._make_out_prompt(self._prompt_count)) + self._append_html(self._make_out_prompt(self._prompt_count)) self._save_prompt_block() - self.appendPlainText(omsg['content']['data'] + '\n') + self._append_plain_text(omsg['content']['data'] + '\n') #--------------------------------------------------------------------------- # 'IPythonWidget' interface @@ -144,7 +144,7 @@ class IPythonWidget(FrontendWidget): the stylesheet is queried for Pygments style information. """ self.setStyleSheet(stylesheet) - self.document().setDefaultStyleSheet(stylesheet) + self._control.document().setDefaultStyleSheet(stylesheet) if syntax_style is None: self._highlighter.set_style_sheet(stylesheet) @@ -180,7 +180,7 @@ class IPythonWidget(FrontendWidget): """ Assuming a prompt has just been written at the end of the buffer, store the QTextBlock that contains it and its length. """ - block = self.document().lastBlock() + block = self._control.document().lastBlock() self._previous_prompt_blocks.append((block, block.length()))