console_widget.py
1430 lines
| 55.7 KiB
| text/x-python
|
PythonLexer
epatters
|
r2602 | # Standard library imports | ||
epatters
|
r2843 | import re | ||
epatters
|
r2665 | import sys | ||
epatters
|
r2763 | from textwrap import dedent | ||
epatters
|
r2602 | |||
# System library imports | ||||
from PyQt4 import QtCore, QtGui | ||||
# Local imports | ||||
epatters
|
r2884 | from IPython.config.configurable import Configurable | ||
from IPython.frontend.qt.util import MetaQObjectHasTraits | ||||
from IPython.utils.traitlets import Bool, Enum, Int | ||||
epatters
|
r2716 | from ansi_code_processor import QtAnsiCodeProcessor | ||
epatters
|
r2602 | from completion_widget import CompletionWidget | ||
epatters
|
r2865 | class ConsolePlainTextEdit(QtGui.QPlainTextEdit): | ||
""" A QPlainTextEdit suitable for use with ConsoleWidget. | ||||
""" | ||||
# Prevents text from being moved by drag and drop. Note that is not, for | ||||
# some reason, sufficient to catch drag events in the ConsoleWidget's | ||||
# event filter. | ||||
def dragEnterEvent(self, event): pass | ||||
def dragLeaveEvent(self, event): pass | ||||
def dragMoveEvent(self, event): pass | ||||
def dropEvent(self, event): pass | ||||
class ConsoleTextEdit(QtGui.QTextEdit): | ||||
""" A QTextEdit suitable for use with ConsoleWidget. | ||||
""" | ||||
# See above. | ||||
def dragEnterEvent(self, event): pass | ||||
def dragLeaveEvent(self, event): pass | ||||
def dragMoveEvent(self, event): pass | ||||
def dropEvent(self, event): pass | ||||
epatters
|
r2884 | class ConsoleWidget(Configurable, QtGui.QWidget): | ||
epatters
|
r2776 | """ An abstract base class for console-type widgets. This class has | ||
functionality for: | ||||
* Maintaining a prompt and editing region | ||||
* Providing the traditional Unix-style console keyboard shortcuts | ||||
* Performing tab completion | ||||
* Paging text | ||||
* Handling ANSI escape codes | ||||
ConsoleWidget also provides a number of utility methods that will be | ||||
convenient to implementors of a console-style widget. | ||||
epatters
|
r2602 | """ | ||
epatters
|
r2884 | __metaclass__ = MetaQObjectHasTraits | ||
epatters
|
r2602 | |||
epatters
|
r2668 | # Whether to process ANSI escape codes. | ||
epatters
|
r2884 | ansi_codes = Bool(True, config=True) | ||
epatters
|
r2668 | |||
epatters
|
r2884 | # The maximum number of lines of text before truncation. Specifying a | ||
# non-positive number disables text truncation (not recommended). | ||||
buffer_size = Int(500, config=True) | ||||
epatters
|
r2668 | |||
epatters
|
r2772 | # Whether to use a list widget or plain text output for tab completion. | ||
epatters
|
r2884 | gui_completion = Bool(True, config=True) | ||
# The type of underlying text widget to use. Valid values are 'plain', which | ||||
# specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit. | ||||
# NOTE: this value can only be specified during initialization. | ||||
kind = Enum(['plain', 'rich'], default_value='plain', config=True) | ||||
# The type of paging to use. Valid values are: | ||||
# 'inside' : The widget pages like a traditional terminal pager. | ||||
# 'hsplit' : When paging is requested, the widget is split | ||||
# horizontally. The top pane contains the console, and the | ||||
# bottom pane contains the paged text. | ||||
# 'vsplit' : Similar to 'hsplit', except that a vertical splitter used. | ||||
# 'custom' : No action is taken by the widget beyond emitting a | ||||
# 'custom_page_requested(str)' signal. | ||||
# 'none' : The text is written directly to the console. | ||||
# NOTE: this value can only be specified during initialization. | ||||
paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'], | ||||
default_value='inside', config=True) | ||||
epatters
|
r2668 | |||
# Whether to override ShortcutEvents for the keybindings defined by this | ||||
# widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take | ||||
# priority (when it has focus) over, e.g., window-level menu shortcuts. | ||||
epatters
|
r2884 | override_shortcuts = Bool(False) | ||
epatters
|
r2668 | |||
epatters
|
r2736 | # Signals that indicate ConsoleWidget state. | ||
copy_available = QtCore.pyqtSignal(bool) | ||||
redo_available = QtCore.pyqtSignal(bool) | ||||
undo_available = QtCore.pyqtSignal(bool) | ||||
epatters
|
r2776 | # Signal emitted when paging is needed and the paging style has been | ||
# specified as 'custom'. | ||||
epatters
|
r2793 | custom_page_requested = QtCore.pyqtSignal(object) | ||
epatters
|
r2776 | |||
epatters
|
r2668 | # Protected class variables. | ||
epatters
|
r2602 | _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left, | ||
QtCore.Qt.Key_F : QtCore.Qt.Key_Right, | ||||
QtCore.Qt.Key_A : QtCore.Qt.Key_Home, | ||||
QtCore.Qt.Key_E : QtCore.Qt.Key_End, | ||||
QtCore.Qt.Key_P : QtCore.Qt.Key_Up, | ||||
QtCore.Qt.Key_N : QtCore.Qt.Key_Down, | ||||
QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, } | ||||
epatters
|
r2687 | _shortcuts = set(_ctrl_down_remap.keys() + | ||
epatters
|
r2897 | [ QtCore.Qt.Key_C, QtCore.Qt.Key_V, QtCore.Qt.Key_O ]) | ||
epatters
|
r2602 | |||
#--------------------------------------------------------------------------- | ||||
epatters
|
r2669 | # 'QObject' interface | ||
epatters
|
r2602 | #--------------------------------------------------------------------------- | ||
epatters
|
r2884 | def __init__(self, parent=None, **kw): | ||
epatters
|
r2736 | """ Create a ConsoleWidget. | ||
epatters
|
r2884 | Parameters: | ||
----------- | ||||
epatters
|
r2736 | parent : QWidget, optional [default None] | ||
The parent for this widget. | ||||
""" | ||||
epatters
|
r2884 | QtGui.QWidget.__init__(self, parent) | ||
Configurable.__init__(self, **kw) | ||||
epatters
|
r2736 | |||
epatters
|
r2776 | # Create the layout and underlying text widget. | ||
layout = QtGui.QStackedLayout(self) | ||||
epatters
|
r2882 | layout.setContentsMargins(0, 0, 0, 0) | ||
epatters
|
r2884 | self._control = self._create_control() | ||
epatters
|
r2776 | self._page_control = None | ||
self._splitter = None | ||||
epatters
|
r2884 | if self.paging in ('hsplit', 'vsplit'): | ||
epatters
|
r2776 | self._splitter = QtGui.QSplitter() | ||
epatters
|
r2884 | if self.paging == 'hsplit': | ||
epatters
|
r2776 | self._splitter.setOrientation(QtCore.Qt.Horizontal) | ||
else: | ||||
self._splitter.setOrientation(QtCore.Qt.Vertical) | ||||
self._splitter.addWidget(self._control) | ||||
layout.addWidget(self._splitter) | ||||
else: | ||||
layout.addWidget(self._control) | ||||
# Create the paging widget, if necessary. | ||||
epatters
|
r2884 | if self.paging in ('inside', 'hsplit', 'vsplit'): | ||
epatters
|
r2776 | self._page_control = self._create_page_control() | ||
if self._splitter: | ||||
self._page_control.hide() | ||||
self._splitter.addWidget(self._page_control) | ||||
else: | ||||
layout.addWidget(self._page_control) | ||||
epatters
|
r2602 | |||
epatters
|
r2715 | # Initialize protected variables. Some variables contain useful state | ||
# information for subclasses; they should be considered read-only. | ||||
epatters
|
r2602 | self._ansi_processor = QtAnsiCodeProcessor() | ||
epatters
|
r2736 | self._completion_widget = CompletionWidget(self._control) | ||
epatters
|
r2643 | self._continuation_prompt = '> ' | ||
epatters
|
r2715 | self._continuation_prompt_html = None | ||
epatters
|
r2602 | self._executing = False | ||
self._prompt = '' | ||||
epatters
|
r2715 | self._prompt_html = None | ||
epatters
|
r2602 | self._prompt_pos = 0 | ||
epatters
|
r2866 | self._prompt_sep = '' | ||
epatters
|
r2602 | self._reading = False | ||
epatters
|
r2706 | self._reading_callback = None | ||
epatters
|
r2723 | self._tab_width = 8 | ||
epatters
|
r2602 | |||
epatters
|
r2736 | # Set a monospaced font. | ||
self.reset_font() | ||||
epatters
|
r2602 | |||
epatters
|
r2736 | def eventFilter(self, obj, event): | ||
""" Reimplemented to ensure a console-like behavior in the underlying | ||||
epatters
|
r2897 | text widgets. | ||
epatters
|
r2736 | """ | ||
epatters
|
r2772 | etype = event.type() | ||
epatters
|
r2897 | if etype == QtCore.QEvent.KeyPress: | ||
# Re-map keys for all filtered widgets. | ||||
key = event.key() | ||||
if self._control_key_down(event.modifiers()) 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(obj, new_event) | ||||
return True | ||||
elif obj == self._control: | ||||
return self._event_filter_console_keypress(event) | ||||
elif obj == self._page_control: | ||||
return self._event_filter_page_keypress(event) | ||||
epatters
|
r2736 | |||
epatters
|
r2772 | # Override shortucts for all filtered widgets. Note that 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 and \ | ||||
sys.platform != 'darwin' and \ | ||||
self._control_key_down(event.modifiers()) and \ | ||||
event.key() in self._shortcuts: | ||||
event.accept() | ||||
return False | ||||
epatters
|
r2736 | return super(ConsoleWidget, self).eventFilter(obj, event) | ||
epatters
|
r2668 | |||
epatters
|
r2669 | #--------------------------------------------------------------------------- | ||
epatters
|
r2776 | # 'QWidget' interface | ||
#--------------------------------------------------------------------------- | ||||
def sizeHint(self): | ||||
""" Reimplemented to suggest a size that is 80 characters wide and | ||||
25 lines high. | ||||
""" | ||||
font_metrics = QtGui.QFontMetrics(self.font) | ||||
epatters
|
r2882 | margin = (self._control.frameWidth() + | ||
self._control.document().documentMargin()) * 2 | ||||
style = self.style() | ||||
splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth) | ||||
epatters
|
r2776 | |||
epatters
|
r2882 | # Despite my best efforts to take the various margins into account, the | ||
# width is still coming out a bit too small, so we include a fudge | ||||
# factor of one character here. | ||||
width = font_metrics.maxWidth() * 81 + margin | ||||
width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent) | ||||
epatters
|
r2884 | if self.paging == 'hsplit': | ||
epatters
|
r2776 | width = width * 2 + splitwidth | ||
epatters
|
r2882 | height = font_metrics.height() * 25 + margin | ||
epatters
|
r2884 | if self.paging == 'vsplit': | ||
epatters
|
r2776 | height = height * 2 + splitwidth | ||
return QtCore.QSize(width, height) | ||||
#--------------------------------------------------------------------------- | ||||
epatters
|
r2736 | # 'ConsoleWidget' public interface | ||
epatters
|
r2669 | #--------------------------------------------------------------------------- | ||
epatters
|
r2762 | def can_paste(self): | ||
""" Returns whether text can be pasted from the clipboard. | ||||
""" | ||||
epatters
|
r2882 | # Only accept text that can be ASCII encoded. | ||
epatters
|
r2762 | if self._control.textInteractionFlags() & QtCore.Qt.TextEditable: | ||
text = QtGui.QApplication.clipboard().text() | ||||
if not text.isEmpty(): | ||||
try: | ||||
str(text) | ||||
return True | ||||
except UnicodeEncodeError: | ||||
pass | ||||
return False | ||||
epatters
|
r2842 | def clear(self, keep_input=True): | ||
epatters
|
r2736 | """ Clear the console, then write a new prompt. If 'keep_input' is set, | ||
epatters
|
r2715 | restores the old input buffer when the new prompt is written. | ||
epatters
|
r2643 | """ | ||
epatters
|
r2715 | if keep_input: | ||
epatters
|
r2643 | input_buffer = self.input_buffer | ||
epatters
|
r2842 | self._control.clear() | ||
epatters
|
r2643 | self._show_prompt() | ||
epatters
|
r2715 | if keep_input: | ||
self.input_buffer = input_buffer | ||||
epatters
|
r2643 | |||
epatters
|
r2736 | def copy(self): | ||
""" Copy the current selected text to the clipboard. | ||||
epatters
|
r2602 | """ | ||
epatters
|
r2736 | self._control.copy() | ||
epatters
|
r2602 | |||
epatters
|
r2688 | def execute(self, source=None, hidden=False, interactive=False): | ||
""" Executes source or the input buffer, possibly prompting for more | ||||
input. | ||||
Parameters: | ||||
----------- | ||||
source : str, optional | ||||
The source to execute. If not specified, the input buffer will be | ||||
used. If specified and 'hidden' is False, the input buffer will be | ||||
replaced with the source before execution. | ||||
hidden : bool, optional (default False) | ||||
If set, no output will be shown and the prompt will not be modified. | ||||
In other words, it will be completely invisible to the user that | ||||
an execution has occurred. | ||||
interactive : bool, optional (default False) | ||||
Whether the console is to treat the source as having been manually | ||||
entered by the user. The effect of this parameter depends on the | ||||
subclass implementation. | ||||
epatters
|
r2689 | Raises: | ||
------- | ||||
RuntimeError | ||||
If incomplete input is given and 'hidden' is True. In this case, | ||||
epatters
|
r2771 | it is not possible to prompt for more input. | ||
epatters
|
r2689 | |||
epatters
|
r2688 | Returns: | ||
-------- | ||||
A boolean indicating whether the source was executed. | ||||
""" | ||||
epatters
|
r2866 | # WARNING: The order in which things happen here is very particular, in | ||
# large part because our syntax highlighting is fragile. If you change | ||||
# something, test carefully! | ||||
epatters
|
r2688 | |||
epatters
|
r2866 | # Decide what to execute. | ||
if source is None: | ||||
source = self.input_buffer | ||||
epatters
|
r2688 | if not hidden: | ||
epatters
|
r2866 | # A newline is appended later, but it should be considered part | ||
# of the input buffer. | ||||
source += '\n' | ||||
elif not hidden: | ||||
self.input_buffer = source | ||||
# Execute the source or show a continuation prompt if it is incomplete. | ||||
complete = self._is_complete(source, interactive) | ||||
if hidden: | ||||
if complete: | ||||
self._execute(source, hidden) | ||||
else: | ||||
error = 'Incomplete noninteractive input: "%s"' | ||||
raise RuntimeError(error % source) | ||||
else: | ||||
if complete: | ||||
self._append_plain_text('\n') | ||||
self._executing_input_buffer = self.input_buffer | ||||
self._executing = True | ||||
self._prompt_finished() | ||||
epatters
|
r2688 | # The maximum block count is only in effect during execution. | ||
# This ensures that _prompt_pos does not become invalid due to | ||||
# text truncation. | ||||
epatters
|
r2736 | self._control.document().setMaximumBlockCount(self.buffer_size) | ||
epatters
|
r2864 | |||
# Setting a positive maximum block count will automatically | ||||
# disable the undo/redo history, but just to be safe: | ||||
self._control.setUndoRedoEnabled(False) | ||||
epatters
|
r2866 | self._execute(source, hidden) | ||
else: | ||||
# Do this inside an edit block so continuation prompts are | ||||
# removed seamlessly via undo/redo. | ||||
epatters
|
r2896 | cursor = self._get_end_cursor() | ||
epatters
|
r2866 | cursor.beginEditBlock() | ||
epatters
|
r2896 | cursor.insertText('\n') | ||
self._insert_continuation_prompt(cursor) | ||||
self._control.moveCursor(QtGui.QTextCursor.End) | ||||
epatters
|
r2866 | cursor.endEditBlock() | ||
epatters
|
r2864 | |||
epatters
|
r2688 | return complete | ||
epatters
|
r2602 | |||
def _get_input_buffer(self): | ||||
epatters
|
r2665 | """ The text that the user has entered entered at the current prompt. | ||
""" | ||||
# If we're executing, the input buffer may not even exist anymore due to | ||||
epatters
|
r2615 | # the limit imposed by 'buffer_size'. Therefore, we store it. | ||
if self._executing: | ||||
return self._executing_input_buffer | ||||
epatters
|
r2602 | cursor = self._get_end_cursor() | ||
cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor) | ||||
input_buffer = str(cursor.selection().toPlainText()) | ||||
epatters
|
r2665 | # Strip out continuation prompts. | ||
epatters
|
r2643 | return input_buffer.replace('\n' + self._continuation_prompt, '\n') | ||
epatters
|
r2602 | |||
def _set_input_buffer(self, string): | ||||
epatters
|
r2665 | """ Replaces the text in the input buffer with 'string'. | ||
""" | ||||
epatters
|
r2764 | # For now, it is an error to modify the input buffer during execution. | ||
if self._executing: | ||||
raise RuntimeError("Cannot change input buffer during execution.") | ||||
epatters
|
r2717 | # Remove old text. | ||
epatters
|
r2602 | cursor = self._get_end_cursor() | ||
epatters
|
r2764 | cursor.beginEditBlock() | ||
epatters
|
r2602 | cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor) | ||
epatters
|
r2717 | cursor.removeSelectedText() | ||
# Insert new text with continuation prompts. | ||||
lines = string.splitlines(True) | ||||
if lines: | ||||
epatters
|
r2736 | self._append_plain_text(lines[0]) | ||
epatters
|
r2717 | for i in xrange(1, len(lines)): | ||
if self._continuation_prompt_html is None: | ||||
epatters
|
r2736 | self._append_plain_text(self._continuation_prompt) | ||
epatters
|
r2717 | else: | ||
epatters
|
r2736 | self._append_html(self._continuation_prompt_html) | ||
self._append_plain_text(lines[i]) | ||||
epatters
|
r2764 | cursor.endEditBlock() | ||
epatters
|
r2736 | self._control.moveCursor(QtGui.QTextCursor.End) | ||
epatters
|
r2602 | |||
input_buffer = property(_get_input_buffer, _set_input_buffer) | ||||
epatters
|
r2665 | def _get_font(self): | ||
""" The base font being used by the ConsoleWidget. | ||||
""" | ||||
epatters
|
r2736 | return self._control.document().defaultFont() | ||
epatters
|
r2665 | |||
def _set_font(self, font): | ||||
""" Sets the base font for the ConsoleWidget to the specified QFont. | ||||
""" | ||||
epatters
|
r2716 | font_metrics = QtGui.QFontMetrics(font) | ||
epatters
|
r2736 | self._control.setTabStopWidth(self.tab_width * font_metrics.width(' ')) | ||
epatters
|
r2716 | |||
epatters
|
r2665 | self._completion_widget.setFont(font) | ||
epatters
|
r2736 | self._control.document().setDefaultFont(font) | ||
epatters
|
r2779 | if self._page_control: | ||
self._page_control.document().setDefaultFont(font) | ||||
epatters
|
r2665 | |||
font = property(_get_font, _set_font) | ||||
epatters
|
r2736 | def paste(self): | ||
""" Paste the contents of the clipboard into the input region. | ||||
""" | ||||
epatters
|
r2763 | if self._control.textInteractionFlags() & QtCore.Qt.TextEditable: | ||
try: | ||||
text = str(QtGui.QApplication.clipboard().text()) | ||||
except UnicodeEncodeError: | ||||
pass | ||||
else: | ||||
epatters
|
r2787 | self._insert_plain_text_into_buffer(dedent(text)) | ||
epatters
|
r2736 | |||
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() | ||||
epatters
|
r2665 | def reset_font(self): | ||
""" Sets the font to the default fixed-width font for this platform. | ||||
""" | ||||
Fernando Perez
|
r2858 | # FIXME: font family and size should be configurable by the user. | ||
epatters
|
r2665 | if sys.platform == 'win32': | ||
epatters
|
r2896 | # FIXME: we should test whether Consolas is available and use it | ||
Fernando Perez
|
r2858 | # first if it is. Consolas ships by default from Vista onwards, | ||
# it's *vastly* more readable and prettier than Courier, and is | ||||
# often installed even on XP systems. So we should first check for | ||||
# it, and only fallback to Courier if absolutely necessary. | ||||
epatters
|
r2665 | name = 'Courier' | ||
elif sys.platform == 'darwin': | ||||
name = 'Monaco' | ||||
else: | ||||
name = 'Monospace' | ||||
font = QtGui.QFont(name, QtGui.qApp.font().pointSize()) | ||||
font.setStyleHint(QtGui.QFont.TypeWriter) | ||||
self._set_font(font) | ||||
epatters
|
r2723 | |||
epatters
|
r2736 | def select_all(self): | ||
""" Selects all the text in the buffer. | ||||
""" | ||||
self._control.selectAll() | ||||
epatters
|
r2723 | def _get_tab_width(self): | ||
""" The width (in terms of space characters) for tab characters. | ||||
""" | ||||
return self._tab_width | ||||
def _set_tab_width(self, tab_width): | ||||
""" Sets the width (in terms of space characters) for tab characters. | ||||
""" | ||||
font_metrics = QtGui.QFontMetrics(self.font) | ||||
epatters
|
r2736 | self._control.setTabStopWidth(tab_width * font_metrics.width(' ')) | ||
epatters
|
r2723 | |||
self._tab_width = tab_width | ||||
tab_width = property(_get_tab_width, _set_tab_width) | ||||
epatters
|
r2736 | |||
def undo(self): | ||||
""" Undo the last operation. If there is no operation to undo, nothing | ||||
happens. | ||||
""" | ||||
self._control.undo() | ||||
epatters
|
r2602 | #--------------------------------------------------------------------------- | ||
# 'ConsoleWidget' abstract interface | ||||
#--------------------------------------------------------------------------- | ||||
epatters
|
r2688 | def _is_complete(self, source, interactive): | ||
""" Returns whether 'source' can be executed. When triggered by an | ||||
Enter/Return key press, 'interactive' is True; otherwise, it is | ||||
False. | ||||
""" | ||||
raise NotImplementedError | ||||
def _execute(self, source, hidden): | ||||
""" Execute 'source'. If 'hidden', do not show any output. | ||||
epatters
|
r2602 | """ | ||
raise NotImplementedError | ||||
def _prompt_started_hook(self): | ||||
""" Called immediately after a new prompt is displayed. | ||||
""" | ||||
pass | ||||
def _prompt_finished_hook(self): | ||||
""" Called immediately after a prompt is finished, i.e. when some input | ||||
will be processed and a new prompt displayed. | ||||
""" | ||||
pass | ||||
def _up_pressed(self): | ||||
""" Called when the up key is pressed. Returns whether to continue | ||||
processing the event. | ||||
""" | ||||
return True | ||||
def _down_pressed(self): | ||||
""" Called when the down key is pressed. Returns whether to continue | ||||
processing the event. | ||||
""" | ||||
return True | ||||
def _tab_pressed(self): | ||||
""" Called when the tab key is pressed. Returns whether to continue | ||||
processing the event. | ||||
""" | ||||
return False | ||||
#-------------------------------------------------------------------------- | ||||
# 'ConsoleWidget' protected interface | ||||
#-------------------------------------------------------------------------- | ||||
epatters
|
r2736 | def _append_html(self, html): | ||
""" Appends html at the end of the console buffer. | ||||
""" | ||||
cursor = self._get_end_cursor() | ||||
self._insert_html(cursor, html) | ||||
epatters
|
r2715 | def _append_html_fetching_plain_text(self, html): | ||
""" Appends 'html', then returns the plain text version of it. | ||||
""" | ||||
cursor = self._get_end_cursor() | ||||
epatters
|
r2787 | return self._insert_html_fetching_plain_text(cursor, html) | ||
epatters
|
r2715 | |||
epatters
|
r2736 | 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() | ||||
epatters
|
r2776 | self._insert_plain_text(cursor, text) | ||
epatters
|
r2736 | |||
epatters
|
r2715 | 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 | ||||
epatters
|
r2736 | self._append_plain_text('\n') | ||
epatters
|
r2715 | self._prompt_finished() | ||
epatters
|
r2736 | self._append_plain_text(text) | ||
epatters
|
r2715 | self._show_prompt() | ||
self.input_buffer = input_buffer | ||||
epatters
|
r2736 | 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): | ||||
epatters
|
r2671 | """ Given a KeyboardModifiers flags object, return whether the Control | ||
key is down (on Mac OS, treat the Command key as a synonym for | ||||
Control). | ||||
""" | ||||
down = bool(modifiers & QtCore.Qt.ControlModifier) | ||||
# Note: on Mac OS, ControlModifier corresponds to the Command key while | ||||
# MetaModifier corresponds to the Control key. | ||||
if sys.platform == 'darwin': | ||||
down = down ^ bool(modifiers & QtCore.Qt.MetaModifier) | ||||
return down | ||||
epatters
|
r2736 | |||
epatters
|
r2884 | def _create_control(self): | ||
epatters
|
r2772 | """ Creates and connects the underlying text widget. | ||
epatters
|
r2736 | """ | ||
epatters
|
r2884 | if self.kind == 'plain': | ||
epatters
|
r2865 | control = ConsolePlainTextEdit() | ||
epatters
|
r2884 | elif self.kind == 'rich': | ||
epatters
|
r2865 | control = ConsoleTextEdit() | ||
epatters
|
r2762 | control.setAcceptRichText(False) | ||
epatters
|
r2736 | control.installEventFilter(self) | ||
epatters
|
r2737 | control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) | ||
control.customContextMenuRequested.connect(self._show_context_menu) | ||||
epatters
|
r2736 | control.copyAvailable.connect(self.copy_available) | ||
control.redoAvailable.connect(self.redo_available) | ||||
control.undoAvailable.connect(self.undo_available) | ||||
epatters
|
r2844 | control.setReadOnly(True) | ||
epatters
|
r2864 | control.setUndoRedoEnabled(False) | ||
epatters
|
r2772 | control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) | ||
epatters
|
r2736 | return control | ||
epatters
|
r2776 | def _create_page_control(self): | ||
""" Creates and connects the underlying paging widget. | ||||
""" | ||||
epatters
|
r2865 | control = ConsolePlainTextEdit() | ||
epatters
|
r2776 | control.installEventFilter(self) | ||
control.setReadOnly(True) | ||||
epatters
|
r2864 | control.setUndoRedoEnabled(False) | ||
epatters
|
r2776 | control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) | ||
return control | ||||
def _event_filter_console_keypress(self, event): | ||||
epatters
|
r2736 | """ Filter key events for the underlying text widget to create a | ||
console-like interface. | ||||
""" | ||||
intercepted = False | ||||
cursor = self._control.textCursor() | ||||
position = cursor.position() | ||||
epatters
|
r2772 | key = event.key() | ||
ctrl_down = self._control_key_down(event.modifiers()) | ||||
epatters
|
r2736 | alt_down = event.modifiers() & QtCore.Qt.AltModifier | ||
shift_down = event.modifiers() & QtCore.Qt.ShiftModifier | ||||
if event.matches(QtGui.QKeySequence.Paste): | ||||
epatters
|
r2737 | # Call our paste instead of the underlying text widget's. | ||
epatters
|
r2736 | self.paste() | ||
intercepted = True | ||||
elif ctrl_down: | ||||
epatters
|
r2851 | if key == QtCore.Qt.Key_K: | ||
epatters
|
r2736 | if self._in_buffer(position): | ||
cursor.movePosition(QtGui.QTextCursor.EndOfLine, | ||||
QtGui.QTextCursor.KeepAnchor) | ||||
epatters
|
r2886 | if not cursor.hasSelection(): | ||
# Line deletion (remove continuation prompt) | ||||
cursor.movePosition(QtGui.QTextCursor.NextBlock, | ||||
QtGui.QTextCursor.KeepAnchor) | ||||
cursor.movePosition(QtGui.QTextCursor.Right, | ||||
QtGui.QTextCursor.KeepAnchor, | ||||
len(self._continuation_prompt)) | ||||
epatters
|
r2736 | cursor.removeSelectedText() | ||
intercepted = True | ||||
epatters
|
r2880 | elif key == QtCore.Qt.Key_L: | ||
epatters
|
r2887 | # 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. | ||||
epatters
|
r2880 | self.clear() | ||
intercepted = True | ||||
epatters
|
r2897 | elif key == QtCore.Qt.Key_O: | ||
if self._page_control and self._page_control.isVisible(): | ||||
self._page_control.setFocus() | ||||
intercept = True | ||||
epatters
|
r2736 | elif key == QtCore.Qt.Key_X: | ||
epatters
|
r2880 | # FIXME: Instead of disabling cut completely, only allow it | ||
# when safe. | ||||
epatters
|
r2736 | 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 | ||||
epatters
|
r2897 | elif key == QtCore.Qt.Key_Greater: | ||
self._control.moveCursor(QtGui.QTextCursor.End) | ||||
intercepted = True | ||||
elif key == QtCore.Qt.Key_Less: | ||||
self._control.setTextCursor(self._get_prompt_cursor()) | ||||
intercepted = True | ||||
epatters
|
r2736 | else: | ||
if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): | ||||
intercepted = True | ||||
epatters
|
r2896 | if self._in_buffer(position): | ||
if self._reading: | ||||
self._append_plain_text('\n') | ||||
self._reading = False | ||||
if self._reading_callback: | ||||
self._reading_callback() | ||||
# If there is only whitespace after the cursor, execute. | ||||
# Otherwise, split the line with a continuation prompt. | ||||
elif not self._executing: | ||||
cursor.movePosition(QtGui.QTextCursor.End, | ||||
QtGui.QTextCursor.KeepAnchor) | ||||
if cursor.selectedText().trimmed().isEmpty(): | ||||
self.execute(interactive=True) | ||||
else: | ||||
cursor.beginEditBlock() | ||||
cursor.setPosition(position) | ||||
cursor.insertText('\n') | ||||
self._insert_continuation_prompt(cursor) | ||||
# Ensure that the whole input buffer is visible. | ||||
# FIXME: This will not be usable if the input buffer | ||||
# is taller than the console widget. | ||||
self._control.moveCursor(QtGui.QTextCursor.End) | ||||
self._control.setTextCursor(cursor) | ||||
cursor.endEditBlock() | ||||
epatters
|
r2736 | |||
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: | ||||
epatters
|
r2849 | if not self._reading: | ||
intercepted = not self._tab_pressed() | ||||
epatters
|
r2736 | |||
elif key == QtCore.Qt.Key_Left: | ||||
intercepted = not self._in_buffer(position - 1) | ||||
elif key == QtCore.Qt.Key_Home: | ||||
start_line = cursor.blockNumber() | ||||
if start_line == self._get_prompt_cursor().blockNumber(): | ||||
start_pos = self._prompt_pos | ||||
else: | ||||
epatters
|
r2886 | cursor.movePosition(QtGui.QTextCursor.StartOfBlock, | ||
QtGui.QTextCursor.KeepAnchor) | ||||
epatters
|
r2736 | start_pos = cursor.position() | ||
start_pos += len(self._continuation_prompt) | ||||
epatters
|
r2886 | cursor.setPosition(position) | ||
epatters
|
r2736 | if shift_down and self._in_buffer(position): | ||
epatters
|
r2886 | cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor) | ||
epatters
|
r2736 | else: | ||
epatters
|
r2886 | cursor.setPosition(start_pos) | ||
self._set_cursor(cursor) | ||||
epatters
|
r2736 | intercepted = True | ||
epatters
|
r2764 | elif key == QtCore.Qt.Key_Backspace: | ||
epatters
|
r2736 | |||
# 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: | ||||
epatters
|
r2864 | cursor.beginEditBlock() | ||
epatters
|
r2764 | cursor.movePosition(QtGui.QTextCursor.StartOfBlock, | ||
QtGui.QTextCursor.KeepAnchor) | ||||
epatters
|
r2736 | cursor.removeSelectedText() | ||
epatters
|
r2785 | cursor.deletePreviousChar() | ||
epatters
|
r2864 | cursor.endEditBlock() | ||
epatters
|
r2785 | intercepted = True | ||
epatters
|
r2736 | |||
# 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: | ||||
epatters
|
r2864 | |||
# Line deletion (remove continuation prompt) | ||||
if not self._reading and cursor.atBlockEnd() and not \ | ||||
cursor.hasSelection(): | ||||
cursor.movePosition(QtGui.QTextCursor.NextBlock, | ||||
QtGui.QTextCursor.KeepAnchor) | ||||
cursor.movePosition(QtGui.QTextCursor.Right, | ||||
QtGui.QTextCursor.KeepAnchor, | ||||
len(self._continuation_prompt)) | ||||
cursor.removeSelectedText() | ||||
intercepted = True | ||||
# Regular forwards deletion: | ||||
else: | ||||
anchor = cursor.anchor() | ||||
intercepted = (not self._in_buffer(anchor) or | ||||
not self._in_buffer(position)) | ||||
epatters
|
r2736 | |||
epatters
|
r2737 | # Don't move the cursor if control is down to allow copy-paste using | ||
epatters
|
r2736 | # the keyboard in any part of the buffer. | ||
if not ctrl_down: | ||||
self._keep_cursor_in_buffer() | ||||
return intercepted | ||||
epatters
|
r2715 | |||
epatters
|
r2776 | def _event_filter_page_keypress(self, event): | ||
""" Filter key events for the paging widget to create console-like | ||||
interface. | ||||
""" | ||||
key = event.key() | ||||
epatters
|
r2897 | ctrl_down = self._control_key_down(event.modifiers()) | ||
alt_down = event.modifiers() & QtCore.Qt.AltModifier | ||||
if ctrl_down: | ||||
if key == QtCore.Qt.Key_O: | ||||
self._control.setFocus() | ||||
intercept = True | ||||
epatters
|
r2776 | |||
epatters
|
r2897 | elif alt_down: | ||
if key == QtCore.Qt.Key_Greater: | ||||
self._page_control.moveCursor(QtGui.QTextCursor.End) | ||||
intercepted = True | ||||
elif key == QtCore.Qt.Key_Less: | ||||
self._page_control.moveCursor(QtGui.QTextCursor.Start) | ||||
intercepted = True | ||||
elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape): | ||||
epatters
|
r2776 | if self._splitter: | ||
self._page_control.hide() | ||||
else: | ||||
self.layout().setCurrentWidget(self._control) | ||||
return True | ||||
elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): | ||||
new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, | ||||
epatters
|
r2897 | QtCore.Qt.Key_PageDown, | ||
QtCore.Qt.NoModifier) | ||||
QtGui.qApp.sendEvent(self._page_control, new_event) | ||||
return True | ||||
elif key == QtCore.Qt.Key_Backspace: | ||||
new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, | ||||
QtCore.Qt.Key_PageUp, | ||||
epatters
|
r2776 | QtCore.Qt.NoModifier) | ||
QtGui.qApp.sendEvent(self._page_control, new_event) | ||||
return True | ||||
return False | ||||
epatters
|
r2725 | def _format_as_columns(self, items, separator=' '): | ||
epatters
|
r2723 | """ Transform a list of strings into a single string with columns. | ||
Parameters | ||||
---------- | ||||
epatters
|
r2725 | items : sequence of strings | ||
epatters
|
r2723 | The strings to process. | ||
separator : str, optional [default is two spaces] | ||||
The string that separates columns. | ||||
Returns | ||||
------- | ||||
The formatted string. | ||||
""" | ||||
epatters
|
r2724 | # Note: this code is adapted from columnize 0.3.2. | ||
# See http://code.google.com/p/pycolumnize/ | ||||
epatters
|
r2867 | # Calculate the number of characters available. | ||
epatters
|
r2772 | width = self._control.viewport().width() | ||
epatters
|
r2882 | char_width = QtGui.QFontMetrics(self.font).maxWidth() | ||
epatters
|
r2867 | displaywidth = max(10, (width / char_width) - 1) | ||
epatters
|
r2724 | |||
epatters
|
r2725 | # Some degenerate cases. | ||
epatters
|
r2724 | size = len(items) | ||
if size == 0: | ||||
epatters
|
r2725 | return '\n' | ||
epatters
|
r2724 | elif size == 1: | ||
return '%s\n' % str(items[0]) | ||||
# Try every row count from 1 upwards | ||||
array_index = lambda nrows, row, col: nrows*col + row | ||||
for nrows in range(1, size): | ||||
ncols = (size + nrows - 1) // nrows | ||||
colwidths = [] | ||||
totwidth = -len(separator) | ||||
for col in range(ncols): | ||||
# Get max column width for this column | ||||
colwidth = 0 | ||||
for row in range(nrows): | ||||
i = array_index(nrows, row, col) | ||||
if i >= size: break | ||||
x = items[i] | ||||
colwidth = max(colwidth, len(x)) | ||||
colwidths.append(colwidth) | ||||
totwidth += colwidth + len(separator) | ||||
if totwidth > displaywidth: | ||||
break | ||||
if totwidth <= displaywidth: | ||||
break | ||||
# The smallest number of rows computed and the max widths for each | ||||
# column has been obtained. Now we just have to format each of the rows. | ||||
string = '' | ||||
for row in range(nrows): | ||||
texts = [] | ||||
for col in range(ncols): | ||||
i = row + nrows*col | ||||
if i >= size: | ||||
texts.append('') | ||||
else: | ||||
texts.append(items[i]) | ||||
while texts and not texts[-1]: | ||||
del texts[-1] | ||||
for col in range(len(texts)): | ||||
texts[col] = texts[col].ljust(colwidths[col]) | ||||
epatters
|
r2725 | string += '%s\n' % str(separator.join(texts)) | ||
epatters
|
r2724 | return string | ||
epatters
|
r2723 | |||
epatters
|
r2715 | def _get_block_plain_text(self, block): | ||
""" Given a QTextBlock, return its unformatted text. | ||||
""" | ||||
cursor = QtGui.QTextCursor(block) | ||||
cursor.movePosition(QtGui.QTextCursor.StartOfBlock) | ||||
cursor.movePosition(QtGui.QTextCursor.EndOfBlock, | ||||
QtGui.QTextCursor.KeepAnchor) | ||||
return str(cursor.selection().toPlainText()) | ||||
epatters
|
r2736 | |||
def _get_cursor(self): | ||||
""" Convenience method that returns a cursor for the current position. | ||||
""" | ||||
return self._control.textCursor() | ||||
epatters
|
r2602 | |||
def _get_end_cursor(self): | ||||
""" Convenience method that returns a cursor for the last character. | ||||
""" | ||||
epatters
|
r2736 | cursor = self._control.textCursor() | ||
epatters
|
r2602 | cursor.movePosition(QtGui.QTextCursor.End) | ||
return cursor | ||||
epatters
|
r2841 | def _get_input_buffer_cursor_column(self): | ||
""" Returns the column of the cursor in the input buffer, excluding the | ||||
contribution by the prompt, or -1 if there is no such column. | ||||
""" | ||||
prompt = self._get_input_buffer_cursor_prompt() | ||||
if prompt is None: | ||||
return -1 | ||||
else: | ||||
cursor = self._control.textCursor() | ||||
return cursor.columnNumber() - len(prompt) | ||||
epatters
|
r2763 | def _get_input_buffer_cursor_line(self): | ||
epatters
|
r2841 | """ Returns line of the input buffer that contains the cursor, or None | ||
if there is no such line. | ||||
""" | ||||
prompt = self._get_input_buffer_cursor_prompt() | ||||
if prompt is None: | ||||
return None | ||||
else: | ||||
cursor = self._control.textCursor() | ||||
text = self._get_block_plain_text(cursor.block()) | ||||
return text[len(prompt):] | ||||
def _get_input_buffer_cursor_prompt(self): | ||||
""" Returns the (plain text) prompt for line of the input buffer that | ||||
contains the cursor, or None if there is no such line. | ||||
epatters
|
r2763 | """ | ||
if self._executing: | ||||
return None | ||||
cursor = self._control.textCursor() | ||||
if cursor.position() >= self._prompt_pos: | ||||
if cursor.blockNumber() == self._get_prompt_cursor().blockNumber(): | ||||
epatters
|
r2841 | return self._prompt | ||
epatters
|
r2763 | else: | ||
epatters
|
r2841 | return self._continuation_prompt | ||
epatters
|
r2763 | else: | ||
return None | ||||
epatters
|
r2602 | def _get_prompt_cursor(self): | ||
""" Convenience method that returns a cursor for the prompt position. | ||||
""" | ||||
epatters
|
r2736 | cursor = self._control.textCursor() | ||
epatters
|
r2602 | cursor.setPosition(self._prompt_pos) | ||
return cursor | ||||
def _get_selection_cursor(self, start, end): | ||||
""" Convenience method that returns a cursor with text selected between | ||||
the positions 'start' and 'end'. | ||||
""" | ||||
epatters
|
r2736 | cursor = self._control.textCursor() | ||
epatters
|
r2602 | cursor.setPosition(start) | ||
cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor) | ||||
return cursor | ||||
def _get_word_start_cursor(self, position): | ||||
""" Find the start of the word to the left the given position. If a | ||||
sequence of non-word characters precedes the first word, skip over | ||||
them. (This emulates the behavior of bash, emacs, etc.) | ||||
""" | ||||
epatters
|
r2736 | document = self._control.document() | ||
epatters
|
r2602 | position -= 1 | ||
epatters
|
r2764 | while position >= self._prompt_pos and \ | ||
epatters
|
r2602 | not document.characterAt(position).isLetterOrNumber(): | ||
position -= 1 | ||||
epatters
|
r2764 | while position >= self._prompt_pos and \ | ||
epatters
|
r2602 | document.characterAt(position).isLetterOrNumber(): | ||
position -= 1 | ||||
epatters
|
r2736 | cursor = self._control.textCursor() | ||
epatters
|
r2602 | cursor.setPosition(position + 1) | ||
return cursor | ||||
def _get_word_end_cursor(self, position): | ||||
""" Find the end of the word to the right the given position. If a | ||||
sequence of non-word characters precedes the first word, skip over | ||||
them. (This emulates the behavior of bash, emacs, etc.) | ||||
""" | ||||
epatters
|
r2736 | document = self._control.document() | ||
epatters
|
r2602 | end = self._get_end_cursor().position() | ||
while position < end and \ | ||||
not document.characterAt(position).isLetterOrNumber(): | ||||
position += 1 | ||||
while position < end and \ | ||||
document.characterAt(position).isLetterOrNumber(): | ||||
position += 1 | ||||
epatters
|
r2736 | cursor = self._control.textCursor() | ||
epatters
|
r2602 | cursor.setPosition(position) | ||
return cursor | ||||
epatters
|
r2896 | def _insert_continuation_prompt(self, cursor): | ||
""" Inserts new continuation prompt using the specified cursor. | ||||
""" | ||||
if self._continuation_prompt_html is None: | ||||
self._insert_plain_text(cursor, self._continuation_prompt) | ||||
else: | ||||
self._continuation_prompt = self._insert_html_fetching_plain_text( | ||||
cursor, self._continuation_prompt_html) | ||||
epatters
|
r2733 | def _insert_html(self, cursor, html): | ||
epatters
|
r2787 | """ Inserts HTML using the specified cursor in such a way that future | ||
epatters
|
r2733 | formatting is unaffected. | ||
""" | ||||
epatters
|
r2764 | cursor.beginEditBlock() | ||
epatters
|
r2733 | cursor.insertHtml(html) | ||
epatters
|
r2763 | # After inserting HTML, the text document "remembers" it's in "html | ||
# mode", which means that subsequent calls adding plain text will result | ||||
# in unwanted formatting, lost tab characters, etc. The following code | ||||
epatters
|
r2797 | # hacks around this behavior, which I consider to be a bug in Qt, by | ||
# (crudely) resetting the document's style state. | ||||
cursor.movePosition(QtGui.QTextCursor.Left, | ||||
epatters
|
r2733 | QtGui.QTextCursor.KeepAnchor) | ||
epatters
|
r2763 | if cursor.selection().toPlainText() == ' ': | ||
cursor.removeSelectedText() | ||||
epatters
|
r2797 | else: | ||
cursor.movePosition(QtGui.QTextCursor.Right) | ||||
epatters
|
r2763 | cursor.insertText(' ', QtGui.QTextCharFormat()) | ||
epatters
|
r2764 | cursor.endEditBlock() | ||
epatters
|
r2763 | |||
epatters
|
r2787 | def _insert_html_fetching_plain_text(self, cursor, html): | ||
""" Inserts HTML using the specified cursor, then returns its plain text | ||||
version. | ||||
""" | ||||
epatters
|
r2800 | cursor.beginEditBlock() | ||
cursor.removeSelectedText() | ||||
epatters
|
r2787 | start = cursor.position() | ||
self._insert_html(cursor, html) | ||||
end = cursor.position() | ||||
cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor) | ||||
text = str(cursor.selection().toPlainText()) | ||||
epatters
|
r2800 | |||
epatters
|
r2787 | cursor.setPosition(end) | ||
epatters
|
r2800 | cursor.endEditBlock() | ||
epatters
|
r2787 | return text | ||
epatters
|
r2776 | def _insert_plain_text(self, cursor, text): | ||
""" Inserts plain text using the specified cursor, processing ANSI codes | ||||
if enabled. | ||||
""" | ||||
cursor.beginEditBlock() | ||||
if self.ansi_codes: | ||||
for substring in self._ansi_processor.split_string(text): | ||||
epatters
|
r2783 | for action in self._ansi_processor.actions: | ||
if action.kind == 'erase' and action.area == 'screen': | ||||
cursor.select(QtGui.QTextCursor.Document) | ||||
cursor.removeSelectedText() | ||||
epatters
|
r2776 | format = self._ansi_processor.get_format() | ||
cursor.insertText(substring, format) | ||||
else: | ||||
cursor.insertText(text) | ||||
cursor.endEditBlock() | ||||
epatters
|
r2787 | def _insert_plain_text_into_buffer(self, text): | ||
epatters
|
r2763 | """ Inserts text into the input buffer at the current cursor position, | ||
ensuring that continuation prompts are inserted as necessary. | ||||
""" | ||||
lines = str(text).splitlines(True) | ||||
if lines: | ||||
self._keep_cursor_in_buffer() | ||||
cursor = self._control.textCursor() | ||||
cursor.beginEditBlock() | ||||
cursor.insertText(lines[0]) | ||||
for line in lines[1:]: | ||||
if self._continuation_prompt_html is None: | ||||
cursor.insertText(self._continuation_prompt) | ||||
else: | ||||
epatters
|
r2787 | self._continuation_prompt = \ | ||
self._insert_html_fetching_plain_text( | ||||
cursor, self._continuation_prompt_html) | ||||
epatters
|
r2763 | cursor.insertText(line) | ||
cursor.endEditBlock() | ||||
self._control.setTextCursor(cursor) | ||||
epatters
|
r2733 | |||
epatters
|
r2847 | def _in_buffer(self, position=None): | ||
""" Returns whether the current cursor (or, if specified, a position) is | ||||
inside the editing region. | ||||
epatters
|
r2736 | """ | ||
epatters
|
r2764 | cursor = self._control.textCursor() | ||
epatters
|
r2847 | if position is None: | ||
position = cursor.position() | ||||
else: | ||||
cursor.setPosition(position) | ||||
epatters
|
r2764 | line = cursor.blockNumber() | ||
prompt_line = self._get_prompt_cursor().blockNumber() | ||||
if line == prompt_line: | ||||
return position >= self._prompt_pos | ||||
elif line > prompt_line: | ||||
cursor.movePosition(QtGui.QTextCursor.StartOfBlock) | ||||
prompt_pos = cursor.position() + len(self._continuation_prompt) | ||||
return position >= prompt_pos | ||||
return False | ||||
epatters
|
r2736 | |||
def _keep_cursor_in_buffer(self): | ||||
""" Ensures that the cursor is inside the editing region. Returns | ||||
whether the cursor was moved. | ||||
""" | ||||
epatters
|
r2847 | moved = not self._in_buffer() | ||
if moved: | ||||
cursor = self._control.textCursor() | ||||
epatters
|
r2736 | cursor.movePosition(QtGui.QTextCursor.End) | ||
self._control.setTextCursor(cursor) | ||||
epatters
|
r2847 | return moved | ||
epatters
|
r2776 | |||
def _page(self, text): | ||||
epatters
|
r2843 | """ Displays text using the pager if it exceeds the height of the | ||
visible area. | ||||
epatters
|
r2776 | """ | ||
epatters
|
r2884 | if self.paging == 'none': | ||
epatters
|
r2776 | self._append_plain_text(text) | ||
else: | ||||
epatters
|
r2843 | line_height = QtGui.QFontMetrics(self.font).height() | ||
minlines = self._control.viewport().height() / line_height | ||||
if re.match("(?:[^\n]*\n){%i}" % minlines, text): | ||||
epatters
|
r2884 | if self.paging == 'custom': | ||
epatters
|
r2843 | self.custom_page_requested.emit(text) | ||
else: | ||||
self._page_control.clear() | ||||
cursor = self._page_control.textCursor() | ||||
self._insert_plain_text(cursor, text) | ||||
self._page_control.moveCursor(QtGui.QTextCursor.Start) | ||||
self._page_control.viewport().resize(self._control.size()) | ||||
if self._splitter: | ||||
self._page_control.show() | ||||
self._page_control.setFocus() | ||||
else: | ||||
self.layout().setCurrentWidget(self._page_control) | ||||
epatters
|
r2776 | else: | ||
epatters
|
r2843 | self._append_plain_text(text) | ||
epatters
|
r2736 | |||
epatters
|
r2602 | def _prompt_started(self): | ||
""" Called immediately after a new prompt is displayed. | ||||
""" | ||||
epatters
|
r2688 | # Temporarily disable the maximum block count to permit undo/redo and | ||
# to ensure that the prompt position does not change due to truncation. | ||||
epatters
|
r2864 | # Because setting this property clears the undo/redo history, we only | ||
# set it if we have to. | ||||
if self._control.document().maximumBlockCount() > 0: | ||||
self._control.document().setMaximumBlockCount(0) | ||||
epatters
|
r2736 | self._control.setUndoRedoEnabled(True) | ||
epatters
|
r2615 | |||
epatters
|
r2736 | self._control.setReadOnly(False) | ||
self._control.moveCursor(QtGui.QTextCursor.End) | ||||
epatters
|
r2615 | |||
epatters
|
r2602 | 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. | ||||
""" | ||||
epatters
|
r2736 | self._control.setReadOnly(True) | ||
epatters
|
r2602 | self._prompt_finished_hook() | ||
epatters
|
r2706 | def _readline(self, prompt='', callback=None): | ||
""" Reads one line of input from the user. | ||||
Parameters | ||||
---------- | ||||
prompt : str, optional | ||||
The prompt to print before reading the line. | ||||
callback : callable, optional | ||||
A callback to execute with the read line. If not specified, input is | ||||
read *synchronously* and this method does not return until it has | ||||
been read. | ||||
Returns | ||||
------- | ||||
If a callback is specified, returns nothing. Otherwise, returns the | ||||
input string with the trailing newline stripped. | ||||
epatters
|
r2705 | """ | ||
epatters
|
r2706 | if self._reading: | ||
raise RuntimeError('Cannot read a line. Widget is already reading.') | ||||
if not callback and not self.isVisible(): | ||||
# If the user cannot see the widget, this function cannot return. | ||||
epatters
|
r2866 | raise RuntimeError('Cannot synchronously read a line if the widget ' | ||
epatters
|
r2706 | 'is not visible!') | ||
epatters
|
r2705 | |||
self._reading = True | ||||
epatters
|
r2708 | self._show_prompt(prompt, newline=False) | ||
epatters
|
r2706 | |||
if callback is None: | ||||
self._reading_callback = None | ||||
while self._reading: | ||||
QtCore.QCoreApplication.processEvents() | ||||
return self.input_buffer.rstrip('\n') | ||||
else: | ||||
self._reading_callback = lambda: \ | ||||
callback(self.input_buffer.rstrip('\n')) | ||||
epatters
|
r2705 | |||
epatters
|
r2715 | def _set_continuation_prompt(self, prompt, html=False): | ||
""" Sets the continuation prompt. | ||||
Parameters | ||||
---------- | ||||
prompt : str | ||||
The prompt to show when more input is needed. | ||||
html : bool, optional (default False) | ||||
If set, the prompt will be inserted as formatted HTML. Otherwise, | ||||
the prompt will be treated as plain text, though ANSI color codes | ||||
will be handled. | ||||
""" | ||||
if html: | ||||
self._continuation_prompt_html = prompt | ||||
else: | ||||
self._continuation_prompt = prompt | ||||
self._continuation_prompt_html = None | ||||
epatters
|
r2736 | |||
def _set_cursor(self, cursor): | ||||
""" Convenience method to set the current cursor. | ||||
""" | ||||
self._control.setTextCursor(cursor) | ||||
epatters
|
r2715 | |||
epatters
|
r2737 | def _show_context_menu(self, pos): | ||
""" Shows a context menu at the given QPoint (in widget coordinates). | ||||
""" | ||||
menu = QtGui.QMenu() | ||||
epatters
|
r2765 | copy_action = menu.addAction('Copy', self.copy) | ||
epatters
|
r2737 | copy_action.setEnabled(self._get_cursor().hasSelection()) | ||
copy_action.setShortcut(QtGui.QKeySequence.Copy) | ||||
epatters
|
r2765 | paste_action = menu.addAction('Paste', self.paste) | ||
epatters
|
r2762 | paste_action.setEnabled(self.can_paste()) | ||
epatters
|
r2737 | paste_action.setShortcut(QtGui.QKeySequence.Paste) | ||
epatters
|
r2765 | menu.addSeparator() | ||
menu.addAction('Select All', self.select_all) | ||||
epatters
|
r2737 | |||
menu.exec_(self._control.mapToGlobal(pos)) | ||||
epatters
|
r2715 | def _show_prompt(self, prompt=None, html=False, newline=True): | ||
epatters
|
r2708 | """ Writes a new prompt at the end of the buffer. | ||
Parameters | ||||
---------- | ||||
prompt : str, optional | ||||
The prompt to show. If not specified, the previous prompt is used. | ||||
epatters
|
r2715 | html : bool, optional (default False) | ||
Only relevant when a prompt is specified. If set, the prompt will | ||||
be inserted as formatted HTML. Otherwise, the prompt will be treated | ||||
as plain text, though ANSI color codes will be handled. | ||||
epatters
|
r2708 | newline : bool, optional (default True) | ||
If set, a new line will be written before showing the prompt if | ||||
there is not already a newline at the end of the buffer. | ||||
""" | ||||
epatters
|
r2715 | # Insert a preliminary newline, if necessary. | ||
epatters
|
r2708 | if newline: | ||
cursor = self._get_end_cursor() | ||||
if cursor.position() > 0: | ||||
cursor.movePosition(QtGui.QTextCursor.Left, | ||||
QtGui.QTextCursor.KeepAnchor) | ||||
if str(cursor.selection().toPlainText()) != '\n': | ||||
epatters
|
r2736 | self._append_plain_text('\n') | ||
epatters
|
r2705 | |||
epatters
|
r2715 | # Write the prompt. | ||
epatters
|
r2866 | self._append_plain_text(self._prompt_sep) | ||
epatters
|
r2715 | if prompt is None: | ||
if self._prompt_html is None: | ||||
epatters
|
r2736 | self._append_plain_text(self._prompt) | ||
epatters
|
r2715 | else: | ||
epatters
|
r2736 | self._append_html(self._prompt_html) | ||
epatters
|
r2715 | else: | ||
if html: | ||||
self._prompt = self._append_html_fetching_plain_text(prompt) | ||||
self._prompt_html = prompt | ||||
else: | ||||
epatters
|
r2736 | self._append_plain_text(prompt) | ||
epatters
|
r2715 | self._prompt = prompt | ||
self._prompt_html = None | ||||
epatters
|
r2705 | |||
epatters
|
r2602 | self._prompt_pos = self._get_end_cursor().position() | ||
self._prompt_started() | ||||
class HistoryConsoleWidget(ConsoleWidget): | ||||
""" A ConsoleWidget that keeps a history of the commands that have been | ||||
executed. | ||||
""" | ||||
#--------------------------------------------------------------------------- | ||||
epatters
|
r2736 | # 'object' interface | ||
epatters
|
r2602 | #--------------------------------------------------------------------------- | ||
epatters
|
r2736 | def __init__(self, *args, **kw): | ||
super(HistoryConsoleWidget, self).__init__(*args, **kw) | ||||
epatters
|
r2602 | self._history = [] | ||
self._history_index = 0 | ||||
#--------------------------------------------------------------------------- | ||||
# 'ConsoleWidget' public interface | ||||
#--------------------------------------------------------------------------- | ||||
epatters
|
r2688 | def execute(self, source=None, hidden=False, interactive=False): | ||
epatters
|
r2602 | """ Reimplemented to the store history. | ||
""" | ||||
epatters
|
r2689 | if not hidden: | ||
history = self.input_buffer if source is None else source | ||||
epatters
|
r2688 | |||
executed = super(HistoryConsoleWidget, self).execute( | ||||
source, hidden, interactive) | ||||
if executed and not hidden: | ||||
epatters
|
r2880 | # Save the command unless it was a blank line. | ||
history = history.rstrip() | ||||
if history: | ||||
self._history.append(history) | ||||
self._history_index = len(self._history) | ||||
epatters
|
r2688 | |||
epatters
|
r2602 | return executed | ||
#--------------------------------------------------------------------------- | ||||
# 'ConsoleWidget' abstract interface | ||||
#--------------------------------------------------------------------------- | ||||
def _up_pressed(self): | ||||
""" Called when the up key is pressed. Returns whether to continue | ||||
processing the event. | ||||
""" | ||||
prompt_cursor = self._get_prompt_cursor() | ||||
epatters
|
r2736 | if self._get_cursor().blockNumber() == prompt_cursor.blockNumber(): | ||
epatters
|
r2602 | self.history_previous() | ||
# Go to the first line of prompt for seemless history scrolling. | ||||
cursor = self._get_prompt_cursor() | ||||
cursor.movePosition(QtGui.QTextCursor.EndOfLine) | ||||
epatters
|
r2736 | self._set_cursor(cursor) | ||
epatters
|
r2602 | |||
return False | ||||
return True | ||||
def _down_pressed(self): | ||||
""" Called when the down key is pressed. Returns whether to continue | ||||
processing the event. | ||||
""" | ||||
end_cursor = self._get_end_cursor() | ||||
epatters
|
r2736 | if self._get_cursor().blockNumber() == end_cursor.blockNumber(): | ||
epatters
|
r2602 | self.history_next() | ||
return False | ||||
return True | ||||
#--------------------------------------------------------------------------- | ||||
epatters
|
r2844 | # 'HistoryConsoleWidget' public interface | ||
epatters
|
r2602 | #--------------------------------------------------------------------------- | ||
def history_previous(self): | ||||
""" If possible, set the input buffer to the previous item in the | ||||
history. | ||||
""" | ||||
if self._history_index > 0: | ||||
self._history_index -= 1 | ||||
self.input_buffer = self._history[self._history_index] | ||||
def history_next(self): | ||||
""" Set the input buffer to the next item in the history, or a blank | ||||
line if there is no subsequent item. | ||||
""" | ||||
if self._history_index < len(self._history): | ||||
self._history_index += 1 | ||||
if self._history_index < len(self._history): | ||||
self.input_buffer = self._history[self._history_index] | ||||
else: | ||||
self.input_buffer = '' | ||||
epatters
|
r2844 | |||
#--------------------------------------------------------------------------- | ||||
# 'HistoryConsoleWidget' protected interface | ||||
#--------------------------------------------------------------------------- | ||||
def _set_history(self, history): | ||||
""" Replace the current history with a sequence of history items. | ||||
""" | ||||
self._history = list(history) | ||||
self._history_index = len(self._history) | ||||