From 5b40b7b2d3b023f2c7c0b4ced6d9463e26766ef2 2010-09-12 05:05:58 From: epatters Date: 2010-09-12 05:05:58 Subject: [PATCH] ConsoleWidget now manually controls the vertical scrollbar. As a result, Ctrl+L works as expected. Additionally, the prompt does not "jump around" while scrolling through history. --- diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index 7b0aad5..124a94d 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -204,6 +204,12 @@ class ConsoleWidget(Configurable, QtGui.QWidget): # 'QWidget' interface #--------------------------------------------------------------------------- + def resizeEvent(self, event): + """ Adjust the scrollbars manually after a resize event. + """ + super(ConsoleWidget, self).resizeEvent(event) + self._adjust_scrollbars() + def sizeHint(self): """ Reimplemented to suggest a size that is 80 characters wide and 25 lines high. @@ -462,6 +468,18 @@ class ConsoleWidget(Configurable, QtGui.QWidget): """ self._control.print_(printer) + def prompt_to_top(self): + """ Moves the prompt to the top of the viewport. + """ + if not self._executing: + scrollbar = self._control.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + cursor = self._control.textCursor() + self._control.setTextCursor(self._get_prompt_cursor()) + self._control.ensureCursorVisible() + QtGui.qApp.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) + self._control.setTextCursor(cursor) + def redo(self): """ Redo the last operation. If there is no operation to redo, nothing happens. @@ -682,12 +700,6 @@ class ConsoleWidget(Configurable, QtGui.QWidget): return menu - def _context_menu_show(self, pos): - """ Shows a context menu at the given QPoint (in widget coordinates). - """ - menu = self._context_menu_make(pos) - menu.exec_(self._control.mapToGlobal(pos)) - def _control_key_down(self, modifiers, include_command=True): """ Given a KeyboardModifiers flags object, return whether the Control key is down. @@ -723,11 +735,20 @@ class ConsoleWidget(Configurable, QtGui.QWidget): # Connect signals. control.cursorPositionChanged.connect(self._cursor_position_changed) - control.customContextMenuRequested.connect(self._context_menu_show) + control.customContextMenuRequested.connect( + self._custom_context_menu_requested) control.copyAvailable.connect(self.copy_available) control.redoAvailable.connect(self.redo_available) control.undoAvailable.connect(self.undo_available) + # Hijack the document size change signal to prevent Qt from adjusting + # the viewport's scrollbar. We are relying on an implementation detail + # of Q(Plain)TextEdit here, which is potentially dangerous, but without + # this functionality we cannot create a nice terminal interface. + layout = control.document().documentLayout() + layout.documentSizeChanged.disconnect() + layout.documentSizeChanged.connect(self._adjust_scrollbars) + # Configure the control. control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) control.setReadOnly(True) @@ -830,14 +851,7 @@ class ConsoleWidget(Configurable, QtGui.QWidget): intercepted = True elif key == QtCore.Qt.Key_L: - # It would be better to simply move the prompt block to the top - # of the control viewport. QPlainTextEdit has a private method - # to do this (setTopBlock), but it cannot be duplicated here - # because it requires access to the QTextControl that underlies - # both QPlainTextEdit and QTextEdit. In short, this can only be - # achieved by appending newlines after the prompt, which is a - # gigantic hack and likely to cause other problems. - self.clear() + self.prompt_to_top() intercepted = True elif key == QtCore.Qt.Key_O: @@ -1354,6 +1368,13 @@ class ConsoleWidget(Configurable, QtGui.QWidget): else: self._append_plain_text(text) + def _prompt_finished(self): + """ Called immediately after a prompt is finished, i.e. when some input + will be processed and a new prompt displayed. + """ + self._control.setReadOnly(True) + self._prompt_finished_hook() + def _prompt_started(self): """ Called immediately after a new prompt is displayed. """ @@ -1371,13 +1392,6 @@ class ConsoleWidget(Configurable, QtGui.QWidget): self._executing = False self._prompt_started_hook() - def _prompt_finished(self): - """ Called immediately after a prompt is finished, i.e. when some input - will be processed and a new prompt displayed. - """ - self._control.setReadOnly(True) - self._prompt_finished_hook() - def _readline(self, prompt='', callback=None): """ Reads one line of input from the user. @@ -1488,6 +1502,23 @@ class ConsoleWidget(Configurable, QtGui.QWidget): #------ Signal handlers ---------------------------------------------------- + def _adjust_scrollbars(self): + """ Expands the vertical scrollbar beyond the range set by Qt. + """ + # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp + # and qtextedit.cpp. + document = self._control.document() + scrollbar = self._control.verticalScrollBar() + viewport_height = self._control.viewport().height() + if isinstance(self._control, QtGui.QPlainTextEdit): + high = max(0, document.lineCount() - 1) + step = viewport_height / self._control.fontMetrics().lineSpacing() + else: + high = document.size().height() + step = viewport_height + scrollbar.setRange(0, high) + scrollbar.setPageStep(step) + def _cursor_position_changed(self): """ Clears the temporary buffer based on the cursor position. """ @@ -1505,3 +1536,9 @@ class ConsoleWidget(Configurable, QtGui.QWidget): else: self._clear_temporary_buffer() self._text_completing_pos = 0 + + def _custom_context_menu_requested(self, pos): + """ Shows a context menu at the given QPoint (in widget coordinates). + """ + menu = self._context_menu_make(pos) + menu.exec_(self._control.mapToGlobal(pos))