From 0b79fc7d22058178f66f286b9fba0d77ffc9e18b 2010-08-11 16:51:31
From: epatters <epatters@enthought.com>
Date: 2010-08-11 16:51:31
Subject: [PATCH] Refactored ConsoleWidget to encapsulate, rather than inherit from, QPlainTextEdit. This permits a QTextEdit to be substituted for a QPlainTextEdit if desired. It also makes it more clear what is the public interface of ConsoleWidget.

---

diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py
index 7308116..b7482c4 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
@@ -61,252 +81,63 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
         self._reading_callback = None
         self._tab_width = 8
 
-        # 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._context_menu = self._create_context_menu()
 
-        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()
+        # Set a monospaced font.
+        self.reset_font()
 
-        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)
+    def eventFilter(self, obj, event):
+        """ Reimplemented to ensure a console-like behavior in the underlying
+            text widget.
+        """
+        if obj == self._control:
+            etype = event.type()
+
+            # Override the default context menu with one that does not have
+            # destructive actions.
+            if etype == QtCore.QEvent.ContextMenu:
+                self._context_menu.exec_(event.globalPos())
+                return True
+
+            # Disable moving text by drag and drop.
+            elif etype == QtCore.QEvent.DragMove:
+                return True
+
+            elif etype == QtCore.QEvent.KeyPress:
+                return self._event_filter_keypress(event)
+
+            # 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
+
+        return super(ConsoleWidget, self).eventFilter(obj, event)
 
     #---------------------------------------------------------------------------
-    # 'QWidget' interface
+    # 'ConsoleWidget' public 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.
-        """
-        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)
-
-            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 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
-    #--------------------------------------------------------------------------
-
-    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)
-
     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 +177,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 +189,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 +224,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 +241,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 +256,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 +299,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 +313,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 +374,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 +440,195 @@ 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_context_menu(self):
+        """ Creates a context menu for 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)
+        menu = QtGui.QMenu(self)
+        clipboard = QtGui.QApplication.clipboard()
+
+        copy_action = QtGui.QAction('Copy', self)
+        copy_action.triggered.connect(self.copy)
+        self.copy_available.connect(copy_action.setEnabled)
+        menu.addAction(copy_action)
+
+        paste_action = QtGui.QAction('Paste', self)
+        paste_action.triggered.connect(self.paste)
+        clipboard.dataChanged.connect(
+            lambda: paste_action.setEnabled(not clipboard.text().isEmpty()))
+        menu.addAction(paste_action)
+        menu.addSeparator()
+
+        select_all_action = QtGui.QAction('Select All', self)
+        select_all_action.triggered.connect(self.select_all)
+        menu.addAction(select_all_action)
+
+        return menu
+
+    def _create_control(self, kind):
+        """ Creates and sets the underlying text widget.
+        """
+        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.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.
+        """
+        intercepted = False
+        replaced_event = None
+        cursor = self._control.textCursor()
+        position = cursor.position()
+        key = event.key()
+        ctrl_down = self._control_key_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]
+                replaced_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._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
+
+        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._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 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 and replaced_event:
+            QtGui.qApp.sendEvent(self._control, replaced_event)
+        return intercepted
 
     def _format_as_columns(self, items, separator='  '):
         """ Transform a list of strings into a single string with columns.
@@ -639,18 +705,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 +729,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 +739,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 +747,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 +756,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 +764,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 +789,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 +824,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 +870,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 +891,23 @@ 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_prompt(self, prompt=None, html=False, newline=True):
         """ Writes a new prompt at the end of the buffer.
@@ -841,20 +933,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 +957,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 +971,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 +1007,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 +1023,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[<span class="out-prompt-number">%i</span>]: '
 
     #---------------------------------------------------------------------------
-    # '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 = '<span class="error">%s</span>' % 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()))