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()))