##// END OF EJS Templates
BUG: Completion widget position and pager focus....
Prabhu Ramachandran -
Show More
@@ -1,133 +1,134 b''
1 # System library imports
1 # System library imports
2 from IPython.external.qt import QtCore, QtGui
2 from IPython.external.qt import QtCore, QtGui
3
3
4
4
5 class CompletionWidget(QtGui.QListWidget):
5 class CompletionWidget(QtGui.QListWidget):
6 """ A widget for GUI tab completion.
6 """ A widget for GUI tab completion.
7 """
7 """
8
8
9 #--------------------------------------------------------------------------
9 #--------------------------------------------------------------------------
10 # 'QObject' interface
10 # 'QObject' interface
11 #--------------------------------------------------------------------------
11 #--------------------------------------------------------------------------
12
12
13 def __init__(self, text_edit):
13 def __init__(self, text_edit):
14 """ Create a completion widget that is attached to the specified Qt
14 """ Create a completion widget that is attached to the specified Qt
15 text edit widget.
15 text edit widget.
16 """
16 """
17 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
17 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 super(CompletionWidget, self).__init__()
18 super(CompletionWidget, self).__init__()
19
19
20 self._text_edit = text_edit
20 self._text_edit = text_edit
21
21
22 self.setAttribute(QtCore.Qt.WA_StaticContents)
22 self.setAttribute(QtCore.Qt.WA_StaticContents)
23 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
23 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
24
24
25 # Ensure that the text edit keeps focus when widget is displayed.
25 # Ensure that the text edit keeps focus when widget is displayed.
26 self.setFocusProxy(self._text_edit)
26 self.setFocusProxy(self._text_edit)
27
27
28 self.setFrameShadow(QtGui.QFrame.Plain)
28 self.setFrameShadow(QtGui.QFrame.Plain)
29 self.setFrameShape(QtGui.QFrame.StyledPanel)
29 self.setFrameShape(QtGui.QFrame.StyledPanel)
30
30
31 self.itemActivated.connect(self._complete_current)
31 self.itemActivated.connect(self._complete_current)
32
32
33 def eventFilter(self, obj, event):
33 def eventFilter(self, obj, event):
34 """ Reimplemented to handle keyboard input and to auto-hide when the
34 """ Reimplemented to handle keyboard input and to auto-hide when the
35 text edit loses focus.
35 text edit loses focus.
36 """
36 """
37 if obj == self._text_edit:
37 if obj == self._text_edit:
38 etype = event.type()
38 etype = event.type()
39
39
40 if etype == QtCore.QEvent.KeyPress:
40 if etype == QtCore.QEvent.KeyPress:
41 key, text = event.key(), event.text()
41 key, text = event.key(), event.text()
42 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
42 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
43 QtCore.Qt.Key_Tab):
43 QtCore.Qt.Key_Tab):
44 self._complete_current()
44 self._complete_current()
45 return True
45 return True
46 elif key == QtCore.Qt.Key_Escape:
46 elif key == QtCore.Qt.Key_Escape:
47 self.hide()
47 self.hide()
48 return True
48 return True
49 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
49 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
50 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
50 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
51 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
51 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
52 self.keyPressEvent(event)
52 self.keyPressEvent(event)
53 return True
53 return True
54
54
55 elif etype == QtCore.QEvent.FocusOut:
55 elif etype == QtCore.QEvent.FocusOut:
56 self.hide()
56 self.hide()
57
57
58 return super(CompletionWidget, self).eventFilter(obj, event)
58 return super(CompletionWidget, self).eventFilter(obj, event)
59
59
60 #--------------------------------------------------------------------------
60 #--------------------------------------------------------------------------
61 # 'QWidget' interface
61 # 'QWidget' interface
62 #--------------------------------------------------------------------------
62 #--------------------------------------------------------------------------
63
63
64 def hideEvent(self, event):
64 def hideEvent(self, event):
65 """ Reimplemented to disconnect signal handlers and event filter.
65 """ Reimplemented to disconnect signal handlers and event filter.
66 """
66 """
67 super(CompletionWidget, self).hideEvent(event)
67 super(CompletionWidget, self).hideEvent(event)
68 self._text_edit.cursorPositionChanged.disconnect(self._update_current)
68 self._text_edit.cursorPositionChanged.disconnect(self._update_current)
69 self._text_edit.removeEventFilter(self)
69 self._text_edit.removeEventFilter(self)
70
70
71 def showEvent(self, event):
71 def showEvent(self, event):
72 """ Reimplemented to connect signal handlers and event filter.
72 """ Reimplemented to connect signal handlers and event filter.
73 """
73 """
74 super(CompletionWidget, self).showEvent(event)
74 super(CompletionWidget, self).showEvent(event)
75 self._text_edit.cursorPositionChanged.connect(self._update_current)
75 self._text_edit.cursorPositionChanged.connect(self._update_current)
76 self._text_edit.installEventFilter(self)
76 self._text_edit.installEventFilter(self)
77
77
78 #--------------------------------------------------------------------------
78 #--------------------------------------------------------------------------
79 # 'CompletionWidget' interface
79 # 'CompletionWidget' interface
80 #--------------------------------------------------------------------------
80 #--------------------------------------------------------------------------
81
81
82 def show_items(self, cursor, items):
82 def show_items(self, cursor, items):
83 """ Shows the completion widget with 'items' at the position specified
83 """ Shows the completion widget with 'items' at the position specified
84 by 'cursor'.
84 by 'cursor'.
85 """
85 """
86 text_edit = self._text_edit
86 text_edit = self._text_edit
87 point = text_edit.cursorRect(cursor).bottomRight()
87 point = text_edit.cursorRect(cursor).bottomRight()
88 point = text_edit.mapToGlobal(point)
88 point = text_edit.mapToGlobal(point)
89 height = self.sizeHint().height()
89 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
90 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
90 if screen_rect.size().height() - point.y() - self.height() < 0:
91 if screen_rect.size().height() - point.y() - height < 0:
91 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
92 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
92 point.setY(point.y() - self.height())
93 point.setY(point.y() - height)
93 self.move(point)
94 self.move(point)
94
95
95 self._start_position = cursor.position()
96 self._start_position = cursor.position()
96 self.clear()
97 self.clear()
97 self.addItems(items)
98 self.addItems(items)
98 self.setCurrentRow(0)
99 self.setCurrentRow(0)
99 self.show()
100 self.show()
100
101
101 #--------------------------------------------------------------------------
102 #--------------------------------------------------------------------------
102 # Protected interface
103 # Protected interface
103 #--------------------------------------------------------------------------
104 #--------------------------------------------------------------------------
104
105
105 def _complete_current(self):
106 def _complete_current(self):
106 """ Perform the completion with the currently selected item.
107 """ Perform the completion with the currently selected item.
107 """
108 """
108 self._current_text_cursor().insertText(self.currentItem().text())
109 self._current_text_cursor().insertText(self.currentItem().text())
109 self.hide()
110 self.hide()
110
111
111 def _current_text_cursor(self):
112 def _current_text_cursor(self):
112 """ Returns a cursor with text between the start position and the
113 """ Returns a cursor with text between the start position and the
113 current position selected.
114 current position selected.
114 """
115 """
115 cursor = self._text_edit.textCursor()
116 cursor = self._text_edit.textCursor()
116 if cursor.position() >= self._start_position:
117 if cursor.position() >= self._start_position:
117 cursor.setPosition(self._start_position,
118 cursor.setPosition(self._start_position,
118 QtGui.QTextCursor.KeepAnchor)
119 QtGui.QTextCursor.KeepAnchor)
119 return cursor
120 return cursor
120
121
121 def _update_current(self):
122 def _update_current(self):
122 """ Updates the current item based on the current text.
123 """ Updates the current item based on the current text.
123 """
124 """
124 prefix = self._current_text_cursor().selection().toPlainText()
125 prefix = self._current_text_cursor().selection().toPlainText()
125 if prefix:
126 if prefix:
126 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
127 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
127 QtCore.Qt.MatchCaseSensitive))
128 QtCore.Qt.MatchCaseSensitive))
128 if items:
129 if items:
129 self.setCurrentItem(items[0])
130 self.setCurrentItem(items[0])
130 else:
131 else:
131 self.hide()
132 self.hide()
132 else:
133 else:
133 self.hide()
134 self.hide()
@@ -1,1786 +1,1787 b''
1 """ An abstract base class for console-type widgets.
1 """ An abstract base class for console-type widgets.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 # Standard library imports
7 # Standard library imports
8 import os
8 import os
9 from os.path import commonprefix
9 from os.path import commonprefix
10 import re
10 import re
11 import sys
11 import sys
12 from textwrap import dedent
12 from textwrap import dedent
13 from unicodedata import category
13 from unicodedata import category
14
14
15 # System library imports
15 # System library imports
16 from IPython.external.qt import QtCore, QtGui
16 from IPython.external.qt import QtCore, QtGui
17
17
18 # Local imports
18 # Local imports
19 from IPython.config.configurable import LoggingConfigurable
19 from IPython.config.configurable import LoggingConfigurable
20 from IPython.frontend.qt.rich_text import HtmlExporter
20 from IPython.frontend.qt.rich_text import HtmlExporter
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 from IPython.utils.text import columnize
22 from IPython.utils.text import columnize
23 from IPython.utils.traitlets import Bool, Enum, Int, Unicode
23 from IPython.utils.traitlets import Bool, Enum, Int, Unicode
24 from ansi_code_processor import QtAnsiCodeProcessor
24 from ansi_code_processor import QtAnsiCodeProcessor
25 from completion_widget import CompletionWidget
25 from completion_widget import CompletionWidget
26 from kill_ring import QtKillRing
26 from kill_ring import QtKillRing
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Functions
29 # Functions
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 def is_letter_or_number(char):
32 def is_letter_or_number(char):
33 """ Returns whether the specified unicode character is a letter or a number.
33 """ Returns whether the specified unicode character is a letter or a number.
34 """
34 """
35 cat = category(char)
35 cat = category(char)
36 return cat.startswith('L') or cat.startswith('N')
36 return cat.startswith('L') or cat.startswith('N')
37
37
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 # Classes
39 # Classes
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41
41
42 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
42 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
43 """ An abstract base class for console-type widgets. This class has
43 """ An abstract base class for console-type widgets. This class has
44 functionality for:
44 functionality for:
45
45
46 * Maintaining a prompt and editing region
46 * Maintaining a prompt and editing region
47 * Providing the traditional Unix-style console keyboard shortcuts
47 * Providing the traditional Unix-style console keyboard shortcuts
48 * Performing tab completion
48 * Performing tab completion
49 * Paging text
49 * Paging text
50 * Handling ANSI escape codes
50 * Handling ANSI escape codes
51
51
52 ConsoleWidget also provides a number of utility methods that will be
52 ConsoleWidget also provides a number of utility methods that will be
53 convenient to implementors of a console-style widget.
53 convenient to implementors of a console-style widget.
54 """
54 """
55 __metaclass__ = MetaQObjectHasTraits
55 __metaclass__ = MetaQObjectHasTraits
56
56
57 #------ Configuration ------------------------------------------------------
57 #------ Configuration ------------------------------------------------------
58
58
59 ansi_codes = Bool(True, config=True,
59 ansi_codes = Bool(True, config=True,
60 help="Whether to process ANSI escape codes."
60 help="Whether to process ANSI escape codes."
61 )
61 )
62 buffer_size = Int(500, config=True,
62 buffer_size = Int(500, config=True,
63 help="""
63 help="""
64 The maximum number of lines of text before truncation. Specifying a
64 The maximum number of lines of text before truncation. Specifying a
65 non-positive number disables text truncation (not recommended).
65 non-positive number disables text truncation (not recommended).
66 """
66 """
67 )
67 )
68 gui_completion = Bool(False, config=True,
68 gui_completion = Bool(False, config=True,
69 help="""
69 help="""
70 Use a list widget instead of plain text output for tab completion.
70 Use a list widget instead of plain text output for tab completion.
71 """
71 """
72 )
72 )
73 # NOTE: this value can only be specified during initialization.
73 # NOTE: this value can only be specified during initialization.
74 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
74 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
75 help="""
75 help="""
76 The type of underlying text widget to use. Valid values are 'plain',
76 The type of underlying text widget to use. Valid values are 'plain',
77 which specifies a QPlainTextEdit, and 'rich', which specifies a
77 which specifies a QPlainTextEdit, and 'rich', which specifies a
78 QTextEdit.
78 QTextEdit.
79 """
79 """
80 )
80 )
81 # NOTE: this value can only be specified during initialization.
81 # NOTE: this value can only be specified during initialization.
82 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
82 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
83 default_value='inside', config=True,
83 default_value='inside', config=True,
84 help="""
84 help="""
85 The type of paging to use. Valid values are:
85 The type of paging to use. Valid values are:
86
86
87 'inside' : The widget pages like a traditional terminal.
87 'inside' : The widget pages like a traditional terminal.
88 'hsplit' : When paging is requested, the widget is split
88 'hsplit' : When paging is requested, the widget is split
89 horizontally. The top pane contains the console, and the
89 horizontally. The top pane contains the console, and the
90 bottom pane contains the paged text.
90 bottom pane contains the paged text.
91 'vsplit' : Similar to 'hsplit', except that a vertical splitter
91 'vsplit' : Similar to 'hsplit', except that a vertical splitter
92 used.
92 used.
93 'custom' : No action is taken by the widget beyond emitting a
93 'custom' : No action is taken by the widget beyond emitting a
94 'custom_page_requested(str)' signal.
94 'custom_page_requested(str)' signal.
95 'none' : The text is written directly to the console.
95 'none' : The text is written directly to the console.
96 """)
96 """)
97
97
98 font_family = Unicode(config=True,
98 font_family = Unicode(config=True,
99 help="""The font family to use for the console.
99 help="""The font family to use for the console.
100 On OSX this defaults to Monaco, on Windows the default is
100 On OSX this defaults to Monaco, on Windows the default is
101 Consolas with fallback of Courier, and on other platforms
101 Consolas with fallback of Courier, and on other platforms
102 the default is Monospace.
102 the default is Monospace.
103 """)
103 """)
104 def _font_family_default(self):
104 def _font_family_default(self):
105 if sys.platform == 'win32':
105 if sys.platform == 'win32':
106 # Consolas ships with Vista/Win7, fallback to Courier if needed
106 # Consolas ships with Vista/Win7, fallback to Courier if needed
107 return 'Consolas'
107 return 'Consolas'
108 elif sys.platform == 'darwin':
108 elif sys.platform == 'darwin':
109 # OSX always has Monaco, no need for a fallback
109 # OSX always has Monaco, no need for a fallback
110 return 'Monaco'
110 return 'Monaco'
111 else:
111 else:
112 # Monospace should always exist, no need for a fallback
112 # Monospace should always exist, no need for a fallback
113 return 'Monospace'
113 return 'Monospace'
114
114
115 font_size = Int(config=True,
115 font_size = Int(config=True,
116 help="""The font size. If unconfigured, Qt will be entrusted
116 help="""The font size. If unconfigured, Qt will be entrusted
117 with the size of the font.
117 with the size of the font.
118 """)
118 """)
119
119
120 # Whether to override ShortcutEvents for the keybindings defined by this
120 # Whether to override ShortcutEvents for the keybindings defined by this
121 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
121 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
122 # priority (when it has focus) over, e.g., window-level menu shortcuts.
122 # priority (when it has focus) over, e.g., window-level menu shortcuts.
123 override_shortcuts = Bool(False)
123 override_shortcuts = Bool(False)
124
124
125 #------ Signals ------------------------------------------------------------
125 #------ Signals ------------------------------------------------------------
126
126
127 # Signals that indicate ConsoleWidget state.
127 # Signals that indicate ConsoleWidget state.
128 copy_available = QtCore.Signal(bool)
128 copy_available = QtCore.Signal(bool)
129 redo_available = QtCore.Signal(bool)
129 redo_available = QtCore.Signal(bool)
130 undo_available = QtCore.Signal(bool)
130 undo_available = QtCore.Signal(bool)
131
131
132 # Signal emitted when paging is needed and the paging style has been
132 # Signal emitted when paging is needed and the paging style has been
133 # specified as 'custom'.
133 # specified as 'custom'.
134 custom_page_requested = QtCore.Signal(object)
134 custom_page_requested = QtCore.Signal(object)
135
135
136 # Signal emitted when the font is changed.
136 # Signal emitted when the font is changed.
137 font_changed = QtCore.Signal(QtGui.QFont)
137 font_changed = QtCore.Signal(QtGui.QFont)
138
138
139 #------ Protected class variables ------------------------------------------
139 #------ Protected class variables ------------------------------------------
140
140
141 # When the control key is down, these keys are mapped.
141 # When the control key is down, these keys are mapped.
142 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
142 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
143 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
143 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
144 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
144 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
145 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
145 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
146 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
146 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
147 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
147 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
148 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
148 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
149 if not sys.platform == 'darwin':
149 if not sys.platform == 'darwin':
150 # On OS X, Ctrl-E already does the right thing, whereas End moves the
150 # On OS X, Ctrl-E already does the right thing, whereas End moves the
151 # cursor to the bottom of the buffer.
151 # cursor to the bottom of the buffer.
152 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
152 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
153
153
154 # The shortcuts defined by this widget. We need to keep track of these to
154 # The shortcuts defined by this widget. We need to keep track of these to
155 # support 'override_shortcuts' above.
155 # support 'override_shortcuts' above.
156 _shortcuts = set(_ctrl_down_remap.keys() +
156 _shortcuts = set(_ctrl_down_remap.keys() +
157 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
157 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
158 QtCore.Qt.Key_V ])
158 QtCore.Qt.Key_V ])
159
159
160 #---------------------------------------------------------------------------
160 #---------------------------------------------------------------------------
161 # 'QObject' interface
161 # 'QObject' interface
162 #---------------------------------------------------------------------------
162 #---------------------------------------------------------------------------
163
163
164 def __init__(self, parent=None, **kw):
164 def __init__(self, parent=None, **kw):
165 """ Create a ConsoleWidget.
165 """ Create a ConsoleWidget.
166
166
167 Parameters:
167 Parameters:
168 -----------
168 -----------
169 parent : QWidget, optional [default None]
169 parent : QWidget, optional [default None]
170 The parent for this widget.
170 The parent for this widget.
171 """
171 """
172 QtGui.QWidget.__init__(self, parent)
172 QtGui.QWidget.__init__(self, parent)
173 LoggingConfigurable.__init__(self, **kw)
173 LoggingConfigurable.__init__(self, **kw)
174
174
175 # Create the layout and underlying text widget.
175 # Create the layout and underlying text widget.
176 layout = QtGui.QStackedLayout(self)
176 layout = QtGui.QStackedLayout(self)
177 layout.setContentsMargins(0, 0, 0, 0)
177 layout.setContentsMargins(0, 0, 0, 0)
178 self._control = self._create_control()
178 self._control = self._create_control()
179 self._page_control = None
179 self._page_control = None
180 self._splitter = None
180 self._splitter = None
181 if self.paging in ('hsplit', 'vsplit'):
181 if self.paging in ('hsplit', 'vsplit'):
182 self._splitter = QtGui.QSplitter()
182 self._splitter = QtGui.QSplitter()
183 if self.paging == 'hsplit':
183 if self.paging == 'hsplit':
184 self._splitter.setOrientation(QtCore.Qt.Horizontal)
184 self._splitter.setOrientation(QtCore.Qt.Horizontal)
185 else:
185 else:
186 self._splitter.setOrientation(QtCore.Qt.Vertical)
186 self._splitter.setOrientation(QtCore.Qt.Vertical)
187 self._splitter.addWidget(self._control)
187 self._splitter.addWidget(self._control)
188 layout.addWidget(self._splitter)
188 layout.addWidget(self._splitter)
189 else:
189 else:
190 layout.addWidget(self._control)
190 layout.addWidget(self._control)
191
191
192 # Create the paging widget, if necessary.
192 # Create the paging widget, if necessary.
193 if self.paging in ('inside', 'hsplit', 'vsplit'):
193 if self.paging in ('inside', 'hsplit', 'vsplit'):
194 self._page_control = self._create_page_control()
194 self._page_control = self._create_page_control()
195 if self._splitter:
195 if self._splitter:
196 self._page_control.hide()
196 self._page_control.hide()
197 self._splitter.addWidget(self._page_control)
197 self._splitter.addWidget(self._page_control)
198 else:
198 else:
199 layout.addWidget(self._page_control)
199 layout.addWidget(self._page_control)
200
200
201 # Initialize protected variables. Some variables contain useful state
201 # Initialize protected variables. Some variables contain useful state
202 # information for subclasses; they should be considered read-only.
202 # information for subclasses; they should be considered read-only.
203 self._append_before_prompt_pos = 0
203 self._append_before_prompt_pos = 0
204 self._ansi_processor = QtAnsiCodeProcessor()
204 self._ansi_processor = QtAnsiCodeProcessor()
205 self._completion_widget = CompletionWidget(self._control)
205 self._completion_widget = CompletionWidget(self._control)
206 self._continuation_prompt = '> '
206 self._continuation_prompt = '> '
207 self._continuation_prompt_html = None
207 self._continuation_prompt_html = None
208 self._executing = False
208 self._executing = False
209 self._filter_drag = False
209 self._filter_drag = False
210 self._filter_resize = False
210 self._filter_resize = False
211 self._html_exporter = HtmlExporter(self._control)
211 self._html_exporter = HtmlExporter(self._control)
212 self._input_buffer_executing = ''
212 self._input_buffer_executing = ''
213 self._input_buffer_pending = ''
213 self._input_buffer_pending = ''
214 self._kill_ring = QtKillRing(self._control)
214 self._kill_ring = QtKillRing(self._control)
215 self._prompt = ''
215 self._prompt = ''
216 self._prompt_html = None
216 self._prompt_html = None
217 self._prompt_pos = 0
217 self._prompt_pos = 0
218 self._prompt_sep = ''
218 self._prompt_sep = ''
219 self._reading = False
219 self._reading = False
220 self._reading_callback = None
220 self._reading_callback = None
221 self._tab_width = 8
221 self._tab_width = 8
222 self._text_completing_pos = 0
222 self._text_completing_pos = 0
223
223
224 # Set a monospaced font.
224 # Set a monospaced font.
225 self.reset_font()
225 self.reset_font()
226
226
227 # Configure actions.
227 # Configure actions.
228 action = QtGui.QAction('Print', None)
228 action = QtGui.QAction('Print', None)
229 action.setEnabled(True)
229 action.setEnabled(True)
230 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
230 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
231 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
231 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
232 # Only override the default if there is a collision.
232 # Only override the default if there is a collision.
233 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
233 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
234 printkey = "Ctrl+Shift+P"
234 printkey = "Ctrl+Shift+P"
235 action.setShortcut(printkey)
235 action.setShortcut(printkey)
236 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
236 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
237 action.triggered.connect(self.print_)
237 action.triggered.connect(self.print_)
238 self.addAction(action)
238 self.addAction(action)
239 self._print_action = action
239 self._print_action = action
240
240
241 action = QtGui.QAction('Save as HTML/XML', None)
241 action = QtGui.QAction('Save as HTML/XML', None)
242 action.setShortcut(QtGui.QKeySequence.Save)
242 action.setShortcut(QtGui.QKeySequence.Save)
243 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
243 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
244 action.triggered.connect(self.export_html)
244 action.triggered.connect(self.export_html)
245 self.addAction(action)
245 self.addAction(action)
246 self._export_action = action
246 self._export_action = action
247
247
248 action = QtGui.QAction('Select All', None)
248 action = QtGui.QAction('Select All', None)
249 action.setEnabled(True)
249 action.setEnabled(True)
250 action.setShortcut(QtGui.QKeySequence.SelectAll)
250 action.setShortcut(QtGui.QKeySequence.SelectAll)
251 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
251 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
252 action.triggered.connect(self.select_all)
252 action.triggered.connect(self.select_all)
253 self.addAction(action)
253 self.addAction(action)
254 self._select_all_action = action
254 self._select_all_action = action
255
255
256 def eventFilter(self, obj, event):
256 def eventFilter(self, obj, event):
257 """ Reimplemented to ensure a console-like behavior in the underlying
257 """ Reimplemented to ensure a console-like behavior in the underlying
258 text widgets.
258 text widgets.
259 """
259 """
260 etype = event.type()
260 etype = event.type()
261 if etype == QtCore.QEvent.KeyPress:
261 if etype == QtCore.QEvent.KeyPress:
262
262
263 # Re-map keys for all filtered widgets.
263 # Re-map keys for all filtered widgets.
264 key = event.key()
264 key = event.key()
265 if self._control_key_down(event.modifiers()) and \
265 if self._control_key_down(event.modifiers()) and \
266 key in self._ctrl_down_remap:
266 key in self._ctrl_down_remap:
267 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
267 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
268 self._ctrl_down_remap[key],
268 self._ctrl_down_remap[key],
269 QtCore.Qt.NoModifier)
269 QtCore.Qt.NoModifier)
270 QtGui.qApp.sendEvent(obj, new_event)
270 QtGui.qApp.sendEvent(obj, new_event)
271 return True
271 return True
272
272
273 elif obj == self._control:
273 elif obj == self._control:
274 return self._event_filter_console_keypress(event)
274 return self._event_filter_console_keypress(event)
275
275
276 elif obj == self._page_control:
276 elif obj == self._page_control:
277 return self._event_filter_page_keypress(event)
277 return self._event_filter_page_keypress(event)
278
278
279 # Make middle-click paste safe.
279 # Make middle-click paste safe.
280 elif etype == QtCore.QEvent.MouseButtonRelease and \
280 elif etype == QtCore.QEvent.MouseButtonRelease and \
281 event.button() == QtCore.Qt.MidButton and \
281 event.button() == QtCore.Qt.MidButton and \
282 obj == self._control.viewport():
282 obj == self._control.viewport():
283 cursor = self._control.cursorForPosition(event.pos())
283 cursor = self._control.cursorForPosition(event.pos())
284 self._control.setTextCursor(cursor)
284 self._control.setTextCursor(cursor)
285 self.paste(QtGui.QClipboard.Selection)
285 self.paste(QtGui.QClipboard.Selection)
286 return True
286 return True
287
287
288 # Manually adjust the scrollbars *after* a resize event is dispatched.
288 # Manually adjust the scrollbars *after* a resize event is dispatched.
289 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
289 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
290 self._filter_resize = True
290 self._filter_resize = True
291 QtGui.qApp.sendEvent(obj, event)
291 QtGui.qApp.sendEvent(obj, event)
292 self._adjust_scrollbars()
292 self._adjust_scrollbars()
293 self._filter_resize = False
293 self._filter_resize = False
294 return True
294 return True
295
295
296 # Override shortcuts for all filtered widgets.
296 # Override shortcuts for all filtered widgets.
297 elif etype == QtCore.QEvent.ShortcutOverride and \
297 elif etype == QtCore.QEvent.ShortcutOverride and \
298 self.override_shortcuts and \
298 self.override_shortcuts and \
299 self._control_key_down(event.modifiers()) and \
299 self._control_key_down(event.modifiers()) and \
300 event.key() in self._shortcuts:
300 event.key() in self._shortcuts:
301 event.accept()
301 event.accept()
302
302
303 # Ensure that drags are safe. The problem is that the drag starting
303 # Ensure that drags are safe. The problem is that the drag starting
304 # logic, which determines whether the drag is a Copy or Move, is locked
304 # logic, which determines whether the drag is a Copy or Move, is locked
305 # down in QTextControl. If the widget is editable, which it must be if
305 # down in QTextControl. If the widget is editable, which it must be if
306 # we're not executing, the drag will be a Move. The following hack
306 # we're not executing, the drag will be a Move. The following hack
307 # prevents QTextControl from deleting the text by clearing the selection
307 # prevents QTextControl from deleting the text by clearing the selection
308 # when a drag leave event originating from this widget is dispatched.
308 # when a drag leave event originating from this widget is dispatched.
309 # The fact that we have to clear the user's selection is unfortunate,
309 # The fact that we have to clear the user's selection is unfortunate,
310 # but the alternative--trying to prevent Qt from using its hardwired
310 # but the alternative--trying to prevent Qt from using its hardwired
311 # drag logic and writing our own--is worse.
311 # drag logic and writing our own--is worse.
312 elif etype == QtCore.QEvent.DragEnter and \
312 elif etype == QtCore.QEvent.DragEnter and \
313 obj == self._control.viewport() and \
313 obj == self._control.viewport() and \
314 event.source() == self._control.viewport():
314 event.source() == self._control.viewport():
315 self._filter_drag = True
315 self._filter_drag = True
316 elif etype == QtCore.QEvent.DragLeave and \
316 elif etype == QtCore.QEvent.DragLeave and \
317 obj == self._control.viewport() and \
317 obj == self._control.viewport() and \
318 self._filter_drag:
318 self._filter_drag:
319 cursor = self._control.textCursor()
319 cursor = self._control.textCursor()
320 cursor.clearSelection()
320 cursor.clearSelection()
321 self._control.setTextCursor(cursor)
321 self._control.setTextCursor(cursor)
322 self._filter_drag = False
322 self._filter_drag = False
323
323
324 # Ensure that drops are safe.
324 # Ensure that drops are safe.
325 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
325 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
326 cursor = self._control.cursorForPosition(event.pos())
326 cursor = self._control.cursorForPosition(event.pos())
327 if self._in_buffer(cursor.position()):
327 if self._in_buffer(cursor.position()):
328 text = event.mimeData().text()
328 text = event.mimeData().text()
329 self._insert_plain_text_into_buffer(cursor, text)
329 self._insert_plain_text_into_buffer(cursor, text)
330
330
331 # Qt is expecting to get something here--drag and drop occurs in its
331 # Qt is expecting to get something here--drag and drop occurs in its
332 # own event loop. Send a DragLeave event to end it.
332 # own event loop. Send a DragLeave event to end it.
333 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
333 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
334 return True
334 return True
335
335
336 return super(ConsoleWidget, self).eventFilter(obj, event)
336 return super(ConsoleWidget, self).eventFilter(obj, event)
337
337
338 #---------------------------------------------------------------------------
338 #---------------------------------------------------------------------------
339 # 'QWidget' interface
339 # 'QWidget' interface
340 #---------------------------------------------------------------------------
340 #---------------------------------------------------------------------------
341
341
342 def sizeHint(self):
342 def sizeHint(self):
343 """ Reimplemented to suggest a size that is 80 characters wide and
343 """ Reimplemented to suggest a size that is 80 characters wide and
344 25 lines high.
344 25 lines high.
345 """
345 """
346 font_metrics = QtGui.QFontMetrics(self.font)
346 font_metrics = QtGui.QFontMetrics(self.font)
347 margin = (self._control.frameWidth() +
347 margin = (self._control.frameWidth() +
348 self._control.document().documentMargin()) * 2
348 self._control.document().documentMargin()) * 2
349 style = self.style()
349 style = self.style()
350 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
350 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
351
351
352 # Note 1: Despite my best efforts to take the various margins into
352 # Note 1: Despite my best efforts to take the various margins into
353 # account, the width is still coming out a bit too small, so we include
353 # account, the width is still coming out a bit too small, so we include
354 # a fudge factor of one character here.
354 # a fudge factor of one character here.
355 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
355 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
356 # to a Qt bug on certain Mac OS systems where it returns 0.
356 # to a Qt bug on certain Mac OS systems where it returns 0.
357 width = font_metrics.width(' ') * 81 + margin
357 width = font_metrics.width(' ') * 81 + margin
358 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
358 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
359 if self.paging == 'hsplit':
359 if self.paging == 'hsplit':
360 width = width * 2 + splitwidth
360 width = width * 2 + splitwidth
361
361
362 height = font_metrics.height() * 25 + margin
362 height = font_metrics.height() * 25 + margin
363 if self.paging == 'vsplit':
363 if self.paging == 'vsplit':
364 height = height * 2 + splitwidth
364 height = height * 2 + splitwidth
365
365
366 return QtCore.QSize(width, height)
366 return QtCore.QSize(width, height)
367
367
368 #---------------------------------------------------------------------------
368 #---------------------------------------------------------------------------
369 # 'ConsoleWidget' public interface
369 # 'ConsoleWidget' public interface
370 #---------------------------------------------------------------------------
370 #---------------------------------------------------------------------------
371
371
372 def can_copy(self):
372 def can_copy(self):
373 """ Returns whether text can be copied to the clipboard.
373 """ Returns whether text can be copied to the clipboard.
374 """
374 """
375 return self._control.textCursor().hasSelection()
375 return self._control.textCursor().hasSelection()
376
376
377 def can_cut(self):
377 def can_cut(self):
378 """ Returns whether text can be cut to the clipboard.
378 """ Returns whether text can be cut to the clipboard.
379 """
379 """
380 cursor = self._control.textCursor()
380 cursor = self._control.textCursor()
381 return (cursor.hasSelection() and
381 return (cursor.hasSelection() and
382 self._in_buffer(cursor.anchor()) and
382 self._in_buffer(cursor.anchor()) and
383 self._in_buffer(cursor.position()))
383 self._in_buffer(cursor.position()))
384
384
385 def can_paste(self):
385 def can_paste(self):
386 """ Returns whether text can be pasted from the clipboard.
386 """ Returns whether text can be pasted from the clipboard.
387 """
387 """
388 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
388 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
389 return bool(QtGui.QApplication.clipboard().text())
389 return bool(QtGui.QApplication.clipboard().text())
390 return False
390 return False
391
391
392 def clear(self, keep_input=True):
392 def clear(self, keep_input=True):
393 """ Clear the console.
393 """ Clear the console.
394
394
395 Parameters:
395 Parameters:
396 -----------
396 -----------
397 keep_input : bool, optional (default True)
397 keep_input : bool, optional (default True)
398 If set, restores the old input buffer if a new prompt is written.
398 If set, restores the old input buffer if a new prompt is written.
399 """
399 """
400 if self._executing:
400 if self._executing:
401 self._control.clear()
401 self._control.clear()
402 else:
402 else:
403 if keep_input:
403 if keep_input:
404 input_buffer = self.input_buffer
404 input_buffer = self.input_buffer
405 self._control.clear()
405 self._control.clear()
406 self._show_prompt()
406 self._show_prompt()
407 if keep_input:
407 if keep_input:
408 self.input_buffer = input_buffer
408 self.input_buffer = input_buffer
409
409
410 def copy(self):
410 def copy(self):
411 """ Copy the currently selected text to the clipboard.
411 """ Copy the currently selected text to the clipboard.
412 """
412 """
413 self._control.copy()
413 self._control.copy()
414
414
415 def cut(self):
415 def cut(self):
416 """ Copy the currently selected text to the clipboard and delete it
416 """ Copy the currently selected text to the clipboard and delete it
417 if it's inside the input buffer.
417 if it's inside the input buffer.
418 """
418 """
419 self.copy()
419 self.copy()
420 if self.can_cut():
420 if self.can_cut():
421 self._control.textCursor().removeSelectedText()
421 self._control.textCursor().removeSelectedText()
422
422
423 def execute(self, source=None, hidden=False, interactive=False):
423 def execute(self, source=None, hidden=False, interactive=False):
424 """ Executes source or the input buffer, possibly prompting for more
424 """ Executes source or the input buffer, possibly prompting for more
425 input.
425 input.
426
426
427 Parameters:
427 Parameters:
428 -----------
428 -----------
429 source : str, optional
429 source : str, optional
430
430
431 The source to execute. If not specified, the input buffer will be
431 The source to execute. If not specified, the input buffer will be
432 used. If specified and 'hidden' is False, the input buffer will be
432 used. If specified and 'hidden' is False, the input buffer will be
433 replaced with the source before execution.
433 replaced with the source before execution.
434
434
435 hidden : bool, optional (default False)
435 hidden : bool, optional (default False)
436
436
437 If set, no output will be shown and the prompt will not be modified.
437 If set, no output will be shown and the prompt will not be modified.
438 In other words, it will be completely invisible to the user that
438 In other words, it will be completely invisible to the user that
439 an execution has occurred.
439 an execution has occurred.
440
440
441 interactive : bool, optional (default False)
441 interactive : bool, optional (default False)
442
442
443 Whether the console is to treat the source as having been manually
443 Whether the console is to treat the source as having been manually
444 entered by the user. The effect of this parameter depends on the
444 entered by the user. The effect of this parameter depends on the
445 subclass implementation.
445 subclass implementation.
446
446
447 Raises:
447 Raises:
448 -------
448 -------
449 RuntimeError
449 RuntimeError
450 If incomplete input is given and 'hidden' is True. In this case,
450 If incomplete input is given and 'hidden' is True. In this case,
451 it is not possible to prompt for more input.
451 it is not possible to prompt for more input.
452
452
453 Returns:
453 Returns:
454 --------
454 --------
455 A boolean indicating whether the source was executed.
455 A boolean indicating whether the source was executed.
456 """
456 """
457 # WARNING: The order in which things happen here is very particular, in
457 # WARNING: The order in which things happen here is very particular, in
458 # large part because our syntax highlighting is fragile. If you change
458 # large part because our syntax highlighting is fragile. If you change
459 # something, test carefully!
459 # something, test carefully!
460
460
461 # Decide what to execute.
461 # Decide what to execute.
462 if source is None:
462 if source is None:
463 source = self.input_buffer
463 source = self.input_buffer
464 if not hidden:
464 if not hidden:
465 # A newline is appended later, but it should be considered part
465 # A newline is appended later, but it should be considered part
466 # of the input buffer.
466 # of the input buffer.
467 source += '\n'
467 source += '\n'
468 elif not hidden:
468 elif not hidden:
469 self.input_buffer = source
469 self.input_buffer = source
470
470
471 # Execute the source or show a continuation prompt if it is incomplete.
471 # Execute the source or show a continuation prompt if it is incomplete.
472 complete = self._is_complete(source, interactive)
472 complete = self._is_complete(source, interactive)
473 if hidden:
473 if hidden:
474 if complete:
474 if complete:
475 self._execute(source, hidden)
475 self._execute(source, hidden)
476 else:
476 else:
477 error = 'Incomplete noninteractive input: "%s"'
477 error = 'Incomplete noninteractive input: "%s"'
478 raise RuntimeError(error % source)
478 raise RuntimeError(error % source)
479 else:
479 else:
480 if complete:
480 if complete:
481 self._append_plain_text('\n')
481 self._append_plain_text('\n')
482 self._input_buffer_executing = self.input_buffer
482 self._input_buffer_executing = self.input_buffer
483 self._executing = True
483 self._executing = True
484 self._prompt_finished()
484 self._prompt_finished()
485
485
486 # The maximum block count is only in effect during execution.
486 # The maximum block count is only in effect during execution.
487 # This ensures that _prompt_pos does not become invalid due to
487 # This ensures that _prompt_pos does not become invalid due to
488 # text truncation.
488 # text truncation.
489 self._control.document().setMaximumBlockCount(self.buffer_size)
489 self._control.document().setMaximumBlockCount(self.buffer_size)
490
490
491 # Setting a positive maximum block count will automatically
491 # Setting a positive maximum block count will automatically
492 # disable the undo/redo history, but just to be safe:
492 # disable the undo/redo history, but just to be safe:
493 self._control.setUndoRedoEnabled(False)
493 self._control.setUndoRedoEnabled(False)
494
494
495 # Perform actual execution.
495 # Perform actual execution.
496 self._execute(source, hidden)
496 self._execute(source, hidden)
497
497
498 else:
498 else:
499 # Do this inside an edit block so continuation prompts are
499 # Do this inside an edit block so continuation prompts are
500 # removed seamlessly via undo/redo.
500 # removed seamlessly via undo/redo.
501 cursor = self._get_end_cursor()
501 cursor = self._get_end_cursor()
502 cursor.beginEditBlock()
502 cursor.beginEditBlock()
503 cursor.insertText('\n')
503 cursor.insertText('\n')
504 self._insert_continuation_prompt(cursor)
504 self._insert_continuation_prompt(cursor)
505 cursor.endEditBlock()
505 cursor.endEditBlock()
506
506
507 # Do not do this inside the edit block. It works as expected
507 # Do not do this inside the edit block. It works as expected
508 # when using a QPlainTextEdit control, but does not have an
508 # when using a QPlainTextEdit control, but does not have an
509 # effect when using a QTextEdit. I believe this is a Qt bug.
509 # effect when using a QTextEdit. I believe this is a Qt bug.
510 self._control.moveCursor(QtGui.QTextCursor.End)
510 self._control.moveCursor(QtGui.QTextCursor.End)
511
511
512 return complete
512 return complete
513
513
514 def export_html(self):
514 def export_html(self):
515 """ Shows a dialog to export HTML/XML in various formats.
515 """ Shows a dialog to export HTML/XML in various formats.
516 """
516 """
517 self._html_exporter.export()
517 self._html_exporter.export()
518
518
519 def _get_input_buffer(self, force=False):
519 def _get_input_buffer(self, force=False):
520 """ The text that the user has entered entered at the current prompt.
520 """ The text that the user has entered entered at the current prompt.
521
521
522 If the console is currently executing, the text that is executing will
522 If the console is currently executing, the text that is executing will
523 always be returned.
523 always be returned.
524 """
524 """
525 # If we're executing, the input buffer may not even exist anymore due to
525 # If we're executing, the input buffer may not even exist anymore due to
526 # the limit imposed by 'buffer_size'. Therefore, we store it.
526 # the limit imposed by 'buffer_size'. Therefore, we store it.
527 if self._executing and not force:
527 if self._executing and not force:
528 return self._input_buffer_executing
528 return self._input_buffer_executing
529
529
530 cursor = self._get_end_cursor()
530 cursor = self._get_end_cursor()
531 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
531 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
532 input_buffer = cursor.selection().toPlainText()
532 input_buffer = cursor.selection().toPlainText()
533
533
534 # Strip out continuation prompts.
534 # Strip out continuation prompts.
535 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
535 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
536
536
537 def _set_input_buffer(self, string):
537 def _set_input_buffer(self, string):
538 """ Sets the text in the input buffer.
538 """ Sets the text in the input buffer.
539
539
540 If the console is currently executing, this call has no *immediate*
540 If the console is currently executing, this call has no *immediate*
541 effect. When the execution is finished, the input buffer will be updated
541 effect. When the execution is finished, the input buffer will be updated
542 appropriately.
542 appropriately.
543 """
543 """
544 # If we're executing, store the text for later.
544 # If we're executing, store the text for later.
545 if self._executing:
545 if self._executing:
546 self._input_buffer_pending = string
546 self._input_buffer_pending = string
547 return
547 return
548
548
549 # Remove old text.
549 # Remove old text.
550 cursor = self._get_end_cursor()
550 cursor = self._get_end_cursor()
551 cursor.beginEditBlock()
551 cursor.beginEditBlock()
552 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
552 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
553 cursor.removeSelectedText()
553 cursor.removeSelectedText()
554
554
555 # Insert new text with continuation prompts.
555 # Insert new text with continuation prompts.
556 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
556 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
557 cursor.endEditBlock()
557 cursor.endEditBlock()
558 self._control.moveCursor(QtGui.QTextCursor.End)
558 self._control.moveCursor(QtGui.QTextCursor.End)
559
559
560 input_buffer = property(_get_input_buffer, _set_input_buffer)
560 input_buffer = property(_get_input_buffer, _set_input_buffer)
561
561
562 def _get_font(self):
562 def _get_font(self):
563 """ The base font being used by the ConsoleWidget.
563 """ The base font being used by the ConsoleWidget.
564 """
564 """
565 return self._control.document().defaultFont()
565 return self._control.document().defaultFont()
566
566
567 def _set_font(self, font):
567 def _set_font(self, font):
568 """ Sets the base font for the ConsoleWidget to the specified QFont.
568 """ Sets the base font for the ConsoleWidget to the specified QFont.
569 """
569 """
570 font_metrics = QtGui.QFontMetrics(font)
570 font_metrics = QtGui.QFontMetrics(font)
571 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
571 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
572
572
573 self._completion_widget.setFont(font)
573 self._completion_widget.setFont(font)
574 self._control.document().setDefaultFont(font)
574 self._control.document().setDefaultFont(font)
575 if self._page_control:
575 if self._page_control:
576 self._page_control.document().setDefaultFont(font)
576 self._page_control.document().setDefaultFont(font)
577
577
578 self.font_changed.emit(font)
578 self.font_changed.emit(font)
579
579
580 font = property(_get_font, _set_font)
580 font = property(_get_font, _set_font)
581
581
582 def paste(self, mode=QtGui.QClipboard.Clipboard):
582 def paste(self, mode=QtGui.QClipboard.Clipboard):
583 """ Paste the contents of the clipboard into the input region.
583 """ Paste the contents of the clipboard into the input region.
584
584
585 Parameters:
585 Parameters:
586 -----------
586 -----------
587 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
587 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
588
588
589 Controls which part of the system clipboard is used. This can be
589 Controls which part of the system clipboard is used. This can be
590 used to access the selection clipboard in X11 and the Find buffer
590 used to access the selection clipboard in X11 and the Find buffer
591 in Mac OS. By default, the regular clipboard is used.
591 in Mac OS. By default, the regular clipboard is used.
592 """
592 """
593 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
593 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
594 # Make sure the paste is safe.
594 # Make sure the paste is safe.
595 self._keep_cursor_in_buffer()
595 self._keep_cursor_in_buffer()
596 cursor = self._control.textCursor()
596 cursor = self._control.textCursor()
597
597
598 # Remove any trailing newline, which confuses the GUI and forces the
598 # Remove any trailing newline, which confuses the GUI and forces the
599 # user to backspace.
599 # user to backspace.
600 text = QtGui.QApplication.clipboard().text(mode).rstrip()
600 text = QtGui.QApplication.clipboard().text(mode).rstrip()
601 self._insert_plain_text_into_buffer(cursor, dedent(text))
601 self._insert_plain_text_into_buffer(cursor, dedent(text))
602
602
603 def print_(self, printer = None):
603 def print_(self, printer = None):
604 """ Print the contents of the ConsoleWidget to the specified QPrinter.
604 """ Print the contents of the ConsoleWidget to the specified QPrinter.
605 """
605 """
606 if (not printer):
606 if (not printer):
607 printer = QtGui.QPrinter()
607 printer = QtGui.QPrinter()
608 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
608 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
609 return
609 return
610 self._control.print_(printer)
610 self._control.print_(printer)
611
611
612 def prompt_to_top(self):
612 def prompt_to_top(self):
613 """ Moves the prompt to the top of the viewport.
613 """ Moves the prompt to the top of the viewport.
614 """
614 """
615 if not self._executing:
615 if not self._executing:
616 prompt_cursor = self._get_prompt_cursor()
616 prompt_cursor = self._get_prompt_cursor()
617 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
617 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
618 self._set_cursor(prompt_cursor)
618 self._set_cursor(prompt_cursor)
619 self._set_top_cursor(prompt_cursor)
619 self._set_top_cursor(prompt_cursor)
620
620
621 def redo(self):
621 def redo(self):
622 """ Redo the last operation. If there is no operation to redo, nothing
622 """ Redo the last operation. If there is no operation to redo, nothing
623 happens.
623 happens.
624 """
624 """
625 self._control.redo()
625 self._control.redo()
626
626
627 def reset_font(self):
627 def reset_font(self):
628 """ Sets the font to the default fixed-width font for this platform.
628 """ Sets the font to the default fixed-width font for this platform.
629 """
629 """
630 if sys.platform == 'win32':
630 if sys.platform == 'win32':
631 # Consolas ships with Vista/Win7, fallback to Courier if needed
631 # Consolas ships with Vista/Win7, fallback to Courier if needed
632 fallback = 'Courier'
632 fallback = 'Courier'
633 elif sys.platform == 'darwin':
633 elif sys.platform == 'darwin':
634 # OSX always has Monaco
634 # OSX always has Monaco
635 fallback = 'Monaco'
635 fallback = 'Monaco'
636 else:
636 else:
637 # Monospace should always exist
637 # Monospace should always exist
638 fallback = 'Monospace'
638 fallback = 'Monospace'
639 font = get_font(self.font_family, fallback)
639 font = get_font(self.font_family, fallback)
640 if self.font_size:
640 if self.font_size:
641 font.setPointSize(self.font_size)
641 font.setPointSize(self.font_size)
642 else:
642 else:
643 font.setPointSize(QtGui.qApp.font().pointSize())
643 font.setPointSize(QtGui.qApp.font().pointSize())
644 font.setStyleHint(QtGui.QFont.TypeWriter)
644 font.setStyleHint(QtGui.QFont.TypeWriter)
645 self._set_font(font)
645 self._set_font(font)
646
646
647 def change_font_size(self, delta):
647 def change_font_size(self, delta):
648 """Change the font size by the specified amount (in points).
648 """Change the font size by the specified amount (in points).
649 """
649 """
650 font = self.font
650 font = self.font
651 size = max(font.pointSize() + delta, 1) # minimum 1 point
651 size = max(font.pointSize() + delta, 1) # minimum 1 point
652 font.setPointSize(size)
652 font.setPointSize(size)
653 self._set_font(font)
653 self._set_font(font)
654
654
655 def select_all(self):
655 def select_all(self):
656 """ Selects all the text in the buffer.
656 """ Selects all the text in the buffer.
657 """
657 """
658 self._control.selectAll()
658 self._control.selectAll()
659
659
660 def _get_tab_width(self):
660 def _get_tab_width(self):
661 """ The width (in terms of space characters) for tab characters.
661 """ The width (in terms of space characters) for tab characters.
662 """
662 """
663 return self._tab_width
663 return self._tab_width
664
664
665 def _set_tab_width(self, tab_width):
665 def _set_tab_width(self, tab_width):
666 """ Sets the width (in terms of space characters) for tab characters.
666 """ Sets the width (in terms of space characters) for tab characters.
667 """
667 """
668 font_metrics = QtGui.QFontMetrics(self.font)
668 font_metrics = QtGui.QFontMetrics(self.font)
669 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
669 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
670
670
671 self._tab_width = tab_width
671 self._tab_width = tab_width
672
672
673 tab_width = property(_get_tab_width, _set_tab_width)
673 tab_width = property(_get_tab_width, _set_tab_width)
674
674
675 def undo(self):
675 def undo(self):
676 """ Undo the last operation. If there is no operation to undo, nothing
676 """ Undo the last operation. If there is no operation to undo, nothing
677 happens.
677 happens.
678 """
678 """
679 self._control.undo()
679 self._control.undo()
680
680
681 #---------------------------------------------------------------------------
681 #---------------------------------------------------------------------------
682 # 'ConsoleWidget' abstract interface
682 # 'ConsoleWidget' abstract interface
683 #---------------------------------------------------------------------------
683 #---------------------------------------------------------------------------
684
684
685 def _is_complete(self, source, interactive):
685 def _is_complete(self, source, interactive):
686 """ Returns whether 'source' can be executed. When triggered by an
686 """ Returns whether 'source' can be executed. When triggered by an
687 Enter/Return key press, 'interactive' is True; otherwise, it is
687 Enter/Return key press, 'interactive' is True; otherwise, it is
688 False.
688 False.
689 """
689 """
690 raise NotImplementedError
690 raise NotImplementedError
691
691
692 def _execute(self, source, hidden):
692 def _execute(self, source, hidden):
693 """ Execute 'source'. If 'hidden', do not show any output.
693 """ Execute 'source'. If 'hidden', do not show any output.
694 """
694 """
695 raise NotImplementedError
695 raise NotImplementedError
696
696
697 def _prompt_started_hook(self):
697 def _prompt_started_hook(self):
698 """ Called immediately after a new prompt is displayed.
698 """ Called immediately after a new prompt is displayed.
699 """
699 """
700 pass
700 pass
701
701
702 def _prompt_finished_hook(self):
702 def _prompt_finished_hook(self):
703 """ Called immediately after a prompt is finished, i.e. when some input
703 """ Called immediately after a prompt is finished, i.e. when some input
704 will be processed and a new prompt displayed.
704 will be processed and a new prompt displayed.
705 """
705 """
706 pass
706 pass
707
707
708 def _up_pressed(self, shift_modifier):
708 def _up_pressed(self, shift_modifier):
709 """ Called when the up key is pressed. Returns whether to continue
709 """ Called when the up key is pressed. Returns whether to continue
710 processing the event.
710 processing the event.
711 """
711 """
712 return True
712 return True
713
713
714 def _down_pressed(self, shift_modifier):
714 def _down_pressed(self, shift_modifier):
715 """ Called when the down key is pressed. Returns whether to continue
715 """ Called when the down key is pressed. Returns whether to continue
716 processing the event.
716 processing the event.
717 """
717 """
718 return True
718 return True
719
719
720 def _tab_pressed(self):
720 def _tab_pressed(self):
721 """ Called when the tab key is pressed. Returns whether to continue
721 """ Called when the tab key is pressed. Returns whether to continue
722 processing the event.
722 processing the event.
723 """
723 """
724 return False
724 return False
725
725
726 #--------------------------------------------------------------------------
726 #--------------------------------------------------------------------------
727 # 'ConsoleWidget' protected interface
727 # 'ConsoleWidget' protected interface
728 #--------------------------------------------------------------------------
728 #--------------------------------------------------------------------------
729
729
730 def _append_custom(self, insert, input, before_prompt=False):
730 def _append_custom(self, insert, input, before_prompt=False):
731 """ A low-level method for appending content to the end of the buffer.
731 """ A low-level method for appending content to the end of the buffer.
732
732
733 If 'before_prompt' is enabled, the content will be inserted before the
733 If 'before_prompt' is enabled, the content will be inserted before the
734 current prompt, if there is one.
734 current prompt, if there is one.
735 """
735 """
736 # Determine where to insert the content.
736 # Determine where to insert the content.
737 cursor = self._control.textCursor()
737 cursor = self._control.textCursor()
738 if before_prompt and not self._executing:
738 if before_prompt and not self._executing:
739 cursor.setPosition(self._append_before_prompt_pos)
739 cursor.setPosition(self._append_before_prompt_pos)
740 else:
740 else:
741 cursor.movePosition(QtGui.QTextCursor.End)
741 cursor.movePosition(QtGui.QTextCursor.End)
742 start_pos = cursor.position()
742 start_pos = cursor.position()
743
743
744 # Perform the insertion.
744 # Perform the insertion.
745 result = insert(cursor, input)
745 result = insert(cursor, input)
746
746
747 # Adjust the prompt position if we have inserted before it. This is safe
747 # Adjust the prompt position if we have inserted before it. This is safe
748 # because buffer truncation is disabled when not executing.
748 # because buffer truncation is disabled when not executing.
749 if before_prompt and not self._executing:
749 if before_prompt and not self._executing:
750 diff = cursor.position() - start_pos
750 diff = cursor.position() - start_pos
751 self._append_before_prompt_pos += diff
751 self._append_before_prompt_pos += diff
752 self._prompt_pos += diff
752 self._prompt_pos += diff
753
753
754 return result
754 return result
755
755
756 def _append_html(self, html, before_prompt=False):
756 def _append_html(self, html, before_prompt=False):
757 """ Appends HTML at the end of the console buffer.
757 """ Appends HTML at the end of the console buffer.
758 """
758 """
759 self._append_custom(self._insert_html, html, before_prompt)
759 self._append_custom(self._insert_html, html, before_prompt)
760
760
761 def _append_html_fetching_plain_text(self, html, before_prompt=False):
761 def _append_html_fetching_plain_text(self, html, before_prompt=False):
762 """ Appends HTML, then returns the plain text version of it.
762 """ Appends HTML, then returns the plain text version of it.
763 """
763 """
764 return self._append_custom(self._insert_html_fetching_plain_text,
764 return self._append_custom(self._insert_html_fetching_plain_text,
765 html, before_prompt)
765 html, before_prompt)
766
766
767 def _append_plain_text(self, text, before_prompt=False):
767 def _append_plain_text(self, text, before_prompt=False):
768 """ Appends plain text, processing ANSI codes if enabled.
768 """ Appends plain text, processing ANSI codes if enabled.
769 """
769 """
770 self._append_custom(self._insert_plain_text, text, before_prompt)
770 self._append_custom(self._insert_plain_text, text, before_prompt)
771
771
772 def _cancel_text_completion(self):
772 def _cancel_text_completion(self):
773 """ If text completion is progress, cancel it.
773 """ If text completion is progress, cancel it.
774 """
774 """
775 if self._text_completing_pos:
775 if self._text_completing_pos:
776 self._clear_temporary_buffer()
776 self._clear_temporary_buffer()
777 self._text_completing_pos = 0
777 self._text_completing_pos = 0
778
778
779 def _clear_temporary_buffer(self):
779 def _clear_temporary_buffer(self):
780 """ Clears the "temporary text" buffer, i.e. all the text following
780 """ Clears the "temporary text" buffer, i.e. all the text following
781 the prompt region.
781 the prompt region.
782 """
782 """
783 # Select and remove all text below the input buffer.
783 # Select and remove all text below the input buffer.
784 cursor = self._get_prompt_cursor()
784 cursor = self._get_prompt_cursor()
785 prompt = self._continuation_prompt.lstrip()
785 prompt = self._continuation_prompt.lstrip()
786 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
786 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
787 temp_cursor = QtGui.QTextCursor(cursor)
787 temp_cursor = QtGui.QTextCursor(cursor)
788 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
788 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
789 text = temp_cursor.selection().toPlainText().lstrip()
789 text = temp_cursor.selection().toPlainText().lstrip()
790 if not text.startswith(prompt):
790 if not text.startswith(prompt):
791 break
791 break
792 else:
792 else:
793 # We've reached the end of the input buffer and no text follows.
793 # We've reached the end of the input buffer and no text follows.
794 return
794 return
795 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
795 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
796 cursor.movePosition(QtGui.QTextCursor.End,
796 cursor.movePosition(QtGui.QTextCursor.End,
797 QtGui.QTextCursor.KeepAnchor)
797 QtGui.QTextCursor.KeepAnchor)
798 cursor.removeSelectedText()
798 cursor.removeSelectedText()
799
799
800 # After doing this, we have no choice but to clear the undo/redo
800 # After doing this, we have no choice but to clear the undo/redo
801 # history. Otherwise, the text is not "temporary" at all, because it
801 # history. Otherwise, the text is not "temporary" at all, because it
802 # can be recalled with undo/redo. Unfortunately, Qt does not expose
802 # can be recalled with undo/redo. Unfortunately, Qt does not expose
803 # fine-grained control to the undo/redo system.
803 # fine-grained control to the undo/redo system.
804 if self._control.isUndoRedoEnabled():
804 if self._control.isUndoRedoEnabled():
805 self._control.setUndoRedoEnabled(False)
805 self._control.setUndoRedoEnabled(False)
806 self._control.setUndoRedoEnabled(True)
806 self._control.setUndoRedoEnabled(True)
807
807
808 def _complete_with_items(self, cursor, items):
808 def _complete_with_items(self, cursor, items):
809 """ Performs completion with 'items' at the specified cursor location.
809 """ Performs completion with 'items' at the specified cursor location.
810 """
810 """
811 self._cancel_text_completion()
811 self._cancel_text_completion()
812
812
813 if len(items) == 1:
813 if len(items) == 1:
814 cursor.setPosition(self._control.textCursor().position(),
814 cursor.setPosition(self._control.textCursor().position(),
815 QtGui.QTextCursor.KeepAnchor)
815 QtGui.QTextCursor.KeepAnchor)
816 cursor.insertText(items[0])
816 cursor.insertText(items[0])
817
817
818 elif len(items) > 1:
818 elif len(items) > 1:
819 current_pos = self._control.textCursor().position()
819 current_pos = self._control.textCursor().position()
820 prefix = commonprefix(items)
820 prefix = commonprefix(items)
821 if prefix:
821 if prefix:
822 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
822 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
823 cursor.insertText(prefix)
823 cursor.insertText(prefix)
824 current_pos = cursor.position()
824 current_pos = cursor.position()
825
825
826 if self.gui_completion:
826 if self.gui_completion:
827 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
827 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
828 self._completion_widget.show_items(cursor, items)
828 self._completion_widget.show_items(cursor, items)
829 else:
829 else:
830 cursor.beginEditBlock()
830 cursor.beginEditBlock()
831 self._append_plain_text('\n')
831 self._append_plain_text('\n')
832 self._page(self._format_as_columns(items))
832 self._page(self._format_as_columns(items))
833 cursor.endEditBlock()
833 cursor.endEditBlock()
834
834
835 cursor.setPosition(current_pos)
835 cursor.setPosition(current_pos)
836 self._control.moveCursor(QtGui.QTextCursor.End)
836 self._control.moveCursor(QtGui.QTextCursor.End)
837 self._control.setTextCursor(cursor)
837 self._control.setTextCursor(cursor)
838 self._text_completing_pos = current_pos
838 self._text_completing_pos = current_pos
839
839
840 def _context_menu_make(self, pos):
840 def _context_menu_make(self, pos):
841 """ Creates a context menu for the given QPoint (in widget coordinates).
841 """ Creates a context menu for the given QPoint (in widget coordinates).
842 """
842 """
843 menu = QtGui.QMenu(self)
843 menu = QtGui.QMenu(self)
844
844
845 cut_action = menu.addAction('Cut', self.cut)
845 cut_action = menu.addAction('Cut', self.cut)
846 cut_action.setEnabled(self.can_cut())
846 cut_action.setEnabled(self.can_cut())
847 cut_action.setShortcut(QtGui.QKeySequence.Cut)
847 cut_action.setShortcut(QtGui.QKeySequence.Cut)
848
848
849 copy_action = menu.addAction('Copy', self.copy)
849 copy_action = menu.addAction('Copy', self.copy)
850 copy_action.setEnabled(self.can_copy())
850 copy_action.setEnabled(self.can_copy())
851 copy_action.setShortcut(QtGui.QKeySequence.Copy)
851 copy_action.setShortcut(QtGui.QKeySequence.Copy)
852
852
853 paste_action = menu.addAction('Paste', self.paste)
853 paste_action = menu.addAction('Paste', self.paste)
854 paste_action.setEnabled(self.can_paste())
854 paste_action.setEnabled(self.can_paste())
855 paste_action.setShortcut(QtGui.QKeySequence.Paste)
855 paste_action.setShortcut(QtGui.QKeySequence.Paste)
856
856
857 menu.addSeparator()
857 menu.addSeparator()
858 menu.addAction(self._select_all_action)
858 menu.addAction(self._select_all_action)
859
859
860 menu.addSeparator()
860 menu.addSeparator()
861 menu.addAction(self._export_action)
861 menu.addAction(self._export_action)
862 menu.addAction(self._print_action)
862 menu.addAction(self._print_action)
863
863
864 return menu
864 return menu
865
865
866 def _control_key_down(self, modifiers, include_command=False):
866 def _control_key_down(self, modifiers, include_command=False):
867 """ Given a KeyboardModifiers flags object, return whether the Control
867 """ Given a KeyboardModifiers flags object, return whether the Control
868 key is down.
868 key is down.
869
869
870 Parameters:
870 Parameters:
871 -----------
871 -----------
872 include_command : bool, optional (default True)
872 include_command : bool, optional (default True)
873 Whether to treat the Command key as a (mutually exclusive) synonym
873 Whether to treat the Command key as a (mutually exclusive) synonym
874 for Control when in Mac OS.
874 for Control when in Mac OS.
875 """
875 """
876 # Note that on Mac OS, ControlModifier corresponds to the Command key
876 # Note that on Mac OS, ControlModifier corresponds to the Command key
877 # while MetaModifier corresponds to the Control key.
877 # while MetaModifier corresponds to the Control key.
878 if sys.platform == 'darwin':
878 if sys.platform == 'darwin':
879 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
879 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
880 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
880 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
881 else:
881 else:
882 return bool(modifiers & QtCore.Qt.ControlModifier)
882 return bool(modifiers & QtCore.Qt.ControlModifier)
883
883
884 def _create_control(self):
884 def _create_control(self):
885 """ Creates and connects the underlying text widget.
885 """ Creates and connects the underlying text widget.
886 """
886 """
887 # Create the underlying control.
887 # Create the underlying control.
888 if self.kind == 'plain':
888 if self.kind == 'plain':
889 control = QtGui.QPlainTextEdit()
889 control = QtGui.QPlainTextEdit()
890 elif self.kind == 'rich':
890 elif self.kind == 'rich':
891 control = QtGui.QTextEdit()
891 control = QtGui.QTextEdit()
892 control.setAcceptRichText(False)
892 control.setAcceptRichText(False)
893
893
894 # Install event filters. The filter on the viewport is needed for
894 # Install event filters. The filter on the viewport is needed for
895 # mouse events and drag events.
895 # mouse events and drag events.
896 control.installEventFilter(self)
896 control.installEventFilter(self)
897 control.viewport().installEventFilter(self)
897 control.viewport().installEventFilter(self)
898
898
899 # Connect signals.
899 # Connect signals.
900 control.cursorPositionChanged.connect(self._cursor_position_changed)
900 control.cursorPositionChanged.connect(self._cursor_position_changed)
901 control.customContextMenuRequested.connect(
901 control.customContextMenuRequested.connect(
902 self._custom_context_menu_requested)
902 self._custom_context_menu_requested)
903 control.copyAvailable.connect(self.copy_available)
903 control.copyAvailable.connect(self.copy_available)
904 control.redoAvailable.connect(self.redo_available)
904 control.redoAvailable.connect(self.redo_available)
905 control.undoAvailable.connect(self.undo_available)
905 control.undoAvailable.connect(self.undo_available)
906
906
907 # Hijack the document size change signal to prevent Qt from adjusting
907 # Hijack the document size change signal to prevent Qt from adjusting
908 # the viewport's scrollbar. We are relying on an implementation detail
908 # the viewport's scrollbar. We are relying on an implementation detail
909 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
909 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
910 # this functionality we cannot create a nice terminal interface.
910 # this functionality we cannot create a nice terminal interface.
911 layout = control.document().documentLayout()
911 layout = control.document().documentLayout()
912 layout.documentSizeChanged.disconnect()
912 layout.documentSizeChanged.disconnect()
913 layout.documentSizeChanged.connect(self._adjust_scrollbars)
913 layout.documentSizeChanged.connect(self._adjust_scrollbars)
914
914
915 # Configure the control.
915 # Configure the control.
916 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
916 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
917 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
917 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
918 control.setReadOnly(True)
918 control.setReadOnly(True)
919 control.setUndoRedoEnabled(False)
919 control.setUndoRedoEnabled(False)
920 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
920 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
921 return control
921 return control
922
922
923 def _create_page_control(self):
923 def _create_page_control(self):
924 """ Creates and connects the underlying paging widget.
924 """ Creates and connects the underlying paging widget.
925 """
925 """
926 if self.kind == 'plain':
926 if self.kind == 'plain':
927 control = QtGui.QPlainTextEdit()
927 control = QtGui.QPlainTextEdit()
928 elif self.kind == 'rich':
928 elif self.kind == 'rich':
929 control = QtGui.QTextEdit()
929 control = QtGui.QTextEdit()
930 control.installEventFilter(self)
930 control.installEventFilter(self)
931 control.setReadOnly(True)
931 control.setReadOnly(True)
932 control.setUndoRedoEnabled(False)
932 control.setUndoRedoEnabled(False)
933 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
933 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
934 return control
934 return control
935
935
936 def _event_filter_console_keypress(self, event):
936 def _event_filter_console_keypress(self, event):
937 """ Filter key events for the underlying text widget to create a
937 """ Filter key events for the underlying text widget to create a
938 console-like interface.
938 console-like interface.
939 """
939 """
940 intercepted = False
940 intercepted = False
941 cursor = self._control.textCursor()
941 cursor = self._control.textCursor()
942 position = cursor.position()
942 position = cursor.position()
943 key = event.key()
943 key = event.key()
944 ctrl_down = self._control_key_down(event.modifiers())
944 ctrl_down = self._control_key_down(event.modifiers())
945 alt_down = event.modifiers() & QtCore.Qt.AltModifier
945 alt_down = event.modifiers() & QtCore.Qt.AltModifier
946 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
946 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
947
947
948 #------ Special sequences ----------------------------------------------
948 #------ Special sequences ----------------------------------------------
949
949
950 if event.matches(QtGui.QKeySequence.Copy):
950 if event.matches(QtGui.QKeySequence.Copy):
951 self.copy()
951 self.copy()
952 intercepted = True
952 intercepted = True
953
953
954 elif event.matches(QtGui.QKeySequence.Cut):
954 elif event.matches(QtGui.QKeySequence.Cut):
955 self.cut()
955 self.cut()
956 intercepted = True
956 intercepted = True
957
957
958 elif event.matches(QtGui.QKeySequence.Paste):
958 elif event.matches(QtGui.QKeySequence.Paste):
959 self.paste()
959 self.paste()
960 intercepted = True
960 intercepted = True
961
961
962 #------ Special modifier logic -----------------------------------------
962 #------ Special modifier logic -----------------------------------------
963
963
964 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
964 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
965 intercepted = True
965 intercepted = True
966
966
967 # Special handling when tab completing in text mode.
967 # Special handling when tab completing in text mode.
968 self._cancel_text_completion()
968 self._cancel_text_completion()
969
969
970 if self._in_buffer(position):
970 if self._in_buffer(position):
971 # Special handling when a reading a line of raw input.
971 # Special handling when a reading a line of raw input.
972 if self._reading:
972 if self._reading:
973 self._append_plain_text('\n')
973 self._append_plain_text('\n')
974 self._reading = False
974 self._reading = False
975 if self._reading_callback:
975 if self._reading_callback:
976 self._reading_callback()
976 self._reading_callback()
977
977
978 # If the input buffer is a single line or there is only
978 # If the input buffer is a single line or there is only
979 # whitespace after the cursor, execute. Otherwise, split the
979 # whitespace after the cursor, execute. Otherwise, split the
980 # line with a continuation prompt.
980 # line with a continuation prompt.
981 elif not self._executing:
981 elif not self._executing:
982 cursor.movePosition(QtGui.QTextCursor.End,
982 cursor.movePosition(QtGui.QTextCursor.End,
983 QtGui.QTextCursor.KeepAnchor)
983 QtGui.QTextCursor.KeepAnchor)
984 at_end = len(cursor.selectedText().strip()) == 0
984 at_end = len(cursor.selectedText().strip()) == 0
985 single_line = (self._get_end_cursor().blockNumber() ==
985 single_line = (self._get_end_cursor().blockNumber() ==
986 self._get_prompt_cursor().blockNumber())
986 self._get_prompt_cursor().blockNumber())
987 if (at_end or shift_down or single_line) and not ctrl_down:
987 if (at_end or shift_down or single_line) and not ctrl_down:
988 self.execute(interactive = not shift_down)
988 self.execute(interactive = not shift_down)
989 else:
989 else:
990 # Do this inside an edit block for clean undo/redo.
990 # Do this inside an edit block for clean undo/redo.
991 cursor.beginEditBlock()
991 cursor.beginEditBlock()
992 cursor.setPosition(position)
992 cursor.setPosition(position)
993 cursor.insertText('\n')
993 cursor.insertText('\n')
994 self._insert_continuation_prompt(cursor)
994 self._insert_continuation_prompt(cursor)
995 cursor.endEditBlock()
995 cursor.endEditBlock()
996
996
997 # Ensure that the whole input buffer is visible.
997 # Ensure that the whole input buffer is visible.
998 # FIXME: This will not be usable if the input buffer is
998 # FIXME: This will not be usable if the input buffer is
999 # taller than the console widget.
999 # taller than the console widget.
1000 self._control.moveCursor(QtGui.QTextCursor.End)
1000 self._control.moveCursor(QtGui.QTextCursor.End)
1001 self._control.setTextCursor(cursor)
1001 self._control.setTextCursor(cursor)
1002
1002
1003 #------ Control/Cmd modifier -------------------------------------------
1003 #------ Control/Cmd modifier -------------------------------------------
1004
1004
1005 elif ctrl_down:
1005 elif ctrl_down:
1006 if key == QtCore.Qt.Key_G:
1006 if key == QtCore.Qt.Key_G:
1007 self._keyboard_quit()
1007 self._keyboard_quit()
1008 intercepted = True
1008 intercepted = True
1009
1009
1010 elif key == QtCore.Qt.Key_K:
1010 elif key == QtCore.Qt.Key_K:
1011 if self._in_buffer(position):
1011 if self._in_buffer(position):
1012 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1012 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1013 QtGui.QTextCursor.KeepAnchor)
1013 QtGui.QTextCursor.KeepAnchor)
1014 if not cursor.hasSelection():
1014 if not cursor.hasSelection():
1015 # Line deletion (remove continuation prompt)
1015 # Line deletion (remove continuation prompt)
1016 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1016 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1017 QtGui.QTextCursor.KeepAnchor)
1017 QtGui.QTextCursor.KeepAnchor)
1018 cursor.movePosition(QtGui.QTextCursor.Right,
1018 cursor.movePosition(QtGui.QTextCursor.Right,
1019 QtGui.QTextCursor.KeepAnchor,
1019 QtGui.QTextCursor.KeepAnchor,
1020 len(self._continuation_prompt))
1020 len(self._continuation_prompt))
1021 self._kill_ring.kill_cursor(cursor)
1021 self._kill_ring.kill_cursor(cursor)
1022 intercepted = True
1022 intercepted = True
1023
1023
1024 elif key == QtCore.Qt.Key_L:
1024 elif key == QtCore.Qt.Key_L:
1025 self.prompt_to_top()
1025 self.prompt_to_top()
1026 intercepted = True
1026 intercepted = True
1027
1027
1028 elif key == QtCore.Qt.Key_O:
1028 elif key == QtCore.Qt.Key_O:
1029 if self._page_control and self._page_control.isVisible():
1029 if self._page_control and self._page_control.isVisible():
1030 self._page_control.setFocus()
1030 self._page_control.setFocus()
1031 intercepted = True
1031 intercepted = True
1032
1032
1033 elif key == QtCore.Qt.Key_U:
1033 elif key == QtCore.Qt.Key_U:
1034 if self._in_buffer(position):
1034 if self._in_buffer(position):
1035 start_line = cursor.blockNumber()
1035 start_line = cursor.blockNumber()
1036 if start_line == self._get_prompt_cursor().blockNumber():
1036 if start_line == self._get_prompt_cursor().blockNumber():
1037 offset = len(self._prompt)
1037 offset = len(self._prompt)
1038 else:
1038 else:
1039 offset = len(self._continuation_prompt)
1039 offset = len(self._continuation_prompt)
1040 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1040 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1041 QtGui.QTextCursor.KeepAnchor)
1041 QtGui.QTextCursor.KeepAnchor)
1042 cursor.movePosition(QtGui.QTextCursor.Right,
1042 cursor.movePosition(QtGui.QTextCursor.Right,
1043 QtGui.QTextCursor.KeepAnchor, offset)
1043 QtGui.QTextCursor.KeepAnchor, offset)
1044 self._kill_ring.kill_cursor(cursor)
1044 self._kill_ring.kill_cursor(cursor)
1045 intercepted = True
1045 intercepted = True
1046
1046
1047 elif key == QtCore.Qt.Key_Y:
1047 elif key == QtCore.Qt.Key_Y:
1048 self._keep_cursor_in_buffer()
1048 self._keep_cursor_in_buffer()
1049 self._kill_ring.yank()
1049 self._kill_ring.yank()
1050 intercepted = True
1050 intercepted = True
1051
1051
1052 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1052 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1053 if key == QtCore.Qt.Key_Backspace:
1053 if key == QtCore.Qt.Key_Backspace:
1054 cursor = self._get_word_start_cursor(position)
1054 cursor = self._get_word_start_cursor(position)
1055 else: # key == QtCore.Qt.Key_Delete
1055 else: # key == QtCore.Qt.Key_Delete
1056 cursor = self._get_word_end_cursor(position)
1056 cursor = self._get_word_end_cursor(position)
1057 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1057 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1058 self._kill_ring.kill_cursor(cursor)
1058 self._kill_ring.kill_cursor(cursor)
1059 intercepted = True
1059 intercepted = True
1060
1060
1061 elif key in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
1061 elif key in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
1062 self.change_font_size(1)
1062 self.change_font_size(1)
1063 intercepted = True
1063 intercepted = True
1064
1064
1065 elif key == QtCore.Qt.Key_Minus:
1065 elif key == QtCore.Qt.Key_Minus:
1066 self.change_font_size(-1)
1066 self.change_font_size(-1)
1067 intercepted = True
1067 intercepted = True
1068
1068
1069 elif key == QtCore.Qt.Key_0:
1069 elif key == QtCore.Qt.Key_0:
1070 self.reset_font()
1070 self.reset_font()
1071 intercepted = True
1071 intercepted = True
1072
1072
1073 #------ Alt modifier ---------------------------------------------------
1073 #------ Alt modifier ---------------------------------------------------
1074
1074
1075 elif alt_down:
1075 elif alt_down:
1076 if key == QtCore.Qt.Key_B:
1076 if key == QtCore.Qt.Key_B:
1077 self._set_cursor(self._get_word_start_cursor(position))
1077 self._set_cursor(self._get_word_start_cursor(position))
1078 intercepted = True
1078 intercepted = True
1079
1079
1080 elif key == QtCore.Qt.Key_F:
1080 elif key == QtCore.Qt.Key_F:
1081 self._set_cursor(self._get_word_end_cursor(position))
1081 self._set_cursor(self._get_word_end_cursor(position))
1082 intercepted = True
1082 intercepted = True
1083
1083
1084 elif key == QtCore.Qt.Key_Y:
1084 elif key == QtCore.Qt.Key_Y:
1085 self._kill_ring.rotate()
1085 self._kill_ring.rotate()
1086 intercepted = True
1086 intercepted = True
1087
1087
1088 elif key == QtCore.Qt.Key_Backspace:
1088 elif key == QtCore.Qt.Key_Backspace:
1089 cursor = self._get_word_start_cursor(position)
1089 cursor = self._get_word_start_cursor(position)
1090 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1090 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1091 self._kill_ring.kill_cursor(cursor)
1091 self._kill_ring.kill_cursor(cursor)
1092 intercepted = True
1092 intercepted = True
1093
1093
1094 elif key == QtCore.Qt.Key_D:
1094 elif key == QtCore.Qt.Key_D:
1095 cursor = self._get_word_end_cursor(position)
1095 cursor = self._get_word_end_cursor(position)
1096 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1096 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1097 self._kill_ring.kill_cursor(cursor)
1097 self._kill_ring.kill_cursor(cursor)
1098 intercepted = True
1098 intercepted = True
1099
1099
1100 elif key == QtCore.Qt.Key_Delete:
1100 elif key == QtCore.Qt.Key_Delete:
1101 intercepted = True
1101 intercepted = True
1102
1102
1103 elif key == QtCore.Qt.Key_Greater:
1103 elif key == QtCore.Qt.Key_Greater:
1104 self._control.moveCursor(QtGui.QTextCursor.End)
1104 self._control.moveCursor(QtGui.QTextCursor.End)
1105 intercepted = True
1105 intercepted = True
1106
1106
1107 elif key == QtCore.Qt.Key_Less:
1107 elif key == QtCore.Qt.Key_Less:
1108 self._control.setTextCursor(self._get_prompt_cursor())
1108 self._control.setTextCursor(self._get_prompt_cursor())
1109 intercepted = True
1109 intercepted = True
1110
1110
1111 #------ No modifiers ---------------------------------------------------
1111 #------ No modifiers ---------------------------------------------------
1112
1112
1113 else:
1113 else:
1114 if shift_down:
1114 if shift_down:
1115 anchormode = QtGui.QTextCursor.KeepAnchor
1115 anchormode = QtGui.QTextCursor.KeepAnchor
1116 else:
1116 else:
1117 anchormode = QtGui.QTextCursor.MoveAnchor
1117 anchormode = QtGui.QTextCursor.MoveAnchor
1118
1118
1119 if key == QtCore.Qt.Key_Escape:
1119 if key == QtCore.Qt.Key_Escape:
1120 self._keyboard_quit()
1120 self._keyboard_quit()
1121 intercepted = True
1121 intercepted = True
1122
1122
1123 elif key == QtCore.Qt.Key_Up:
1123 elif key == QtCore.Qt.Key_Up:
1124 if self._reading or not self._up_pressed(shift_down):
1124 if self._reading or not self._up_pressed(shift_down):
1125 intercepted = True
1125 intercepted = True
1126 else:
1126 else:
1127 prompt_line = self._get_prompt_cursor().blockNumber()
1127 prompt_line = self._get_prompt_cursor().blockNumber()
1128 intercepted = cursor.blockNumber() <= prompt_line
1128 intercepted = cursor.blockNumber() <= prompt_line
1129
1129
1130 elif key == QtCore.Qt.Key_Down:
1130 elif key == QtCore.Qt.Key_Down:
1131 if self._reading or not self._down_pressed(shift_down):
1131 if self._reading or not self._down_pressed(shift_down):
1132 intercepted = True
1132 intercepted = True
1133 else:
1133 else:
1134 end_line = self._get_end_cursor().blockNumber()
1134 end_line = self._get_end_cursor().blockNumber()
1135 intercepted = cursor.blockNumber() == end_line
1135 intercepted = cursor.blockNumber() == end_line
1136
1136
1137 elif key == QtCore.Qt.Key_Tab:
1137 elif key == QtCore.Qt.Key_Tab:
1138 if not self._reading:
1138 if not self._reading:
1139 intercepted = not self._tab_pressed()
1139 intercepted = not self._tab_pressed()
1140
1140
1141 elif key == QtCore.Qt.Key_Left:
1141 elif key == QtCore.Qt.Key_Left:
1142
1142
1143 # Move to the previous line
1143 # Move to the previous line
1144 line, col = cursor.blockNumber(), cursor.columnNumber()
1144 line, col = cursor.blockNumber(), cursor.columnNumber()
1145 if line > self._get_prompt_cursor().blockNumber() and \
1145 if line > self._get_prompt_cursor().blockNumber() and \
1146 col == len(self._continuation_prompt):
1146 col == len(self._continuation_prompt):
1147 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1147 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1148 mode=anchormode)
1148 mode=anchormode)
1149 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1149 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1150 mode=anchormode)
1150 mode=anchormode)
1151 intercepted = True
1151 intercepted = True
1152
1152
1153 # Regular left movement
1153 # Regular left movement
1154 else:
1154 else:
1155 intercepted = not self._in_buffer(position - 1)
1155 intercepted = not self._in_buffer(position - 1)
1156
1156
1157 elif key == QtCore.Qt.Key_Right:
1157 elif key == QtCore.Qt.Key_Right:
1158 original_block_number = cursor.blockNumber()
1158 original_block_number = cursor.blockNumber()
1159 cursor.movePosition(QtGui.QTextCursor.Right,
1159 cursor.movePosition(QtGui.QTextCursor.Right,
1160 mode=anchormode)
1160 mode=anchormode)
1161 if cursor.blockNumber() != original_block_number:
1161 if cursor.blockNumber() != original_block_number:
1162 cursor.movePosition(QtGui.QTextCursor.Right,
1162 cursor.movePosition(QtGui.QTextCursor.Right,
1163 n=len(self._continuation_prompt),
1163 n=len(self._continuation_prompt),
1164 mode=anchormode)
1164 mode=anchormode)
1165 self._set_cursor(cursor)
1165 self._set_cursor(cursor)
1166 intercepted = True
1166 intercepted = True
1167
1167
1168 elif key == QtCore.Qt.Key_Home:
1168 elif key == QtCore.Qt.Key_Home:
1169 start_line = cursor.blockNumber()
1169 start_line = cursor.blockNumber()
1170 if start_line == self._get_prompt_cursor().blockNumber():
1170 if start_line == self._get_prompt_cursor().blockNumber():
1171 start_pos = self._prompt_pos
1171 start_pos = self._prompt_pos
1172 else:
1172 else:
1173 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1173 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1174 QtGui.QTextCursor.KeepAnchor)
1174 QtGui.QTextCursor.KeepAnchor)
1175 start_pos = cursor.position()
1175 start_pos = cursor.position()
1176 start_pos += len(self._continuation_prompt)
1176 start_pos += len(self._continuation_prompt)
1177 cursor.setPosition(position)
1177 cursor.setPosition(position)
1178 if shift_down and self._in_buffer(position):
1178 if shift_down and self._in_buffer(position):
1179 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1179 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1180 else:
1180 else:
1181 cursor.setPosition(start_pos)
1181 cursor.setPosition(start_pos)
1182 self._set_cursor(cursor)
1182 self._set_cursor(cursor)
1183 intercepted = True
1183 intercepted = True
1184
1184
1185 elif key == QtCore.Qt.Key_Backspace:
1185 elif key == QtCore.Qt.Key_Backspace:
1186
1186
1187 # Line deletion (remove continuation prompt)
1187 # Line deletion (remove continuation prompt)
1188 line, col = cursor.blockNumber(), cursor.columnNumber()
1188 line, col = cursor.blockNumber(), cursor.columnNumber()
1189 if not self._reading and \
1189 if not self._reading and \
1190 col == len(self._continuation_prompt) and \
1190 col == len(self._continuation_prompt) and \
1191 line > self._get_prompt_cursor().blockNumber():
1191 line > self._get_prompt_cursor().blockNumber():
1192 cursor.beginEditBlock()
1192 cursor.beginEditBlock()
1193 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1193 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1194 QtGui.QTextCursor.KeepAnchor)
1194 QtGui.QTextCursor.KeepAnchor)
1195 cursor.removeSelectedText()
1195 cursor.removeSelectedText()
1196 cursor.deletePreviousChar()
1196 cursor.deletePreviousChar()
1197 cursor.endEditBlock()
1197 cursor.endEditBlock()
1198 intercepted = True
1198 intercepted = True
1199
1199
1200 # Regular backwards deletion
1200 # Regular backwards deletion
1201 else:
1201 else:
1202 anchor = cursor.anchor()
1202 anchor = cursor.anchor()
1203 if anchor == position:
1203 if anchor == position:
1204 intercepted = not self._in_buffer(position - 1)
1204 intercepted = not self._in_buffer(position - 1)
1205 else:
1205 else:
1206 intercepted = not self._in_buffer(min(anchor, position))
1206 intercepted = not self._in_buffer(min(anchor, position))
1207
1207
1208 elif key == QtCore.Qt.Key_Delete:
1208 elif key == QtCore.Qt.Key_Delete:
1209
1209
1210 # Line deletion (remove continuation prompt)
1210 # Line deletion (remove continuation prompt)
1211 if not self._reading and self._in_buffer(position) and \
1211 if not self._reading and self._in_buffer(position) and \
1212 cursor.atBlockEnd() and not cursor.hasSelection():
1212 cursor.atBlockEnd() and not cursor.hasSelection():
1213 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1213 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1214 QtGui.QTextCursor.KeepAnchor)
1214 QtGui.QTextCursor.KeepAnchor)
1215 cursor.movePosition(QtGui.QTextCursor.Right,
1215 cursor.movePosition(QtGui.QTextCursor.Right,
1216 QtGui.QTextCursor.KeepAnchor,
1216 QtGui.QTextCursor.KeepAnchor,
1217 len(self._continuation_prompt))
1217 len(self._continuation_prompt))
1218 cursor.removeSelectedText()
1218 cursor.removeSelectedText()
1219 intercepted = True
1219 intercepted = True
1220
1220
1221 # Regular forwards deletion:
1221 # Regular forwards deletion:
1222 else:
1222 else:
1223 anchor = cursor.anchor()
1223 anchor = cursor.anchor()
1224 intercepted = (not self._in_buffer(anchor) or
1224 intercepted = (not self._in_buffer(anchor) or
1225 not self._in_buffer(position))
1225 not self._in_buffer(position))
1226
1226
1227 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1227 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1228 # using the keyboard in any part of the buffer. Also, permit scrolling
1228 # using the keyboard in any part of the buffer. Also, permit scrolling
1229 # with Page Up/Down keys. Finally, if we're executing, don't move the
1229 # with Page Up/Down keys. Finally, if we're executing, don't move the
1230 # cursor (if even this made sense, we can't guarantee that the prompt
1230 # cursor (if even this made sense, we can't guarantee that the prompt
1231 # position is still valid due to text truncation).
1231 # position is still valid due to text truncation).
1232 if not (self._control_key_down(event.modifiers(), include_command=True)
1232 if not (self._control_key_down(event.modifiers(), include_command=True)
1233 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1233 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1234 or (self._executing and not self._reading)):
1234 or (self._executing and not self._reading)):
1235 self._keep_cursor_in_buffer()
1235 self._keep_cursor_in_buffer()
1236
1236
1237 return intercepted
1237 return intercepted
1238
1238
1239 def _event_filter_page_keypress(self, event):
1239 def _event_filter_page_keypress(self, event):
1240 """ Filter key events for the paging widget to create console-like
1240 """ Filter key events for the paging widget to create console-like
1241 interface.
1241 interface.
1242 """
1242 """
1243 key = event.key()
1243 key = event.key()
1244 ctrl_down = self._control_key_down(event.modifiers())
1244 ctrl_down = self._control_key_down(event.modifiers())
1245 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1245 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1246
1246
1247 if ctrl_down:
1247 if ctrl_down:
1248 if key == QtCore.Qt.Key_O:
1248 if key == QtCore.Qt.Key_O:
1249 self._control.setFocus()
1249 self._control.setFocus()
1250 intercept = True
1250 intercept = True
1251
1251
1252 elif alt_down:
1252 elif alt_down:
1253 if key == QtCore.Qt.Key_Greater:
1253 if key == QtCore.Qt.Key_Greater:
1254 self._page_control.moveCursor(QtGui.QTextCursor.End)
1254 self._page_control.moveCursor(QtGui.QTextCursor.End)
1255 intercepted = True
1255 intercepted = True
1256
1256
1257 elif key == QtCore.Qt.Key_Less:
1257 elif key == QtCore.Qt.Key_Less:
1258 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1258 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1259 intercepted = True
1259 intercepted = True
1260
1260
1261 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1261 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1262 if self._splitter:
1262 if self._splitter:
1263 self._page_control.hide()
1263 self._page_control.hide()
1264 self._control.setFocus()
1264 else:
1265 else:
1265 self.layout().setCurrentWidget(self._control)
1266 self.layout().setCurrentWidget(self._control)
1266 return True
1267 return True
1267
1268
1268 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1269 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1269 QtCore.Qt.Key_Tab):
1270 QtCore.Qt.Key_Tab):
1270 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1271 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1271 QtCore.Qt.Key_PageDown,
1272 QtCore.Qt.Key_PageDown,
1272 QtCore.Qt.NoModifier)
1273 QtCore.Qt.NoModifier)
1273 QtGui.qApp.sendEvent(self._page_control, new_event)
1274 QtGui.qApp.sendEvent(self._page_control, new_event)
1274 return True
1275 return True
1275
1276
1276 elif key == QtCore.Qt.Key_Backspace:
1277 elif key == QtCore.Qt.Key_Backspace:
1277 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1278 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1278 QtCore.Qt.Key_PageUp,
1279 QtCore.Qt.Key_PageUp,
1279 QtCore.Qt.NoModifier)
1280 QtCore.Qt.NoModifier)
1280 QtGui.qApp.sendEvent(self._page_control, new_event)
1281 QtGui.qApp.sendEvent(self._page_control, new_event)
1281 return True
1282 return True
1282
1283
1283 return False
1284 return False
1284
1285
1285 def _format_as_columns(self, items, separator=' '):
1286 def _format_as_columns(self, items, separator=' '):
1286 """ Transform a list of strings into a single string with columns.
1287 """ Transform a list of strings into a single string with columns.
1287
1288
1288 Parameters
1289 Parameters
1289 ----------
1290 ----------
1290 items : sequence of strings
1291 items : sequence of strings
1291 The strings to process.
1292 The strings to process.
1292
1293
1293 separator : str, optional [default is two spaces]
1294 separator : str, optional [default is two spaces]
1294 The string that separates columns.
1295 The string that separates columns.
1295
1296
1296 Returns
1297 Returns
1297 -------
1298 -------
1298 The formatted string.
1299 The formatted string.
1299 """
1300 """
1300 # Calculate the number of characters available.
1301 # Calculate the number of characters available.
1301 width = self._control.viewport().width()
1302 width = self._control.viewport().width()
1302 char_width = QtGui.QFontMetrics(self.font).width(' ')
1303 char_width = QtGui.QFontMetrics(self.font).width(' ')
1303 displaywidth = max(10, (width / char_width) - 1)
1304 displaywidth = max(10, (width / char_width) - 1)
1304
1305
1305 return columnize(items, separator, displaywidth)
1306 return columnize(items, separator, displaywidth)
1306
1307
1307 def _get_block_plain_text(self, block):
1308 def _get_block_plain_text(self, block):
1308 """ Given a QTextBlock, return its unformatted text.
1309 """ Given a QTextBlock, return its unformatted text.
1309 """
1310 """
1310 cursor = QtGui.QTextCursor(block)
1311 cursor = QtGui.QTextCursor(block)
1311 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1312 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1312 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1313 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1313 QtGui.QTextCursor.KeepAnchor)
1314 QtGui.QTextCursor.KeepAnchor)
1314 return cursor.selection().toPlainText()
1315 return cursor.selection().toPlainText()
1315
1316
1316 def _get_cursor(self):
1317 def _get_cursor(self):
1317 """ Convenience method that returns a cursor for the current position.
1318 """ Convenience method that returns a cursor for the current position.
1318 """
1319 """
1319 return self._control.textCursor()
1320 return self._control.textCursor()
1320
1321
1321 def _get_end_cursor(self):
1322 def _get_end_cursor(self):
1322 """ Convenience method that returns a cursor for the last character.
1323 """ Convenience method that returns a cursor for the last character.
1323 """
1324 """
1324 cursor = self._control.textCursor()
1325 cursor = self._control.textCursor()
1325 cursor.movePosition(QtGui.QTextCursor.End)
1326 cursor.movePosition(QtGui.QTextCursor.End)
1326 return cursor
1327 return cursor
1327
1328
1328 def _get_input_buffer_cursor_column(self):
1329 def _get_input_buffer_cursor_column(self):
1329 """ Returns the column of the cursor in the input buffer, excluding the
1330 """ Returns the column of the cursor in the input buffer, excluding the
1330 contribution by the prompt, or -1 if there is no such column.
1331 contribution by the prompt, or -1 if there is no such column.
1331 """
1332 """
1332 prompt = self._get_input_buffer_cursor_prompt()
1333 prompt = self._get_input_buffer_cursor_prompt()
1333 if prompt is None:
1334 if prompt is None:
1334 return -1
1335 return -1
1335 else:
1336 else:
1336 cursor = self._control.textCursor()
1337 cursor = self._control.textCursor()
1337 return cursor.columnNumber() - len(prompt)
1338 return cursor.columnNumber() - len(prompt)
1338
1339
1339 def _get_input_buffer_cursor_line(self):
1340 def _get_input_buffer_cursor_line(self):
1340 """ Returns the text of the line of the input buffer that contains the
1341 """ Returns the text of the line of the input buffer that contains the
1341 cursor, or None if there is no such line.
1342 cursor, or None if there is no such line.
1342 """
1343 """
1343 prompt = self._get_input_buffer_cursor_prompt()
1344 prompt = self._get_input_buffer_cursor_prompt()
1344 if prompt is None:
1345 if prompt is None:
1345 return None
1346 return None
1346 else:
1347 else:
1347 cursor = self._control.textCursor()
1348 cursor = self._control.textCursor()
1348 text = self._get_block_plain_text(cursor.block())
1349 text = self._get_block_plain_text(cursor.block())
1349 return text[len(prompt):]
1350 return text[len(prompt):]
1350
1351
1351 def _get_input_buffer_cursor_prompt(self):
1352 def _get_input_buffer_cursor_prompt(self):
1352 """ Returns the (plain text) prompt for line of the input buffer that
1353 """ Returns the (plain text) prompt for line of the input buffer that
1353 contains the cursor, or None if there is no such line.
1354 contains the cursor, or None if there is no such line.
1354 """
1355 """
1355 if self._executing:
1356 if self._executing:
1356 return None
1357 return None
1357 cursor = self._control.textCursor()
1358 cursor = self._control.textCursor()
1358 if cursor.position() >= self._prompt_pos:
1359 if cursor.position() >= self._prompt_pos:
1359 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1360 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1360 return self._prompt
1361 return self._prompt
1361 else:
1362 else:
1362 return self._continuation_prompt
1363 return self._continuation_prompt
1363 else:
1364 else:
1364 return None
1365 return None
1365
1366
1366 def _get_prompt_cursor(self):
1367 def _get_prompt_cursor(self):
1367 """ Convenience method that returns a cursor for the prompt position.
1368 """ Convenience method that returns a cursor for the prompt position.
1368 """
1369 """
1369 cursor = self._control.textCursor()
1370 cursor = self._control.textCursor()
1370 cursor.setPosition(self._prompt_pos)
1371 cursor.setPosition(self._prompt_pos)
1371 return cursor
1372 return cursor
1372
1373
1373 def _get_selection_cursor(self, start, end):
1374 def _get_selection_cursor(self, start, end):
1374 """ Convenience method that returns a cursor with text selected between
1375 """ Convenience method that returns a cursor with text selected between
1375 the positions 'start' and 'end'.
1376 the positions 'start' and 'end'.
1376 """
1377 """
1377 cursor = self._control.textCursor()
1378 cursor = self._control.textCursor()
1378 cursor.setPosition(start)
1379 cursor.setPosition(start)
1379 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1380 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1380 return cursor
1381 return cursor
1381
1382
1382 def _get_word_start_cursor(self, position):
1383 def _get_word_start_cursor(self, position):
1383 """ Find the start of the word to the left the given position. If a
1384 """ Find the start of the word to the left the given position. If a
1384 sequence of non-word characters precedes the first word, skip over
1385 sequence of non-word characters precedes the first word, skip over
1385 them. (This emulates the behavior of bash, emacs, etc.)
1386 them. (This emulates the behavior of bash, emacs, etc.)
1386 """
1387 """
1387 document = self._control.document()
1388 document = self._control.document()
1388 position -= 1
1389 position -= 1
1389 while position >= self._prompt_pos and \
1390 while position >= self._prompt_pos and \
1390 not is_letter_or_number(document.characterAt(position)):
1391 not is_letter_or_number(document.characterAt(position)):
1391 position -= 1
1392 position -= 1
1392 while position >= self._prompt_pos and \
1393 while position >= self._prompt_pos and \
1393 is_letter_or_number(document.characterAt(position)):
1394 is_letter_or_number(document.characterAt(position)):
1394 position -= 1
1395 position -= 1
1395 cursor = self._control.textCursor()
1396 cursor = self._control.textCursor()
1396 cursor.setPosition(position + 1)
1397 cursor.setPosition(position + 1)
1397 return cursor
1398 return cursor
1398
1399
1399 def _get_word_end_cursor(self, position):
1400 def _get_word_end_cursor(self, position):
1400 """ Find the end of the word to the right the given position. If a
1401 """ Find the end of the word to the right the given position. If a
1401 sequence of non-word characters precedes the first word, skip over
1402 sequence of non-word characters precedes the first word, skip over
1402 them. (This emulates the behavior of bash, emacs, etc.)
1403 them. (This emulates the behavior of bash, emacs, etc.)
1403 """
1404 """
1404 document = self._control.document()
1405 document = self._control.document()
1405 end = self._get_end_cursor().position()
1406 end = self._get_end_cursor().position()
1406 while position < end and \
1407 while position < end and \
1407 not is_letter_or_number(document.characterAt(position)):
1408 not is_letter_or_number(document.characterAt(position)):
1408 position += 1
1409 position += 1
1409 while position < end and \
1410 while position < end and \
1410 is_letter_or_number(document.characterAt(position)):
1411 is_letter_or_number(document.characterAt(position)):
1411 position += 1
1412 position += 1
1412 cursor = self._control.textCursor()
1413 cursor = self._control.textCursor()
1413 cursor.setPosition(position)
1414 cursor.setPosition(position)
1414 return cursor
1415 return cursor
1415
1416
1416 def _insert_continuation_prompt(self, cursor):
1417 def _insert_continuation_prompt(self, cursor):
1417 """ Inserts new continuation prompt using the specified cursor.
1418 """ Inserts new continuation prompt using the specified cursor.
1418 """
1419 """
1419 if self._continuation_prompt_html is None:
1420 if self._continuation_prompt_html is None:
1420 self._insert_plain_text(cursor, self._continuation_prompt)
1421 self._insert_plain_text(cursor, self._continuation_prompt)
1421 else:
1422 else:
1422 self._continuation_prompt = self._insert_html_fetching_plain_text(
1423 self._continuation_prompt = self._insert_html_fetching_plain_text(
1423 cursor, self._continuation_prompt_html)
1424 cursor, self._continuation_prompt_html)
1424
1425
1425 def _insert_html(self, cursor, html):
1426 def _insert_html(self, cursor, html):
1426 """ Inserts HTML using the specified cursor in such a way that future
1427 """ Inserts HTML using the specified cursor in such a way that future
1427 formatting is unaffected.
1428 formatting is unaffected.
1428 """
1429 """
1429 cursor.beginEditBlock()
1430 cursor.beginEditBlock()
1430 cursor.insertHtml(html)
1431 cursor.insertHtml(html)
1431
1432
1432 # After inserting HTML, the text document "remembers" it's in "html
1433 # After inserting HTML, the text document "remembers" it's in "html
1433 # mode", which means that subsequent calls adding plain text will result
1434 # mode", which means that subsequent calls adding plain text will result
1434 # in unwanted formatting, lost tab characters, etc. The following code
1435 # in unwanted formatting, lost tab characters, etc. The following code
1435 # hacks around this behavior, which I consider to be a bug in Qt, by
1436 # hacks around this behavior, which I consider to be a bug in Qt, by
1436 # (crudely) resetting the document's style state.
1437 # (crudely) resetting the document's style state.
1437 cursor.movePosition(QtGui.QTextCursor.Left,
1438 cursor.movePosition(QtGui.QTextCursor.Left,
1438 QtGui.QTextCursor.KeepAnchor)
1439 QtGui.QTextCursor.KeepAnchor)
1439 if cursor.selection().toPlainText() == ' ':
1440 if cursor.selection().toPlainText() == ' ':
1440 cursor.removeSelectedText()
1441 cursor.removeSelectedText()
1441 else:
1442 else:
1442 cursor.movePosition(QtGui.QTextCursor.Right)
1443 cursor.movePosition(QtGui.QTextCursor.Right)
1443 cursor.insertText(' ', QtGui.QTextCharFormat())
1444 cursor.insertText(' ', QtGui.QTextCharFormat())
1444 cursor.endEditBlock()
1445 cursor.endEditBlock()
1445
1446
1446 def _insert_html_fetching_plain_text(self, cursor, html):
1447 def _insert_html_fetching_plain_text(self, cursor, html):
1447 """ Inserts HTML using the specified cursor, then returns its plain text
1448 """ Inserts HTML using the specified cursor, then returns its plain text
1448 version.
1449 version.
1449 """
1450 """
1450 cursor.beginEditBlock()
1451 cursor.beginEditBlock()
1451 cursor.removeSelectedText()
1452 cursor.removeSelectedText()
1452
1453
1453 start = cursor.position()
1454 start = cursor.position()
1454 self._insert_html(cursor, html)
1455 self._insert_html(cursor, html)
1455 end = cursor.position()
1456 end = cursor.position()
1456 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1457 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1457 text = cursor.selection().toPlainText()
1458 text = cursor.selection().toPlainText()
1458
1459
1459 cursor.setPosition(end)
1460 cursor.setPosition(end)
1460 cursor.endEditBlock()
1461 cursor.endEditBlock()
1461 return text
1462 return text
1462
1463
1463 def _insert_plain_text(self, cursor, text):
1464 def _insert_plain_text(self, cursor, text):
1464 """ Inserts plain text using the specified cursor, processing ANSI codes
1465 """ Inserts plain text using the specified cursor, processing ANSI codes
1465 if enabled.
1466 if enabled.
1466 """
1467 """
1467 cursor.beginEditBlock()
1468 cursor.beginEditBlock()
1468 if self.ansi_codes:
1469 if self.ansi_codes:
1469 for substring in self._ansi_processor.split_string(text):
1470 for substring in self._ansi_processor.split_string(text):
1470 for act in self._ansi_processor.actions:
1471 for act in self._ansi_processor.actions:
1471
1472
1472 # Unlike real terminal emulators, we don't distinguish
1473 # Unlike real terminal emulators, we don't distinguish
1473 # between the screen and the scrollback buffer. A screen
1474 # between the screen and the scrollback buffer. A screen
1474 # erase request clears everything.
1475 # erase request clears everything.
1475 if act.action == 'erase' and act.area == 'screen':
1476 if act.action == 'erase' and act.area == 'screen':
1476 cursor.select(QtGui.QTextCursor.Document)
1477 cursor.select(QtGui.QTextCursor.Document)
1477 cursor.removeSelectedText()
1478 cursor.removeSelectedText()
1478
1479
1479 # Simulate a form feed by scrolling just past the last line.
1480 # Simulate a form feed by scrolling just past the last line.
1480 elif act.action == 'scroll' and act.unit == 'page':
1481 elif act.action == 'scroll' and act.unit == 'page':
1481 cursor.insertText('\n')
1482 cursor.insertText('\n')
1482 cursor.endEditBlock()
1483 cursor.endEditBlock()
1483 self._set_top_cursor(cursor)
1484 self._set_top_cursor(cursor)
1484 cursor.joinPreviousEditBlock()
1485 cursor.joinPreviousEditBlock()
1485 cursor.deletePreviousChar()
1486 cursor.deletePreviousChar()
1486
1487
1487 format = self._ansi_processor.get_format()
1488 format = self._ansi_processor.get_format()
1488 cursor.insertText(substring, format)
1489 cursor.insertText(substring, format)
1489 else:
1490 else:
1490 cursor.insertText(text)
1491 cursor.insertText(text)
1491 cursor.endEditBlock()
1492 cursor.endEditBlock()
1492
1493
1493 def _insert_plain_text_into_buffer(self, cursor, text):
1494 def _insert_plain_text_into_buffer(self, cursor, text):
1494 """ Inserts text into the input buffer using the specified cursor (which
1495 """ Inserts text into the input buffer using the specified cursor (which
1495 must be in the input buffer), ensuring that continuation prompts are
1496 must be in the input buffer), ensuring that continuation prompts are
1496 inserted as necessary.
1497 inserted as necessary.
1497 """
1498 """
1498 lines = text.splitlines(True)
1499 lines = text.splitlines(True)
1499 if lines:
1500 if lines:
1500 cursor.beginEditBlock()
1501 cursor.beginEditBlock()
1501 cursor.insertText(lines[0])
1502 cursor.insertText(lines[0])
1502 for line in lines[1:]:
1503 for line in lines[1:]:
1503 if self._continuation_prompt_html is None:
1504 if self._continuation_prompt_html is None:
1504 cursor.insertText(self._continuation_prompt)
1505 cursor.insertText(self._continuation_prompt)
1505 else:
1506 else:
1506 self._continuation_prompt = \
1507 self._continuation_prompt = \
1507 self._insert_html_fetching_plain_text(
1508 self._insert_html_fetching_plain_text(
1508 cursor, self._continuation_prompt_html)
1509 cursor, self._continuation_prompt_html)
1509 cursor.insertText(line)
1510 cursor.insertText(line)
1510 cursor.endEditBlock()
1511 cursor.endEditBlock()
1511
1512
1512 def _in_buffer(self, position=None):
1513 def _in_buffer(self, position=None):
1513 """ Returns whether the current cursor (or, if specified, a position) is
1514 """ Returns whether the current cursor (or, if specified, a position) is
1514 inside the editing region.
1515 inside the editing region.
1515 """
1516 """
1516 cursor = self._control.textCursor()
1517 cursor = self._control.textCursor()
1517 if position is None:
1518 if position is None:
1518 position = cursor.position()
1519 position = cursor.position()
1519 else:
1520 else:
1520 cursor.setPosition(position)
1521 cursor.setPosition(position)
1521 line = cursor.blockNumber()
1522 line = cursor.blockNumber()
1522 prompt_line = self._get_prompt_cursor().blockNumber()
1523 prompt_line = self._get_prompt_cursor().blockNumber()
1523 if line == prompt_line:
1524 if line == prompt_line:
1524 return position >= self._prompt_pos
1525 return position >= self._prompt_pos
1525 elif line > prompt_line:
1526 elif line > prompt_line:
1526 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1527 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1527 prompt_pos = cursor.position() + len(self._continuation_prompt)
1528 prompt_pos = cursor.position() + len(self._continuation_prompt)
1528 return position >= prompt_pos
1529 return position >= prompt_pos
1529 return False
1530 return False
1530
1531
1531 def _keep_cursor_in_buffer(self):
1532 def _keep_cursor_in_buffer(self):
1532 """ Ensures that the cursor is inside the editing region. Returns
1533 """ Ensures that the cursor is inside the editing region. Returns
1533 whether the cursor was moved.
1534 whether the cursor was moved.
1534 """
1535 """
1535 moved = not self._in_buffer()
1536 moved = not self._in_buffer()
1536 if moved:
1537 if moved:
1537 cursor = self._control.textCursor()
1538 cursor = self._control.textCursor()
1538 cursor.movePosition(QtGui.QTextCursor.End)
1539 cursor.movePosition(QtGui.QTextCursor.End)
1539 self._control.setTextCursor(cursor)
1540 self._control.setTextCursor(cursor)
1540 return moved
1541 return moved
1541
1542
1542 def _keyboard_quit(self):
1543 def _keyboard_quit(self):
1543 """ Cancels the current editing task ala Ctrl-G in Emacs.
1544 """ Cancels the current editing task ala Ctrl-G in Emacs.
1544 """
1545 """
1545 if self._text_completing_pos:
1546 if self._text_completing_pos:
1546 self._cancel_text_completion()
1547 self._cancel_text_completion()
1547 else:
1548 else:
1548 self.input_buffer = ''
1549 self.input_buffer = ''
1549
1550
1550 def _page(self, text, html=False):
1551 def _page(self, text, html=False):
1551 """ Displays text using the pager if it exceeds the height of the
1552 """ Displays text using the pager if it exceeds the height of the
1552 viewport.
1553 viewport.
1553
1554
1554 Parameters:
1555 Parameters:
1555 -----------
1556 -----------
1556 html : bool, optional (default False)
1557 html : bool, optional (default False)
1557 If set, the text will be interpreted as HTML instead of plain text.
1558 If set, the text will be interpreted as HTML instead of plain text.
1558 """
1559 """
1559 line_height = QtGui.QFontMetrics(self.font).height()
1560 line_height = QtGui.QFontMetrics(self.font).height()
1560 minlines = self._control.viewport().height() / line_height
1561 minlines = self._control.viewport().height() / line_height
1561 if self.paging != 'none' and \
1562 if self.paging != 'none' and \
1562 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1563 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1563 if self.paging == 'custom':
1564 if self.paging == 'custom':
1564 self.custom_page_requested.emit(text)
1565 self.custom_page_requested.emit(text)
1565 else:
1566 else:
1566 self._page_control.clear()
1567 self._page_control.clear()
1567 cursor = self._page_control.textCursor()
1568 cursor = self._page_control.textCursor()
1568 if html:
1569 if html:
1569 self._insert_html(cursor, text)
1570 self._insert_html(cursor, text)
1570 else:
1571 else:
1571 self._insert_plain_text(cursor, text)
1572 self._insert_plain_text(cursor, text)
1572 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1573 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1573
1574
1574 self._page_control.viewport().resize(self._control.size())
1575 self._page_control.viewport().resize(self._control.size())
1575 if self._splitter:
1576 if self._splitter:
1576 self._page_control.show()
1577 self._page_control.show()
1577 self._page_control.setFocus()
1578 self._page_control.setFocus()
1578 else:
1579 else:
1579 self.layout().setCurrentWidget(self._page_control)
1580 self.layout().setCurrentWidget(self._page_control)
1580 elif html:
1581 elif html:
1581 self._append_plain_html(text)
1582 self._append_plain_html(text)
1582 else:
1583 else:
1583 self._append_plain_text(text)
1584 self._append_plain_text(text)
1584
1585
1585 def _prompt_finished(self):
1586 def _prompt_finished(self):
1586 """ Called immediately after a prompt is finished, i.e. when some input
1587 """ Called immediately after a prompt is finished, i.e. when some input
1587 will be processed and a new prompt displayed.
1588 will be processed and a new prompt displayed.
1588 """
1589 """
1589 self._control.setReadOnly(True)
1590 self._control.setReadOnly(True)
1590 self._prompt_finished_hook()
1591 self._prompt_finished_hook()
1591
1592
1592 def _prompt_started(self):
1593 def _prompt_started(self):
1593 """ Called immediately after a new prompt is displayed.
1594 """ Called immediately after a new prompt is displayed.
1594 """
1595 """
1595 # Temporarily disable the maximum block count to permit undo/redo and
1596 # Temporarily disable the maximum block count to permit undo/redo and
1596 # to ensure that the prompt position does not change due to truncation.
1597 # to ensure that the prompt position does not change due to truncation.
1597 self._control.document().setMaximumBlockCount(0)
1598 self._control.document().setMaximumBlockCount(0)
1598 self._control.setUndoRedoEnabled(True)
1599 self._control.setUndoRedoEnabled(True)
1599
1600
1600 # Work around bug in QPlainTextEdit: input method is not re-enabled
1601 # Work around bug in QPlainTextEdit: input method is not re-enabled
1601 # when read-only is disabled.
1602 # when read-only is disabled.
1602 self._control.setReadOnly(False)
1603 self._control.setReadOnly(False)
1603 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1604 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1604
1605
1605 if not self._reading:
1606 if not self._reading:
1606 self._executing = False
1607 self._executing = False
1607 self._prompt_started_hook()
1608 self._prompt_started_hook()
1608
1609
1609 # If the input buffer has changed while executing, load it.
1610 # If the input buffer has changed while executing, load it.
1610 if self._input_buffer_pending:
1611 if self._input_buffer_pending:
1611 self.input_buffer = self._input_buffer_pending
1612 self.input_buffer = self._input_buffer_pending
1612 self._input_buffer_pending = ''
1613 self._input_buffer_pending = ''
1613
1614
1614 self._control.moveCursor(QtGui.QTextCursor.End)
1615 self._control.moveCursor(QtGui.QTextCursor.End)
1615
1616
1616 def _readline(self, prompt='', callback=None):
1617 def _readline(self, prompt='', callback=None):
1617 """ Reads one line of input from the user.
1618 """ Reads one line of input from the user.
1618
1619
1619 Parameters
1620 Parameters
1620 ----------
1621 ----------
1621 prompt : str, optional
1622 prompt : str, optional
1622 The prompt to print before reading the line.
1623 The prompt to print before reading the line.
1623
1624
1624 callback : callable, optional
1625 callback : callable, optional
1625 A callback to execute with the read line. If not specified, input is
1626 A callback to execute with the read line. If not specified, input is
1626 read *synchronously* and this method does not return until it has
1627 read *synchronously* and this method does not return until it has
1627 been read.
1628 been read.
1628
1629
1629 Returns
1630 Returns
1630 -------
1631 -------
1631 If a callback is specified, returns nothing. Otherwise, returns the
1632 If a callback is specified, returns nothing. Otherwise, returns the
1632 input string with the trailing newline stripped.
1633 input string with the trailing newline stripped.
1633 """
1634 """
1634 if self._reading:
1635 if self._reading:
1635 raise RuntimeError('Cannot read a line. Widget is already reading.')
1636 raise RuntimeError('Cannot read a line. Widget is already reading.')
1636
1637
1637 if not callback and not self.isVisible():
1638 if not callback and not self.isVisible():
1638 # If the user cannot see the widget, this function cannot return.
1639 # If the user cannot see the widget, this function cannot return.
1639 raise RuntimeError('Cannot synchronously read a line if the widget '
1640 raise RuntimeError('Cannot synchronously read a line if the widget '
1640 'is not visible!')
1641 'is not visible!')
1641
1642
1642 self._reading = True
1643 self._reading = True
1643 self._show_prompt(prompt, newline=False)
1644 self._show_prompt(prompt, newline=False)
1644
1645
1645 if callback is None:
1646 if callback is None:
1646 self._reading_callback = None
1647 self._reading_callback = None
1647 while self._reading:
1648 while self._reading:
1648 QtCore.QCoreApplication.processEvents()
1649 QtCore.QCoreApplication.processEvents()
1649 return self._get_input_buffer(force=True).rstrip('\n')
1650 return self._get_input_buffer(force=True).rstrip('\n')
1650
1651
1651 else:
1652 else:
1652 self._reading_callback = lambda: \
1653 self._reading_callback = lambda: \
1653 callback(self._get_input_buffer(force=True).rstrip('\n'))
1654 callback(self._get_input_buffer(force=True).rstrip('\n'))
1654
1655
1655 def _set_continuation_prompt(self, prompt, html=False):
1656 def _set_continuation_prompt(self, prompt, html=False):
1656 """ Sets the continuation prompt.
1657 """ Sets the continuation prompt.
1657
1658
1658 Parameters
1659 Parameters
1659 ----------
1660 ----------
1660 prompt : str
1661 prompt : str
1661 The prompt to show when more input is needed.
1662 The prompt to show when more input is needed.
1662
1663
1663 html : bool, optional (default False)
1664 html : bool, optional (default False)
1664 If set, the prompt will be inserted as formatted HTML. Otherwise,
1665 If set, the prompt will be inserted as formatted HTML. Otherwise,
1665 the prompt will be treated as plain text, though ANSI color codes
1666 the prompt will be treated as plain text, though ANSI color codes
1666 will be handled.
1667 will be handled.
1667 """
1668 """
1668 if html:
1669 if html:
1669 self._continuation_prompt_html = prompt
1670 self._continuation_prompt_html = prompt
1670 else:
1671 else:
1671 self._continuation_prompt = prompt
1672 self._continuation_prompt = prompt
1672 self._continuation_prompt_html = None
1673 self._continuation_prompt_html = None
1673
1674
1674 def _set_cursor(self, cursor):
1675 def _set_cursor(self, cursor):
1675 """ Convenience method to set the current cursor.
1676 """ Convenience method to set the current cursor.
1676 """
1677 """
1677 self._control.setTextCursor(cursor)
1678 self._control.setTextCursor(cursor)
1678
1679
1679 def _set_top_cursor(self, cursor):
1680 def _set_top_cursor(self, cursor):
1680 """ Scrolls the viewport so that the specified cursor is at the top.
1681 """ Scrolls the viewport so that the specified cursor is at the top.
1681 """
1682 """
1682 scrollbar = self._control.verticalScrollBar()
1683 scrollbar = self._control.verticalScrollBar()
1683 scrollbar.setValue(scrollbar.maximum())
1684 scrollbar.setValue(scrollbar.maximum())
1684 original_cursor = self._control.textCursor()
1685 original_cursor = self._control.textCursor()
1685 self._control.setTextCursor(cursor)
1686 self._control.setTextCursor(cursor)
1686 self._control.ensureCursorVisible()
1687 self._control.ensureCursorVisible()
1687 self._control.setTextCursor(original_cursor)
1688 self._control.setTextCursor(original_cursor)
1688
1689
1689 def _show_prompt(self, prompt=None, html=False, newline=True):
1690 def _show_prompt(self, prompt=None, html=False, newline=True):
1690 """ Writes a new prompt at the end of the buffer.
1691 """ Writes a new prompt at the end of the buffer.
1691
1692
1692 Parameters
1693 Parameters
1693 ----------
1694 ----------
1694 prompt : str, optional
1695 prompt : str, optional
1695 The prompt to show. If not specified, the previous prompt is used.
1696 The prompt to show. If not specified, the previous prompt is used.
1696
1697
1697 html : bool, optional (default False)
1698 html : bool, optional (default False)
1698 Only relevant when a prompt is specified. If set, the prompt will
1699 Only relevant when a prompt is specified. If set, the prompt will
1699 be inserted as formatted HTML. Otherwise, the prompt will be treated
1700 be inserted as formatted HTML. Otherwise, the prompt will be treated
1700 as plain text, though ANSI color codes will be handled.
1701 as plain text, though ANSI color codes will be handled.
1701
1702
1702 newline : bool, optional (default True)
1703 newline : bool, optional (default True)
1703 If set, a new line will be written before showing the prompt if
1704 If set, a new line will be written before showing the prompt if
1704 there is not already a newline at the end of the buffer.
1705 there is not already a newline at the end of the buffer.
1705 """
1706 """
1706 # Save the current end position to support _append*(before_prompt=True).
1707 # Save the current end position to support _append*(before_prompt=True).
1707 cursor = self._get_end_cursor()
1708 cursor = self._get_end_cursor()
1708 self._append_before_prompt_pos = cursor.position()
1709 self._append_before_prompt_pos = cursor.position()
1709
1710
1710 # Insert a preliminary newline, if necessary.
1711 # Insert a preliminary newline, if necessary.
1711 if newline and cursor.position() > 0:
1712 if newline and cursor.position() > 0:
1712 cursor.movePosition(QtGui.QTextCursor.Left,
1713 cursor.movePosition(QtGui.QTextCursor.Left,
1713 QtGui.QTextCursor.KeepAnchor)
1714 QtGui.QTextCursor.KeepAnchor)
1714 if cursor.selection().toPlainText() != '\n':
1715 if cursor.selection().toPlainText() != '\n':
1715 self._append_plain_text('\n')
1716 self._append_plain_text('\n')
1716
1717
1717 # Write the prompt.
1718 # Write the prompt.
1718 self._append_plain_text(self._prompt_sep)
1719 self._append_plain_text(self._prompt_sep)
1719 if prompt is None:
1720 if prompt is None:
1720 if self._prompt_html is None:
1721 if self._prompt_html is None:
1721 self._append_plain_text(self._prompt)
1722 self._append_plain_text(self._prompt)
1722 else:
1723 else:
1723 self._append_html(self._prompt_html)
1724 self._append_html(self._prompt_html)
1724 else:
1725 else:
1725 if html:
1726 if html:
1726 self._prompt = self._append_html_fetching_plain_text(prompt)
1727 self._prompt = self._append_html_fetching_plain_text(prompt)
1727 self._prompt_html = prompt
1728 self._prompt_html = prompt
1728 else:
1729 else:
1729 self._append_plain_text(prompt)
1730 self._append_plain_text(prompt)
1730 self._prompt = prompt
1731 self._prompt = prompt
1731 self._prompt_html = None
1732 self._prompt_html = None
1732
1733
1733 self._prompt_pos = self._get_end_cursor().position()
1734 self._prompt_pos = self._get_end_cursor().position()
1734 self._prompt_started()
1735 self._prompt_started()
1735
1736
1736 #------ Signal handlers ----------------------------------------------------
1737 #------ Signal handlers ----------------------------------------------------
1737
1738
1738 def _adjust_scrollbars(self):
1739 def _adjust_scrollbars(self):
1739 """ Expands the vertical scrollbar beyond the range set by Qt.
1740 """ Expands the vertical scrollbar beyond the range set by Qt.
1740 """
1741 """
1741 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1742 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1742 # and qtextedit.cpp.
1743 # and qtextedit.cpp.
1743 document = self._control.document()
1744 document = self._control.document()
1744 scrollbar = self._control.verticalScrollBar()
1745 scrollbar = self._control.verticalScrollBar()
1745 viewport_height = self._control.viewport().height()
1746 viewport_height = self._control.viewport().height()
1746 if isinstance(self._control, QtGui.QPlainTextEdit):
1747 if isinstance(self._control, QtGui.QPlainTextEdit):
1747 maximum = max(0, document.lineCount() - 1)
1748 maximum = max(0, document.lineCount() - 1)
1748 step = viewport_height / self._control.fontMetrics().lineSpacing()
1749 step = viewport_height / self._control.fontMetrics().lineSpacing()
1749 else:
1750 else:
1750 # QTextEdit does not do line-based layout and blocks will not in
1751 # QTextEdit does not do line-based layout and blocks will not in
1751 # general have the same height. Therefore it does not make sense to
1752 # general have the same height. Therefore it does not make sense to
1752 # attempt to scroll in line height increments.
1753 # attempt to scroll in line height increments.
1753 maximum = document.size().height()
1754 maximum = document.size().height()
1754 step = viewport_height
1755 step = viewport_height
1755 diff = maximum - scrollbar.maximum()
1756 diff = maximum - scrollbar.maximum()
1756 scrollbar.setRange(0, maximum)
1757 scrollbar.setRange(0, maximum)
1757 scrollbar.setPageStep(step)
1758 scrollbar.setPageStep(step)
1758
1759
1759 # Compensate for undesirable scrolling that occurs automatically due to
1760 # Compensate for undesirable scrolling that occurs automatically due to
1760 # maximumBlockCount() text truncation.
1761 # maximumBlockCount() text truncation.
1761 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1762 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1762 scrollbar.setValue(scrollbar.value() + diff)
1763 scrollbar.setValue(scrollbar.value() + diff)
1763
1764
1764 def _cursor_position_changed(self):
1765 def _cursor_position_changed(self):
1765 """ Clears the temporary buffer based on the cursor position.
1766 """ Clears the temporary buffer based on the cursor position.
1766 """
1767 """
1767 if self._text_completing_pos:
1768 if self._text_completing_pos:
1768 document = self._control.document()
1769 document = self._control.document()
1769 if self._text_completing_pos < document.characterCount():
1770 if self._text_completing_pos < document.characterCount():
1770 cursor = self._control.textCursor()
1771 cursor = self._control.textCursor()
1771 pos = cursor.position()
1772 pos = cursor.position()
1772 text_cursor = self._control.textCursor()
1773 text_cursor = self._control.textCursor()
1773 text_cursor.setPosition(self._text_completing_pos)
1774 text_cursor.setPosition(self._text_completing_pos)
1774 if pos < self._text_completing_pos or \
1775 if pos < self._text_completing_pos or \
1775 cursor.blockNumber() > text_cursor.blockNumber():
1776 cursor.blockNumber() > text_cursor.blockNumber():
1776 self._clear_temporary_buffer()
1777 self._clear_temporary_buffer()
1777 self._text_completing_pos = 0
1778 self._text_completing_pos = 0
1778 else:
1779 else:
1779 self._clear_temporary_buffer()
1780 self._clear_temporary_buffer()
1780 self._text_completing_pos = 0
1781 self._text_completing_pos = 0
1781
1782
1782 def _custom_context_menu_requested(self, pos):
1783 def _custom_context_menu_requested(self, pos):
1783 """ Shows a context menu at the given QPoint (in widget coordinates).
1784 """ Shows a context menu at the given QPoint (in widget coordinates).
1784 """
1785 """
1785 menu = self._context_menu_make(pos)
1786 menu = self._context_menu_make(pos)
1786 menu.exec_(self._control.mapToGlobal(pos))
1787 menu.exec_(self._control.mapToGlobal(pos))
General Comments 0
You need to be logged in to leave comments. Login now