##// END OF EJS Templates
* Ctrl-G and Escape now clear the input buffer....
epatters -
Show More
@@ -1,1502 +1,1520 b''
1 # Standard library imports
1 # Standard library imports
2 from os.path import commonprefix
2 from os.path import commonprefix
3 import re
3 import re
4 import sys
4 import sys
5 from textwrap import dedent
5 from textwrap import dedent
6
6
7 # System library imports
7 # System library imports
8 from PyQt4 import QtCore, QtGui
8 from PyQt4 import QtCore, QtGui
9
9
10 # Local imports
10 # Local imports
11 from IPython.config.configurable import Configurable
11 from IPython.config.configurable import Configurable
12 from IPython.frontend.qt.util import MetaQObjectHasTraits
12 from IPython.frontend.qt.util import MetaQObjectHasTraits
13 from IPython.utils.traitlets import Bool, Enum, Int
13 from IPython.utils.traitlets import Bool, Enum, Int
14 from ansi_code_processor import QtAnsiCodeProcessor
14 from ansi_code_processor import QtAnsiCodeProcessor
15 from completion_widget import CompletionWidget
15 from completion_widget import CompletionWidget
16
16
17
17
18 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
18 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
19 """ A QPlainTextEdit suitable for use with ConsoleWidget.
19 """ A QPlainTextEdit suitable for use with ConsoleWidget.
20 """
20 """
21 # Prevents text from being moved by drag and drop. Note that is not, for
21 # Prevents text from being moved by drag and drop. Note that is not, for
22 # some reason, sufficient to catch drag events in the ConsoleWidget's
22 # some reason, sufficient to catch drag events in the ConsoleWidget's
23 # event filter.
23 # event filter.
24 def dragEnterEvent(self, event): pass
24 def dragEnterEvent(self, event): pass
25 def dragLeaveEvent(self, event): pass
25 def dragLeaveEvent(self, event): pass
26 def dragMoveEvent(self, event): pass
26 def dragMoveEvent(self, event): pass
27 def dropEvent(self, event): pass
27 def dropEvent(self, event): pass
28
28
29 class ConsoleTextEdit(QtGui.QTextEdit):
29 class ConsoleTextEdit(QtGui.QTextEdit):
30 """ A QTextEdit suitable for use with ConsoleWidget.
30 """ A QTextEdit suitable for use with ConsoleWidget.
31 """
31 """
32 # See above.
32 # See above.
33 def dragEnterEvent(self, event): pass
33 def dragEnterEvent(self, event): pass
34 def dragLeaveEvent(self, event): pass
34 def dragLeaveEvent(self, event): pass
35 def dragMoveEvent(self, event): pass
35 def dragMoveEvent(self, event): pass
36 def dropEvent(self, event): pass
36 def dropEvent(self, event): pass
37
37
38
38
39 class ConsoleWidget(Configurable, QtGui.QWidget):
39 class ConsoleWidget(Configurable, QtGui.QWidget):
40 """ An abstract base class for console-type widgets. This class has
40 """ An abstract base class for console-type widgets. This class has
41 functionality for:
41 functionality for:
42
42
43 * Maintaining a prompt and editing region
43 * Maintaining a prompt and editing region
44 * Providing the traditional Unix-style console keyboard shortcuts
44 * Providing the traditional Unix-style console keyboard shortcuts
45 * Performing tab completion
45 * Performing tab completion
46 * Paging text
46 * Paging text
47 * Handling ANSI escape codes
47 * Handling ANSI escape codes
48
48
49 ConsoleWidget also provides a number of utility methods that will be
49 ConsoleWidget also provides a number of utility methods that will be
50 convenient to implementors of a console-style widget.
50 convenient to implementors of a console-style widget.
51 """
51 """
52 __metaclass__ = MetaQObjectHasTraits
52 __metaclass__ = MetaQObjectHasTraits
53
53
54 # Whether to process ANSI escape codes.
54 # Whether to process ANSI escape codes.
55 ansi_codes = Bool(True, config=True)
55 ansi_codes = Bool(True, config=True)
56
56
57 # The maximum number of lines of text before truncation. Specifying a
57 # The maximum number of lines of text before truncation. Specifying a
58 # non-positive number disables text truncation (not recommended).
58 # non-positive number disables text truncation (not recommended).
59 buffer_size = Int(500, config=True)
59 buffer_size = Int(500, config=True)
60
60
61 # Whether to use a list widget or plain text output for tab completion.
61 # Whether to use a list widget or plain text output for tab completion.
62 gui_completion = Bool(False, config=True)
62 gui_completion = Bool(False, config=True)
63
63
64 # The type of underlying text widget to use. Valid values are 'plain', which
64 # The type of underlying text widget to use. Valid values are 'plain', which
65 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
65 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
66 # NOTE: this value can only be specified during initialization.
66 # NOTE: this value can only be specified during initialization.
67 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
67 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
68
68
69 # The type of paging to use. Valid values are:
69 # The type of paging to use. Valid values are:
70 # 'inside' : The widget pages like a traditional terminal pager.
70 # 'inside' : The widget pages like a traditional terminal pager.
71 # 'hsplit' : When paging is requested, the widget is split
71 # 'hsplit' : When paging is requested, the widget is split
72 # horizontally. The top pane contains the console, and the
72 # horizontally. The top pane contains the console, and the
73 # bottom pane contains the paged text.
73 # bottom pane contains the paged text.
74 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
74 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
75 # 'custom' : No action is taken by the widget beyond emitting a
75 # 'custom' : No action is taken by the widget beyond emitting a
76 # 'custom_page_requested(str)' signal.
76 # 'custom_page_requested(str)' signal.
77 # 'none' : The text is written directly to the console.
77 # 'none' : The text is written directly to the console.
78 # NOTE: this value can only be specified during initialization.
78 # NOTE: this value can only be specified during initialization.
79 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
79 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
80 default_value='inside', config=True)
80 default_value='inside', config=True)
81
81
82 # Whether to override ShortcutEvents for the keybindings defined by this
82 # Whether to override ShortcutEvents for the keybindings defined by this
83 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
83 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
84 # priority (when it has focus) over, e.g., window-level menu shortcuts.
84 # priority (when it has focus) over, e.g., window-level menu shortcuts.
85 override_shortcuts = Bool(False)
85 override_shortcuts = Bool(False)
86
86
87 # Signals that indicate ConsoleWidget state.
87 # Signals that indicate ConsoleWidget state.
88 copy_available = QtCore.pyqtSignal(bool)
88 copy_available = QtCore.pyqtSignal(bool)
89 redo_available = QtCore.pyqtSignal(bool)
89 redo_available = QtCore.pyqtSignal(bool)
90 undo_available = QtCore.pyqtSignal(bool)
90 undo_available = QtCore.pyqtSignal(bool)
91
91
92 # Signal emitted when paging is needed and the paging style has been
92 # Signal emitted when paging is needed and the paging style has been
93 # specified as 'custom'.
93 # specified as 'custom'.
94 custom_page_requested = QtCore.pyqtSignal(object)
94 custom_page_requested = QtCore.pyqtSignal(object)
95
95
96 # Protected class variables.
96 # Protected class variables.
97 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
97 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
98 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
98 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
99 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
99 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
100 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
100 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
101 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
101 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
102 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
102 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
103 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
103 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
104 _shortcuts = set(_ctrl_down_remap.keys() +
104 _shortcuts = set(_ctrl_down_remap.keys() +
105 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V, QtCore.Qt.Key_O ])
105 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
106 QtCore.Qt.Key_V ])
106
107
107 #---------------------------------------------------------------------------
108 #---------------------------------------------------------------------------
108 # 'QObject' interface
109 # 'QObject' interface
109 #---------------------------------------------------------------------------
110 #---------------------------------------------------------------------------
110
111
111 def __init__(self, parent=None, **kw):
112 def __init__(self, parent=None, **kw):
112 """ Create a ConsoleWidget.
113 """ Create a ConsoleWidget.
113
114
114 Parameters:
115 Parameters:
115 -----------
116 -----------
116 parent : QWidget, optional [default None]
117 parent : QWidget, optional [default None]
117 The parent for this widget.
118 The parent for this widget.
118 """
119 """
119 QtGui.QWidget.__init__(self, parent)
120 QtGui.QWidget.__init__(self, parent)
120 Configurable.__init__(self, **kw)
121 Configurable.__init__(self, **kw)
121
122
122 # Create the layout and underlying text widget.
123 # Create the layout and underlying text widget.
123 layout = QtGui.QStackedLayout(self)
124 layout = QtGui.QStackedLayout(self)
124 layout.setContentsMargins(0, 0, 0, 0)
125 layout.setContentsMargins(0, 0, 0, 0)
125 self._control = self._create_control()
126 self._control = self._create_control()
126 self._page_control = None
127 self._page_control = None
127 self._splitter = None
128 self._splitter = None
128 if self.paging in ('hsplit', 'vsplit'):
129 if self.paging in ('hsplit', 'vsplit'):
129 self._splitter = QtGui.QSplitter()
130 self._splitter = QtGui.QSplitter()
130 if self.paging == 'hsplit':
131 if self.paging == 'hsplit':
131 self._splitter.setOrientation(QtCore.Qt.Horizontal)
132 self._splitter.setOrientation(QtCore.Qt.Horizontal)
132 else:
133 else:
133 self._splitter.setOrientation(QtCore.Qt.Vertical)
134 self._splitter.setOrientation(QtCore.Qt.Vertical)
134 self._splitter.addWidget(self._control)
135 self._splitter.addWidget(self._control)
135 layout.addWidget(self._splitter)
136 layout.addWidget(self._splitter)
136 else:
137 else:
137 layout.addWidget(self._control)
138 layout.addWidget(self._control)
138
139
139 # Create the paging widget, if necessary.
140 # Create the paging widget, if necessary.
140 if self.paging in ('inside', 'hsplit', 'vsplit'):
141 if self.paging in ('inside', 'hsplit', 'vsplit'):
141 self._page_control = self._create_page_control()
142 self._page_control = self._create_page_control()
142 if self._splitter:
143 if self._splitter:
143 self._page_control.hide()
144 self._page_control.hide()
144 self._splitter.addWidget(self._page_control)
145 self._splitter.addWidget(self._page_control)
145 else:
146 else:
146 layout.addWidget(self._page_control)
147 layout.addWidget(self._page_control)
147
148
148 # Initialize protected variables. Some variables contain useful state
149 # Initialize protected variables. Some variables contain useful state
149 # information for subclasses; they should be considered read-only.
150 # information for subclasses; they should be considered read-only.
150 self._ansi_processor = QtAnsiCodeProcessor()
151 self._ansi_processor = QtAnsiCodeProcessor()
151 self._completion_widget = CompletionWidget(self._control)
152 self._completion_widget = CompletionWidget(self._control)
152 self._continuation_prompt = '> '
153 self._continuation_prompt = '> '
153 self._continuation_prompt_html = None
154 self._continuation_prompt_html = None
154 self._executing = False
155 self._executing = False
155 self._prompt = ''
156 self._prompt = ''
156 self._prompt_html = None
157 self._prompt_html = None
157 self._prompt_pos = 0
158 self._prompt_pos = 0
158 self._prompt_sep = ''
159 self._prompt_sep = ''
159 self._reading = False
160 self._reading = False
160 self._reading_callback = None
161 self._reading_callback = None
161 self._tab_width = 8
162 self._tab_width = 8
162 self._text_completing_pos = 0
163 self._text_completing_pos = 0
163
164
164 # Set a monospaced font.
165 # Set a monospaced font.
165 self.reset_font()
166 self.reset_font()
166
167
167 def eventFilter(self, obj, event):
168 def eventFilter(self, obj, event):
168 """ Reimplemented to ensure a console-like behavior in the underlying
169 """ Reimplemented to ensure a console-like behavior in the underlying
169 text widgets.
170 text widgets.
170 """
171 """
171 etype = event.type()
172 etype = event.type()
172 if etype == QtCore.QEvent.KeyPress:
173 if etype == QtCore.QEvent.KeyPress:
173
174
174 # Re-map keys for all filtered widgets.
175 # Re-map keys for all filtered widgets.
175 key = event.key()
176 key = event.key()
176 if self._control_key_down(event.modifiers()) and \
177 if self._control_key_down(event.modifiers()) and \
177 key in self._ctrl_down_remap:
178 key in self._ctrl_down_remap:
178 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
179 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
179 self._ctrl_down_remap[key],
180 self._ctrl_down_remap[key],
180 QtCore.Qt.NoModifier)
181 QtCore.Qt.NoModifier)
181 QtGui.qApp.sendEvent(obj, new_event)
182 QtGui.qApp.sendEvent(obj, new_event)
182 return True
183 return True
183
184
184 elif obj == self._control:
185 elif obj == self._control:
185 return self._event_filter_console_keypress(event)
186 return self._event_filter_console_keypress(event)
186
187
187 elif obj == self._page_control:
188 elif obj == self._page_control:
188 return self._event_filter_page_keypress(event)
189 return self._event_filter_page_keypress(event)
189
190
190 # Override shortucts for all filtered widgets. Note that on Mac OS it is
191 # Override shortucts for all filtered widgets. Note that on Mac OS it is
191 # always unnecessary to override shortcuts, hence the check below (users
192 # always unnecessary to override shortcuts, hence the check below (users
192 # should just use the Control key instead of the Command key).
193 # should just use the Control key instead of the Command key).
193 elif etype == QtCore.QEvent.ShortcutOverride and \
194 elif etype == QtCore.QEvent.ShortcutOverride and \
194 sys.platform != 'darwin' and \
195 sys.platform != 'darwin' and \
195 self._control_key_down(event.modifiers()) and \
196 self._control_key_down(event.modifiers()) and \
196 event.key() in self._shortcuts:
197 event.key() in self._shortcuts:
197 event.accept()
198 event.accept()
198 return False
199 return False
199
200
200 return super(ConsoleWidget, self).eventFilter(obj, event)
201 return super(ConsoleWidget, self).eventFilter(obj, event)
201
202
202 #---------------------------------------------------------------------------
203 #---------------------------------------------------------------------------
203 # 'QWidget' interface
204 # 'QWidget' interface
204 #---------------------------------------------------------------------------
205 #---------------------------------------------------------------------------
205
206
206 def sizeHint(self):
207 def sizeHint(self):
207 """ Reimplemented to suggest a size that is 80 characters wide and
208 """ Reimplemented to suggest a size that is 80 characters wide and
208 25 lines high.
209 25 lines high.
209 """
210 """
210 font_metrics = QtGui.QFontMetrics(self.font)
211 font_metrics = QtGui.QFontMetrics(self.font)
211 margin = (self._control.frameWidth() +
212 margin = (self._control.frameWidth() +
212 self._control.document().documentMargin()) * 2
213 self._control.document().documentMargin()) * 2
213 style = self.style()
214 style = self.style()
214 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
215 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
215
216
216 # Despite my best efforts to take the various margins into account, the
217 # Despite my best efforts to take the various margins into account, the
217 # width is still coming out a bit too small, so we include a fudge
218 # width is still coming out a bit too small, so we include a fudge
218 # factor of one character here.
219 # factor of one character here.
219 width = font_metrics.maxWidth() * 81 + margin
220 width = font_metrics.maxWidth() * 81 + margin
220 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
221 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
221 if self.paging == 'hsplit':
222 if self.paging == 'hsplit':
222 width = width * 2 + splitwidth
223 width = width * 2 + splitwidth
223
224
224 height = font_metrics.height() * 25 + margin
225 height = font_metrics.height() * 25 + margin
225 if self.paging == 'vsplit':
226 if self.paging == 'vsplit':
226 height = height * 2 + splitwidth
227 height = height * 2 + splitwidth
227
228
228 return QtCore.QSize(width, height)
229 return QtCore.QSize(width, height)
229
230
230 #---------------------------------------------------------------------------
231 #---------------------------------------------------------------------------
231 # 'ConsoleWidget' public interface
232 # 'ConsoleWidget' public interface
232 #---------------------------------------------------------------------------
233 #---------------------------------------------------------------------------
233
234
234 def can_paste(self):
235 def can_paste(self):
235 """ Returns whether text can be pasted from the clipboard.
236 """ Returns whether text can be pasted from the clipboard.
236 """
237 """
237 # Only accept text that can be ASCII encoded.
238 # Only accept text that can be ASCII encoded.
238 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
239 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
239 text = QtGui.QApplication.clipboard().text()
240 text = QtGui.QApplication.clipboard().text()
240 if not text.isEmpty():
241 if not text.isEmpty():
241 try:
242 try:
242 str(text)
243 str(text)
243 return True
244 return True
244 except UnicodeEncodeError:
245 except UnicodeEncodeError:
245 pass
246 pass
246 return False
247 return False
247
248
248 def clear(self, keep_input=True):
249 def clear(self, keep_input=True):
249 """ Clear the console, then write a new prompt. If 'keep_input' is set,
250 """ Clear the console, then write a new prompt. If 'keep_input' is set,
250 restores the old input buffer when the new prompt is written.
251 restores the old input buffer when the new prompt is written.
251 """
252 """
252 if keep_input:
253 if keep_input:
253 input_buffer = self.input_buffer
254 input_buffer = self.input_buffer
254 self._control.clear()
255 self._control.clear()
255 self._show_prompt()
256 self._show_prompt()
256 if keep_input:
257 if keep_input:
257 self.input_buffer = input_buffer
258 self.input_buffer = input_buffer
258
259
259 def copy(self):
260 def copy(self):
260 """ Copy the current selected text to the clipboard.
261 """ Copy the current selected text to the clipboard.
261 """
262 """
262 self._control.copy()
263 self._control.copy()
263
264
264 def execute(self, source=None, hidden=False, interactive=False):
265 def execute(self, source=None, hidden=False, interactive=False):
265 """ Executes source or the input buffer, possibly prompting for more
266 """ Executes source or the input buffer, possibly prompting for more
266 input.
267 input.
267
268
268 Parameters:
269 Parameters:
269 -----------
270 -----------
270 source : str, optional
271 source : str, optional
271
272
272 The source to execute. If not specified, the input buffer will be
273 The source to execute. If not specified, the input buffer will be
273 used. If specified and 'hidden' is False, the input buffer will be
274 used. If specified and 'hidden' is False, the input buffer will be
274 replaced with the source before execution.
275 replaced with the source before execution.
275
276
276 hidden : bool, optional (default False)
277 hidden : bool, optional (default False)
277
278
278 If set, no output will be shown and the prompt will not be modified.
279 If set, no output will be shown and the prompt will not be modified.
279 In other words, it will be completely invisible to the user that
280 In other words, it will be completely invisible to the user that
280 an execution has occurred.
281 an execution has occurred.
281
282
282 interactive : bool, optional (default False)
283 interactive : bool, optional (default False)
283
284
284 Whether the console is to treat the source as having been manually
285 Whether the console is to treat the source as having been manually
285 entered by the user. The effect of this parameter depends on the
286 entered by the user. The effect of this parameter depends on the
286 subclass implementation.
287 subclass implementation.
287
288
288 Raises:
289 Raises:
289 -------
290 -------
290 RuntimeError
291 RuntimeError
291 If incomplete input is given and 'hidden' is True. In this case,
292 If incomplete input is given and 'hidden' is True. In this case,
292 it is not possible to prompt for more input.
293 it is not possible to prompt for more input.
293
294
294 Returns:
295 Returns:
295 --------
296 --------
296 A boolean indicating whether the source was executed.
297 A boolean indicating whether the source was executed.
297 """
298 """
298 # WARNING: The order in which things happen here is very particular, in
299 # WARNING: The order in which things happen here is very particular, in
299 # large part because our syntax highlighting is fragile. If you change
300 # large part because our syntax highlighting is fragile. If you change
300 # something, test carefully!
301 # something, test carefully!
301
302
302 # Decide what to execute.
303 # Decide what to execute.
303 if source is None:
304 if source is None:
304 source = self.input_buffer
305 source = self.input_buffer
305 if not hidden:
306 if not hidden:
306 # A newline is appended later, but it should be considered part
307 # A newline is appended later, but it should be considered part
307 # of the input buffer.
308 # of the input buffer.
308 source += '\n'
309 source += '\n'
309 elif not hidden:
310 elif not hidden:
310 self.input_buffer = source
311 self.input_buffer = source
311
312
312 # Execute the source or show a continuation prompt if it is incomplete.
313 # Execute the source or show a continuation prompt if it is incomplete.
313 complete = self._is_complete(source, interactive)
314 complete = self._is_complete(source, interactive)
314 if hidden:
315 if hidden:
315 if complete:
316 if complete:
316 self._execute(source, hidden)
317 self._execute(source, hidden)
317 else:
318 else:
318 error = 'Incomplete noninteractive input: "%s"'
319 error = 'Incomplete noninteractive input: "%s"'
319 raise RuntimeError(error % source)
320 raise RuntimeError(error % source)
320 else:
321 else:
321 if complete:
322 if complete:
322 self._append_plain_text('\n')
323 self._append_plain_text('\n')
323 self._executing_input_buffer = self.input_buffer
324 self._executing_input_buffer = self.input_buffer
324 self._executing = True
325 self._executing = True
325 self._prompt_finished()
326 self._prompt_finished()
326
327
327 # The maximum block count is only in effect during execution.
328 # The maximum block count is only in effect during execution.
328 # This ensures that _prompt_pos does not become invalid due to
329 # This ensures that _prompt_pos does not become invalid due to
329 # text truncation.
330 # text truncation.
330 self._control.document().setMaximumBlockCount(self.buffer_size)
331 self._control.document().setMaximumBlockCount(self.buffer_size)
331
332
332 # Setting a positive maximum block count will automatically
333 # Setting a positive maximum block count will automatically
333 # disable the undo/redo history, but just to be safe:
334 # disable the undo/redo history, but just to be safe:
334 self._control.setUndoRedoEnabled(False)
335 self._control.setUndoRedoEnabled(False)
335
336
336 self._execute(source, hidden)
337 self._execute(source, hidden)
337
338
338 else:
339 else:
339 # Do this inside an edit block so continuation prompts are
340 # Do this inside an edit block so continuation prompts are
340 # removed seamlessly via undo/redo.
341 # removed seamlessly via undo/redo.
341 cursor = self._get_end_cursor()
342 cursor = self._get_end_cursor()
342 cursor.beginEditBlock()
343 cursor.beginEditBlock()
343 cursor.insertText('\n')
344 cursor.insertText('\n')
344 self._insert_continuation_prompt(cursor)
345 self._insert_continuation_prompt(cursor)
345 cursor.endEditBlock()
346 cursor.endEditBlock()
346
347
347 # Do not do this inside the edit block. It works as expected
348 # Do not do this inside the edit block. It works as expected
348 # when using a QPlainTextEdit control, but does not have an
349 # when using a QPlainTextEdit control, but does not have an
349 # effect when using a QTextEdit. I believe this is a Qt bug.
350 # effect when using a QTextEdit. I believe this is a Qt bug.
350 self._control.moveCursor(QtGui.QTextCursor.End)
351 self._control.moveCursor(QtGui.QTextCursor.End)
351
352
352 return complete
353 return complete
353
354
354 def _get_input_buffer(self):
355 def _get_input_buffer(self):
355 """ The text that the user has entered entered at the current prompt.
356 """ The text that the user has entered entered at the current prompt.
356 """
357 """
357 # If we're executing, the input buffer may not even exist anymore due to
358 # If we're executing, the input buffer may not even exist anymore due to
358 # the limit imposed by 'buffer_size'. Therefore, we store it.
359 # the limit imposed by 'buffer_size'. Therefore, we store it.
359 if self._executing:
360 if self._executing:
360 return self._executing_input_buffer
361 return self._executing_input_buffer
361
362
362 cursor = self._get_end_cursor()
363 cursor = self._get_end_cursor()
363 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
364 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
364 input_buffer = str(cursor.selection().toPlainText())
365 input_buffer = str(cursor.selection().toPlainText())
365
366
366 # Strip out continuation prompts.
367 # Strip out continuation prompts.
367 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
368 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
368
369
369 def _set_input_buffer(self, string):
370 def _set_input_buffer(self, string):
370 """ Replaces the text in the input buffer with 'string'.
371 """ Replaces the text in the input buffer with 'string'.
371 """
372 """
372 # For now, it is an error to modify the input buffer during execution.
373 # For now, it is an error to modify the input buffer during execution.
373 if self._executing:
374 if self._executing:
374 raise RuntimeError("Cannot change input buffer during execution.")
375 raise RuntimeError("Cannot change input buffer during execution.")
375
376
376 # Remove old text.
377 # Remove old text.
377 cursor = self._get_end_cursor()
378 cursor = self._get_end_cursor()
378 cursor.beginEditBlock()
379 cursor.beginEditBlock()
379 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
380 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
380 cursor.removeSelectedText()
381 cursor.removeSelectedText()
381
382
382 # Insert new text with continuation prompts.
383 # Insert new text with continuation prompts.
383 lines = string.splitlines(True)
384 lines = string.splitlines(True)
384 if lines:
385 if lines:
385 self._append_plain_text(lines[0])
386 self._append_plain_text(lines[0])
386 for i in xrange(1, len(lines)):
387 for i in xrange(1, len(lines)):
387 if self._continuation_prompt_html is None:
388 if self._continuation_prompt_html is None:
388 self._append_plain_text(self._continuation_prompt)
389 self._append_plain_text(self._continuation_prompt)
389 else:
390 else:
390 self._append_html(self._continuation_prompt_html)
391 self._append_html(self._continuation_prompt_html)
391 self._append_plain_text(lines[i])
392 self._append_plain_text(lines[i])
392 cursor.endEditBlock()
393 cursor.endEditBlock()
393 self._control.moveCursor(QtGui.QTextCursor.End)
394 self._control.moveCursor(QtGui.QTextCursor.End)
394
395
395 input_buffer = property(_get_input_buffer, _set_input_buffer)
396 input_buffer = property(_get_input_buffer, _set_input_buffer)
396
397
397 def _get_font(self):
398 def _get_font(self):
398 """ The base font being used by the ConsoleWidget.
399 """ The base font being used by the ConsoleWidget.
399 """
400 """
400 return self._control.document().defaultFont()
401 return self._control.document().defaultFont()
401
402
402 def _set_font(self, font):
403 def _set_font(self, font):
403 """ Sets the base font for the ConsoleWidget to the specified QFont.
404 """ Sets the base font for the ConsoleWidget to the specified QFont.
404 """
405 """
405 font_metrics = QtGui.QFontMetrics(font)
406 font_metrics = QtGui.QFontMetrics(font)
406 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
407 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
407
408
408 self._completion_widget.setFont(font)
409 self._completion_widget.setFont(font)
409 self._control.document().setDefaultFont(font)
410 self._control.document().setDefaultFont(font)
410 if self._page_control:
411 if self._page_control:
411 self._page_control.document().setDefaultFont(font)
412 self._page_control.document().setDefaultFont(font)
412
413
413 font = property(_get_font, _set_font)
414 font = property(_get_font, _set_font)
414
415
415 def paste(self):
416 def paste(self):
416 """ Paste the contents of the clipboard into the input region.
417 """ Paste the contents of the clipboard into the input region.
417 """
418 """
418 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
419 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
419 try:
420 try:
420 text = str(QtGui.QApplication.clipboard().text())
421 text = str(QtGui.QApplication.clipboard().text())
421 except UnicodeEncodeError:
422 except UnicodeEncodeError:
422 pass
423 pass
423 else:
424 else:
424 self._insert_plain_text_into_buffer(dedent(text))
425 self._insert_plain_text_into_buffer(dedent(text))
425
426
426 def print_(self, printer):
427 def print_(self, printer):
427 """ Print the contents of the ConsoleWidget to the specified QPrinter.
428 """ Print the contents of the ConsoleWidget to the specified QPrinter.
428 """
429 """
429 self._control.print_(printer)
430 self._control.print_(printer)
430
431
431 def redo(self):
432 def redo(self):
432 """ Redo the last operation. If there is no operation to redo, nothing
433 """ Redo the last operation. If there is no operation to redo, nothing
433 happens.
434 happens.
434 """
435 """
435 self._control.redo()
436 self._control.redo()
436
437
437 def reset_font(self):
438 def reset_font(self):
438 """ Sets the font to the default fixed-width font for this platform.
439 """ Sets the font to the default fixed-width font for this platform.
439 """
440 """
440 if sys.platform == 'win32':
441 if sys.platform == 'win32':
441 # FIXME: we should test whether Consolas is available and use it
442 # FIXME: we should test whether Consolas is available and use it
442 # first if it is. Consolas ships by default from Vista onwards,
443 # first if it is. Consolas ships by default from Vista onwards,
443 # it's *vastly* more readable and prettier than Courier, and is
444 # it's *vastly* more readable and prettier than Courier, and is
444 # often installed even on XP systems. So we should first check for
445 # often installed even on XP systems. So we should first check for
445 # it, and only fallback to Courier if absolutely necessary.
446 # it, and only fallback to Courier if absolutely necessary.
446 name = 'Courier'
447 name = 'Courier'
447 elif sys.platform == 'darwin':
448 elif sys.platform == 'darwin':
448 name = 'Monaco'
449 name = 'Monaco'
449 else:
450 else:
450 name = 'Monospace'
451 name = 'Monospace'
451 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
452 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
452 font.setStyleHint(QtGui.QFont.TypeWriter)
453 font.setStyleHint(QtGui.QFont.TypeWriter)
453 self._set_font(font)
454 self._set_font(font)
454
455
455 def select_all(self):
456 def select_all(self):
456 """ Selects all the text in the buffer.
457 """ Selects all the text in the buffer.
457 """
458 """
458 self._control.selectAll()
459 self._control.selectAll()
459
460
460 def _get_tab_width(self):
461 def _get_tab_width(self):
461 """ The width (in terms of space characters) for tab characters.
462 """ The width (in terms of space characters) for tab characters.
462 """
463 """
463 return self._tab_width
464 return self._tab_width
464
465
465 def _set_tab_width(self, tab_width):
466 def _set_tab_width(self, tab_width):
466 """ Sets the width (in terms of space characters) for tab characters.
467 """ Sets the width (in terms of space characters) for tab characters.
467 """
468 """
468 font_metrics = QtGui.QFontMetrics(self.font)
469 font_metrics = QtGui.QFontMetrics(self.font)
469 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
470 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
470
471
471 self._tab_width = tab_width
472 self._tab_width = tab_width
472
473
473 tab_width = property(_get_tab_width, _set_tab_width)
474 tab_width = property(_get_tab_width, _set_tab_width)
474
475
475 def undo(self):
476 def undo(self):
476 """ Undo the last operation. If there is no operation to undo, nothing
477 """ Undo the last operation. If there is no operation to undo, nothing
477 happens.
478 happens.
478 """
479 """
479 self._control.undo()
480 self._control.undo()
480
481
481 #---------------------------------------------------------------------------
482 #---------------------------------------------------------------------------
482 # 'ConsoleWidget' abstract interface
483 # 'ConsoleWidget' abstract interface
483 #---------------------------------------------------------------------------
484 #---------------------------------------------------------------------------
484
485
485 def _is_complete(self, source, interactive):
486 def _is_complete(self, source, interactive):
486 """ Returns whether 'source' can be executed. When triggered by an
487 """ Returns whether 'source' can be executed. When triggered by an
487 Enter/Return key press, 'interactive' is True; otherwise, it is
488 Enter/Return key press, 'interactive' is True; otherwise, it is
488 False.
489 False.
489 """
490 """
490 raise NotImplementedError
491 raise NotImplementedError
491
492
492 def _execute(self, source, hidden):
493 def _execute(self, source, hidden):
493 """ Execute 'source'. If 'hidden', do not show any output.
494 """ Execute 'source'. If 'hidden', do not show any output.
494 """
495 """
495 raise NotImplementedError
496 raise NotImplementedError
496
497
497 def _prompt_started_hook(self):
498 def _prompt_started_hook(self):
498 """ Called immediately after a new prompt is displayed.
499 """ Called immediately after a new prompt is displayed.
499 """
500 """
500 pass
501 pass
501
502
502 def _prompt_finished_hook(self):
503 def _prompt_finished_hook(self):
503 """ Called immediately after a prompt is finished, i.e. when some input
504 """ Called immediately after a prompt is finished, i.e. when some input
504 will be processed and a new prompt displayed.
505 will be processed and a new prompt displayed.
505 """
506 """
506 pass
507 pass
507
508
508 def _up_pressed(self):
509 def _up_pressed(self):
509 """ Called when the up key is pressed. Returns whether to continue
510 """ Called when the up key is pressed. Returns whether to continue
510 processing the event.
511 processing the event.
511 """
512 """
512 return True
513 return True
513
514
514 def _down_pressed(self):
515 def _down_pressed(self):
515 """ Called when the down key is pressed. Returns whether to continue
516 """ Called when the down key is pressed. Returns whether to continue
516 processing the event.
517 processing the event.
517 """
518 """
518 return True
519 return True
519
520
520 def _tab_pressed(self):
521 def _tab_pressed(self):
521 """ Called when the tab key is pressed. Returns whether to continue
522 """ Called when the tab key is pressed. Returns whether to continue
522 processing the event.
523 processing the event.
523 """
524 """
524 return False
525 return False
525
526
526 #--------------------------------------------------------------------------
527 #--------------------------------------------------------------------------
527 # 'ConsoleWidget' protected interface
528 # 'ConsoleWidget' protected interface
528 #--------------------------------------------------------------------------
529 #--------------------------------------------------------------------------
529
530
530 def _append_html(self, html):
531 def _append_html(self, html):
531 """ Appends html at the end of the console buffer.
532 """ Appends html at the end of the console buffer.
532 """
533 """
533 cursor = self._get_end_cursor()
534 cursor = self._get_end_cursor()
534 self._insert_html(cursor, html)
535 self._insert_html(cursor, html)
535
536
536 def _append_html_fetching_plain_text(self, html):
537 def _append_html_fetching_plain_text(self, html):
537 """ Appends 'html', then returns the plain text version of it.
538 """ Appends 'html', then returns the plain text version of it.
538 """
539 """
539 cursor = self._get_end_cursor()
540 cursor = self._get_end_cursor()
540 return self._insert_html_fetching_plain_text(cursor, html)
541 return self._insert_html_fetching_plain_text(cursor, html)
541
542
542 def _append_plain_text(self, text):
543 def _append_plain_text(self, text):
543 """ Appends plain text at the end of the console buffer, processing
544 """ Appends plain text at the end of the console buffer, processing
544 ANSI codes if enabled.
545 ANSI codes if enabled.
545 """
546 """
546 cursor = self._get_end_cursor()
547 cursor = self._get_end_cursor()
547 self._insert_plain_text(cursor, text)
548 self._insert_plain_text(cursor, text)
548
549
549 def _append_plain_text_keeping_prompt(self, text):
550 def _append_plain_text_keeping_prompt(self, text):
550 """ Writes 'text' after the current prompt, then restores the old prompt
551 """ Writes 'text' after the current prompt, then restores the old prompt
551 with its old input buffer.
552 with its old input buffer.
552 """
553 """
553 input_buffer = self.input_buffer
554 input_buffer = self.input_buffer
554 self._append_plain_text('\n')
555 self._append_plain_text('\n')
555 self._prompt_finished()
556 self._prompt_finished()
556
557
557 self._append_plain_text(text)
558 self._append_plain_text(text)
558 self._show_prompt()
559 self._show_prompt()
559 self.input_buffer = input_buffer
560 self.input_buffer = input_buffer
560
561
561 def _clear_temporary_buffer(self):
562 def _clear_temporary_buffer(self):
562 """ Clears the "temporary text" buffer, i.e. all the text following
563 """ Clears the "temporary text" buffer, i.e. all the text following
563 the prompt region.
564 the prompt region.
564 """
565 """
565 cursor = self._get_prompt_cursor()
566 cursor = self._get_prompt_cursor()
566 if cursor.movePosition(QtGui.QTextCursor.NextBlock):
567 if cursor.movePosition(QtGui.QTextCursor.NextBlock):
567 prompt = self._continuation_prompt.lstrip()
568 prompt = self._continuation_prompt.lstrip()
568 while True:
569 while True:
569 temp_cursor = QtGui.QTextCursor(cursor)
570 temp_cursor = QtGui.QTextCursor(cursor)
570 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
571 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
571 text = str(temp_cursor.selection().toPlainText()).lstrip()
572 text = str(temp_cursor.selection().toPlainText()).lstrip()
572 if not text.startswith(prompt) or \
573 if not text.startswith(prompt) or \
573 not cursor.movePosition(QtGui.QTextCursor.NextBlock):
574 not cursor.movePosition(QtGui.QTextCursor.NextBlock):
574 break
575 break
575 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
576 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
576 cursor.movePosition(QtGui.QTextCursor.End,
577 cursor.movePosition(QtGui.QTextCursor.End,
577 QtGui.QTextCursor.KeepAnchor)
578 QtGui.QTextCursor.KeepAnchor)
578 cursor.removeSelectedText()
579 cursor.removeSelectedText()
579
580
580 def _complete_with_items(self, cursor, items):
581 def _complete_with_items(self, cursor, items):
581 """ Performs completion with 'items' at the specified cursor location.
582 """ Performs completion with 'items' at the specified cursor location.
582 """
583 """
583 if self._text_completing_pos:
584 if self._text_completing_pos:
584 self._clear_temporary_buffer()
585 self._clear_temporary_buffer()
585 self._text_completing_pos = 0
586 self._text_completing_pos = 0
586
587
587 if len(items) == 1:
588 if len(items) == 1:
588 cursor.setPosition(self._control.textCursor().position(),
589 cursor.setPosition(self._control.textCursor().position(),
589 QtGui.QTextCursor.KeepAnchor)
590 QtGui.QTextCursor.KeepAnchor)
590 cursor.insertText(items[0])
591 cursor.insertText(items[0])
591
592
592 elif len(items) > 1:
593 elif len(items) > 1:
593 current_pos = self._control.textCursor().position()
594 current_pos = self._control.textCursor().position()
594 prefix = commonprefix(items)
595 prefix = commonprefix(items)
595 if prefix:
596 if prefix:
596 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
597 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
597 cursor.insertText(prefix)
598 cursor.insertText(prefix)
598 current_pos = cursor.position()
599 current_pos = cursor.position()
599
600
600 if self.gui_completion:
601 if self.gui_completion:
601 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
602 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
602 self._completion_widget.show_items(cursor, items)
603 self._completion_widget.show_items(cursor, items)
603 else:
604 else:
604 cursor.beginEditBlock()
605 cursor.beginEditBlock()
605 self._append_plain_text('\n')
606 self._append_plain_text('\n')
606 self._page(self._format_as_columns(items))
607 self._page(self._format_as_columns(items))
607 cursor.endEditBlock()
608 cursor.endEditBlock()
608
609
609 cursor.setPosition(current_pos)
610 cursor.setPosition(current_pos)
610 self._control.moveCursor(QtGui.QTextCursor.End)
611 self._control.moveCursor(QtGui.QTextCursor.End)
611 self._control.setTextCursor(cursor)
612 self._control.setTextCursor(cursor)
612 self._text_completing_pos = current_pos
613 self._text_completing_pos = current_pos
613
614
614 def _control_key_down(self, modifiers):
615 def _control_key_down(self, modifiers):
615 """ Given a KeyboardModifiers flags object, return whether the Control
616 """ Given a KeyboardModifiers flags object, return whether the Control
616 key is down (on Mac OS, treat the Command key as a synonym for
617 key is down (on Mac OS, treat the Command key as a synonym for
617 Control).
618 Control).
618 """
619 """
619 down = bool(modifiers & QtCore.Qt.ControlModifier)
620 down = bool(modifiers & QtCore.Qt.ControlModifier)
620
621
621 # Note: on Mac OS, ControlModifier corresponds to the Command key while
622 # Note: on Mac OS, ControlModifier corresponds to the Command key while
622 # MetaModifier corresponds to the Control key.
623 # MetaModifier corresponds to the Control key.
623 if sys.platform == 'darwin':
624 if sys.platform == 'darwin':
624 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
625 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
625
626
626 return down
627 return down
627
628
628 def _create_control(self):
629 def _create_control(self):
629 """ Creates and connects the underlying text widget.
630 """ Creates and connects the underlying text widget.
630 """
631 """
631 if self.kind == 'plain':
632 if self.kind == 'plain':
632 control = ConsolePlainTextEdit()
633 control = ConsolePlainTextEdit()
633 elif self.kind == 'rich':
634 elif self.kind == 'rich':
634 control = ConsoleTextEdit()
635 control = ConsoleTextEdit()
635 control.setAcceptRichText(False)
636 control.setAcceptRichText(False)
636 control.installEventFilter(self)
637 control.installEventFilter(self)
637 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
638 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
638 control.cursorPositionChanged.connect(self._cursor_position_changed)
639 control.cursorPositionChanged.connect(self._cursor_position_changed)
639 control.customContextMenuRequested.connect(self._show_context_menu)
640 control.customContextMenuRequested.connect(self._show_context_menu)
640 control.copyAvailable.connect(self.copy_available)
641 control.copyAvailable.connect(self.copy_available)
641 control.redoAvailable.connect(self.redo_available)
642 control.redoAvailable.connect(self.redo_available)
642 control.undoAvailable.connect(self.undo_available)
643 control.undoAvailable.connect(self.undo_available)
643 control.setReadOnly(True)
644 control.setReadOnly(True)
644 control.setUndoRedoEnabled(False)
645 control.setUndoRedoEnabled(False)
645 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
646 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
646 return control
647 return control
647
648
648 def _create_page_control(self):
649 def _create_page_control(self):
649 """ Creates and connects the underlying paging widget.
650 """ Creates and connects the underlying paging widget.
650 """
651 """
651 control = ConsolePlainTextEdit()
652 control = ConsolePlainTextEdit()
652 control.installEventFilter(self)
653 control.installEventFilter(self)
653 control.setReadOnly(True)
654 control.setReadOnly(True)
654 control.setUndoRedoEnabled(False)
655 control.setUndoRedoEnabled(False)
655 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
656 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
656 return control
657 return control
657
658
658 def _event_filter_console_keypress(self, event):
659 def _event_filter_console_keypress(self, event):
659 """ Filter key events for the underlying text widget to create a
660 """ Filter key events for the underlying text widget to create a
660 console-like interface.
661 console-like interface.
661 """
662 """
662 intercepted = False
663 intercepted = False
663 cursor = self._control.textCursor()
664 cursor = self._control.textCursor()
664 position = cursor.position()
665 position = cursor.position()
665 key = event.key()
666 key = event.key()
666 ctrl_down = self._control_key_down(event.modifiers())
667 ctrl_down = self._control_key_down(event.modifiers())
667 alt_down = event.modifiers() & QtCore.Qt.AltModifier
668 alt_down = event.modifiers() & QtCore.Qt.AltModifier
668 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
669 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
669
670
670 # Special handling when tab completing in text mode:
671 if self._text_completing_pos:
672 if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
673 QtCore.Qt.Key_Escape):
674 self._clear_temporary_buffer()
675 self._text_completing_pos = 0
676
677 if event.matches(QtGui.QKeySequence.Paste):
671 if event.matches(QtGui.QKeySequence.Paste):
678 # Call our paste instead of the underlying text widget's.
672 # Call our paste instead of the underlying text widget's.
679 self.paste()
673 self.paste()
680 intercepted = True
674 intercepted = True
681
675
682 elif ctrl_down:
676 elif ctrl_down:
683 if key == QtCore.Qt.Key_K:
677 if key == QtCore.Qt.Key_G:
678 self._keyboard_quit()
679 intercepted = True
680
681 elif key == QtCore.Qt.Key_K:
684 if self._in_buffer(position):
682 if self._in_buffer(position):
685 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
683 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
686 QtGui.QTextCursor.KeepAnchor)
684 QtGui.QTextCursor.KeepAnchor)
687 if not cursor.hasSelection():
685 if not cursor.hasSelection():
688 # Line deletion (remove continuation prompt)
686 # Line deletion (remove continuation prompt)
689 cursor.movePosition(QtGui.QTextCursor.NextBlock,
687 cursor.movePosition(QtGui.QTextCursor.NextBlock,
690 QtGui.QTextCursor.KeepAnchor)
688 QtGui.QTextCursor.KeepAnchor)
691 cursor.movePosition(QtGui.QTextCursor.Right,
689 cursor.movePosition(QtGui.QTextCursor.Right,
692 QtGui.QTextCursor.KeepAnchor,
690 QtGui.QTextCursor.KeepAnchor,
693 len(self._continuation_prompt))
691 len(self._continuation_prompt))
694 cursor.removeSelectedText()
692 cursor.removeSelectedText()
695 intercepted = True
693 intercepted = True
696
694
697 elif key == QtCore.Qt.Key_L:
695 elif key == QtCore.Qt.Key_L:
698 # It would be better to simply move the prompt block to the top
696 # It would be better to simply move the prompt block to the top
699 # of the control viewport. QPlainTextEdit has a private method
697 # of the control viewport. QPlainTextEdit has a private method
700 # to do this (setTopBlock), but it cannot be duplicated here
698 # to do this (setTopBlock), but it cannot be duplicated here
701 # because it requires access to the QTextControl that underlies
699 # because it requires access to the QTextControl that underlies
702 # both QPlainTextEdit and QTextEdit. In short, this can only be
700 # both QPlainTextEdit and QTextEdit. In short, this can only be
703 # achieved by appending newlines after the prompt, which is a
701 # achieved by appending newlines after the prompt, which is a
704 # gigantic hack and likely to cause other problems.
702 # gigantic hack and likely to cause other problems.
705 self.clear()
703 self.clear()
706 intercepted = True
704 intercepted = True
707
705
708 elif key == QtCore.Qt.Key_O:
706 elif key == QtCore.Qt.Key_O:
709 if self._page_control and self._page_control.isVisible():
707 if self._page_control and self._page_control.isVisible():
710 self._page_control.setFocus()
708 self._page_control.setFocus()
711 intercept = True
709 intercept = True
712
710
713 elif key == QtCore.Qt.Key_X:
711 elif key == QtCore.Qt.Key_X:
714 # FIXME: Instead of disabling cut completely, only allow it
712 # FIXME: Instead of disabling cut completely, only allow it
715 # when safe.
713 # when safe.
716 intercepted = True
714 intercepted = True
717
715
718 elif key == QtCore.Qt.Key_Y:
716 elif key == QtCore.Qt.Key_Y:
719 self.paste()
717 self.paste()
720 intercepted = True
718 intercepted = True
721
719
722 elif alt_down:
720 elif alt_down:
723 if key == QtCore.Qt.Key_B:
721 if key == QtCore.Qt.Key_B:
724 self._set_cursor(self._get_word_start_cursor(position))
722 self._set_cursor(self._get_word_start_cursor(position))
725 intercepted = True
723 intercepted = True
726
724
727 elif key == QtCore.Qt.Key_F:
725 elif key == QtCore.Qt.Key_F:
728 self._set_cursor(self._get_word_end_cursor(position))
726 self._set_cursor(self._get_word_end_cursor(position))
729 intercepted = True
727 intercepted = True
730
728
731 elif key == QtCore.Qt.Key_Backspace:
729 elif key == QtCore.Qt.Key_Backspace:
732 cursor = self._get_word_start_cursor(position)
730 cursor = self._get_word_start_cursor(position)
733 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
731 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
734 cursor.removeSelectedText()
732 cursor.removeSelectedText()
735 intercepted = True
733 intercepted = True
736
734
737 elif key == QtCore.Qt.Key_D:
735 elif key == QtCore.Qt.Key_D:
738 cursor = self._get_word_end_cursor(position)
736 cursor = self._get_word_end_cursor(position)
739 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
737 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
740 cursor.removeSelectedText()
738 cursor.removeSelectedText()
741 intercepted = True
739 intercepted = True
742
740
743 elif key == QtCore.Qt.Key_Greater:
741 elif key == QtCore.Qt.Key_Greater:
744 self._control.moveCursor(QtGui.QTextCursor.End)
742 self._control.moveCursor(QtGui.QTextCursor.End)
745 intercepted = True
743 intercepted = True
746
744
747 elif key == QtCore.Qt.Key_Less:
745 elif key == QtCore.Qt.Key_Less:
748 self._control.setTextCursor(self._get_prompt_cursor())
746 self._control.setTextCursor(self._get_prompt_cursor())
749 intercepted = True
747 intercepted = True
750
748
751 else:
749 else:
752 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
750 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
753 intercepted = True
751 intercepted = True
752
753 # Special handling when tab completing in text mode.
754 if self._text_completing_pos:
755 self._clear_temporary_buffer()
756 self._text_completing_pos = 0
757
754 if self._in_buffer(position):
758 if self._in_buffer(position):
755 if self._reading:
759 if self._reading:
756 self._append_plain_text('\n')
760 self._append_plain_text('\n')
757 self._reading = False
761 self._reading = False
758 if self._reading_callback:
762 if self._reading_callback:
759 self._reading_callback()
763 self._reading_callback()
760
764
761 # If there is only whitespace after the cursor, execute.
765 # If there is only whitespace after the cursor, execute.
762 # Otherwise, split the line with a continuation prompt.
766 # Otherwise, split the line with a continuation prompt.
763 elif not self._executing:
767 elif not self._executing:
764 cursor.movePosition(QtGui.QTextCursor.End,
768 cursor.movePosition(QtGui.QTextCursor.End,
765 QtGui.QTextCursor.KeepAnchor)
769 QtGui.QTextCursor.KeepAnchor)
766 if cursor.selectedText().trimmed().isEmpty():
770 at_end = cursor.selectedText().trimmed().isEmpty()
767 self.execute(interactive=True)
771 if at_end or shift_down:
772 self.execute(interactive = not shift_down)
768 else:
773 else:
769 # Do this inside an edit block for clean undo/redo.
774 # Do this inside an edit block for clean undo/redo.
770 cursor.beginEditBlock()
775 cursor.beginEditBlock()
771 cursor.setPosition(position)
776 cursor.setPosition(position)
772 cursor.insertText('\n')
777 cursor.insertText('\n')
773 self._insert_continuation_prompt(cursor)
778 self._insert_continuation_prompt(cursor)
774 cursor.endEditBlock()
779 cursor.endEditBlock()
775
780
776 # Ensure that the whole input buffer is visible.
781 # Ensure that the whole input buffer is visible.
777 # FIXME: This will not be usable if the input buffer
782 # FIXME: This will not be usable if the input buffer
778 # is taller than the console widget.
783 # is taller than the console widget.
779 self._control.moveCursor(QtGui.QTextCursor.End)
784 self._control.moveCursor(QtGui.QTextCursor.End)
780 self._control.setTextCursor(cursor)
785 self._control.setTextCursor(cursor)
781
786
787 elif key == QtCore.Qt.Key_Escape:
788 self._keyboard_quit()
789 intercepted = True
790
782 elif key == QtCore.Qt.Key_Up:
791 elif key == QtCore.Qt.Key_Up:
783 if self._reading or not self._up_pressed():
792 if self._reading or not self._up_pressed():
784 intercepted = True
793 intercepted = True
785 else:
794 else:
786 prompt_line = self._get_prompt_cursor().blockNumber()
795 prompt_line = self._get_prompt_cursor().blockNumber()
787 intercepted = cursor.blockNumber() <= prompt_line
796 intercepted = cursor.blockNumber() <= prompt_line
788
797
789 elif key == QtCore.Qt.Key_Down:
798 elif key == QtCore.Qt.Key_Down:
790 if self._reading or not self._down_pressed():
799 if self._reading or not self._down_pressed():
791 intercepted = True
800 intercepted = True
792 else:
801 else:
793 end_line = self._get_end_cursor().blockNumber()
802 end_line = self._get_end_cursor().blockNumber()
794 intercepted = cursor.blockNumber() == end_line
803 intercepted = cursor.blockNumber() == end_line
795
804
796 elif key == QtCore.Qt.Key_Tab:
805 elif key == QtCore.Qt.Key_Tab:
797 if not self._reading:
806 if not self._reading:
798 intercepted = not self._tab_pressed()
807 intercepted = not self._tab_pressed()
799
808
800 elif key == QtCore.Qt.Key_Left:
809 elif key == QtCore.Qt.Key_Left:
801 intercepted = not self._in_buffer(position - 1)
810 intercepted = not self._in_buffer(position - 1)
802
811
803 elif key == QtCore.Qt.Key_Home:
812 elif key == QtCore.Qt.Key_Home:
804 start_line = cursor.blockNumber()
813 start_line = cursor.blockNumber()
805 if start_line == self._get_prompt_cursor().blockNumber():
814 if start_line == self._get_prompt_cursor().blockNumber():
806 start_pos = self._prompt_pos
815 start_pos = self._prompt_pos
807 else:
816 else:
808 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
817 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
809 QtGui.QTextCursor.KeepAnchor)
818 QtGui.QTextCursor.KeepAnchor)
810 start_pos = cursor.position()
819 start_pos = cursor.position()
811 start_pos += len(self._continuation_prompt)
820 start_pos += len(self._continuation_prompt)
812 cursor.setPosition(position)
821 cursor.setPosition(position)
813 if shift_down and self._in_buffer(position):
822 if shift_down and self._in_buffer(position):
814 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
823 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
815 else:
824 else:
816 cursor.setPosition(start_pos)
825 cursor.setPosition(start_pos)
817 self._set_cursor(cursor)
826 self._set_cursor(cursor)
818 intercepted = True
827 intercepted = True
819
828
820 elif key == QtCore.Qt.Key_Backspace:
829 elif key == QtCore.Qt.Key_Backspace:
821
830
822 # Line deletion (remove continuation prompt)
831 # Line deletion (remove continuation prompt)
823 len_prompt = len(self._continuation_prompt)
832 len_prompt = len(self._continuation_prompt)
824 if not self._reading and \
833 if not self._reading and \
825 cursor.columnNumber() == len_prompt and \
834 cursor.columnNumber() == len_prompt and \
826 position != self._prompt_pos:
835 position != self._prompt_pos:
827 cursor.beginEditBlock()
836 cursor.beginEditBlock()
828 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
837 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
829 QtGui.QTextCursor.KeepAnchor)
838 QtGui.QTextCursor.KeepAnchor)
830 cursor.removeSelectedText()
839 cursor.removeSelectedText()
831 cursor.deletePreviousChar()
840 cursor.deletePreviousChar()
832 cursor.endEditBlock()
841 cursor.endEditBlock()
833 intercepted = True
842 intercepted = True
834
843
835 # Regular backwards deletion
844 # Regular backwards deletion
836 else:
845 else:
837 anchor = cursor.anchor()
846 anchor = cursor.anchor()
838 if anchor == position:
847 if anchor == position:
839 intercepted = not self._in_buffer(position - 1)
848 intercepted = not self._in_buffer(position - 1)
840 else:
849 else:
841 intercepted = not self._in_buffer(min(anchor, position))
850 intercepted = not self._in_buffer(min(anchor, position))
842
851
843 elif key == QtCore.Qt.Key_Delete:
852 elif key == QtCore.Qt.Key_Delete:
844
853
845 # Line deletion (remove continuation prompt)
854 # Line deletion (remove continuation prompt)
846 if not self._reading and cursor.atBlockEnd() and not \
855 if not self._reading and cursor.atBlockEnd() and not \
847 cursor.hasSelection():
856 cursor.hasSelection():
848 cursor.movePosition(QtGui.QTextCursor.NextBlock,
857 cursor.movePosition(QtGui.QTextCursor.NextBlock,
849 QtGui.QTextCursor.KeepAnchor)
858 QtGui.QTextCursor.KeepAnchor)
850 cursor.movePosition(QtGui.QTextCursor.Right,
859 cursor.movePosition(QtGui.QTextCursor.Right,
851 QtGui.QTextCursor.KeepAnchor,
860 QtGui.QTextCursor.KeepAnchor,
852 len(self._continuation_prompt))
861 len(self._continuation_prompt))
853 cursor.removeSelectedText()
862 cursor.removeSelectedText()
854 intercepted = True
863 intercepted = True
855
864
856 # Regular forwards deletion:
865 # Regular forwards deletion:
857 else:
866 else:
858 anchor = cursor.anchor()
867 anchor = cursor.anchor()
859 intercepted = (not self._in_buffer(anchor) or
868 intercepted = (not self._in_buffer(anchor) or
860 not self._in_buffer(position))
869 not self._in_buffer(position))
861
870
862 # Don't move the cursor if control is down to allow copy-paste using
871 # Don't move the cursor if control is down to allow copy-paste using
863 # the keyboard in any part of the buffer.
872 # the keyboard in any part of the buffer.
864 if not ctrl_down:
873 if not ctrl_down:
865 self._keep_cursor_in_buffer()
874 self._keep_cursor_in_buffer()
866
875
867 return intercepted
876 return intercepted
868
877
869 def _event_filter_page_keypress(self, event):
878 def _event_filter_page_keypress(self, event):
870 """ Filter key events for the paging widget to create console-like
879 """ Filter key events for the paging widget to create console-like
871 interface.
880 interface.
872 """
881 """
873 key = event.key()
882 key = event.key()
874 ctrl_down = self._control_key_down(event.modifiers())
883 ctrl_down = self._control_key_down(event.modifiers())
875 alt_down = event.modifiers() & QtCore.Qt.AltModifier
884 alt_down = event.modifiers() & QtCore.Qt.AltModifier
876
885
877 if ctrl_down:
886 if ctrl_down:
878 if key == QtCore.Qt.Key_O:
887 if key == QtCore.Qt.Key_O:
879 self._control.setFocus()
888 self._control.setFocus()
880 intercept = True
889 intercept = True
881
890
882 elif alt_down:
891 elif alt_down:
883 if key == QtCore.Qt.Key_Greater:
892 if key == QtCore.Qt.Key_Greater:
884 self._page_control.moveCursor(QtGui.QTextCursor.End)
893 self._page_control.moveCursor(QtGui.QTextCursor.End)
885 intercepted = True
894 intercepted = True
886
895
887 elif key == QtCore.Qt.Key_Less:
896 elif key == QtCore.Qt.Key_Less:
888 self._page_control.moveCursor(QtGui.QTextCursor.Start)
897 self._page_control.moveCursor(QtGui.QTextCursor.Start)
889 intercepted = True
898 intercepted = True
890
899
891 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
900 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
892 if self._splitter:
901 if self._splitter:
893 self._page_control.hide()
902 self._page_control.hide()
894 else:
903 else:
895 self.layout().setCurrentWidget(self._control)
904 self.layout().setCurrentWidget(self._control)
896 return True
905 return True
897
906
898 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
907 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
899 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
908 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
900 QtCore.Qt.Key_PageDown,
909 QtCore.Qt.Key_PageDown,
901 QtCore.Qt.NoModifier)
910 QtCore.Qt.NoModifier)
902 QtGui.qApp.sendEvent(self._page_control, new_event)
911 QtGui.qApp.sendEvent(self._page_control, new_event)
903 return True
912 return True
904
913
905 elif key == QtCore.Qt.Key_Backspace:
914 elif key == QtCore.Qt.Key_Backspace:
906 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
915 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
907 QtCore.Qt.Key_PageUp,
916 QtCore.Qt.Key_PageUp,
908 QtCore.Qt.NoModifier)
917 QtCore.Qt.NoModifier)
909 QtGui.qApp.sendEvent(self._page_control, new_event)
918 QtGui.qApp.sendEvent(self._page_control, new_event)
910 return True
919 return True
911
920
912 return False
921 return False
913
922
914 def _format_as_columns(self, items, separator=' '):
923 def _format_as_columns(self, items, separator=' '):
915 """ Transform a list of strings into a single string with columns.
924 """ Transform a list of strings into a single string with columns.
916
925
917 Parameters
926 Parameters
918 ----------
927 ----------
919 items : sequence of strings
928 items : sequence of strings
920 The strings to process.
929 The strings to process.
921
930
922 separator : str, optional [default is two spaces]
931 separator : str, optional [default is two spaces]
923 The string that separates columns.
932 The string that separates columns.
924
933
925 Returns
934 Returns
926 -------
935 -------
927 The formatted string.
936 The formatted string.
928 """
937 """
929 # Note: this code is adapted from columnize 0.3.2.
938 # Note: this code is adapted from columnize 0.3.2.
930 # See http://code.google.com/p/pycolumnize/
939 # See http://code.google.com/p/pycolumnize/
931
940
932 # Calculate the number of characters available.
941 # Calculate the number of characters available.
933 width = self._control.viewport().width()
942 width = self._control.viewport().width()
934 char_width = QtGui.QFontMetrics(self.font).maxWidth()
943 char_width = QtGui.QFontMetrics(self.font).maxWidth()
935 displaywidth = max(10, (width / char_width) - 1)
944 displaywidth = max(10, (width / char_width) - 1)
936
945
937 # Some degenerate cases.
946 # Some degenerate cases.
938 size = len(items)
947 size = len(items)
939 if size == 0:
948 if size == 0:
940 return '\n'
949 return '\n'
941 elif size == 1:
950 elif size == 1:
942 return '%s\n' % str(items[0])
951 return '%s\n' % str(items[0])
943
952
944 # Try every row count from 1 upwards
953 # Try every row count from 1 upwards
945 array_index = lambda nrows, row, col: nrows*col + row
954 array_index = lambda nrows, row, col: nrows*col + row
946 for nrows in range(1, size):
955 for nrows in range(1, size):
947 ncols = (size + nrows - 1) // nrows
956 ncols = (size + nrows - 1) // nrows
948 colwidths = []
957 colwidths = []
949 totwidth = -len(separator)
958 totwidth = -len(separator)
950 for col in range(ncols):
959 for col in range(ncols):
951 # Get max column width for this column
960 # Get max column width for this column
952 colwidth = 0
961 colwidth = 0
953 for row in range(nrows):
962 for row in range(nrows):
954 i = array_index(nrows, row, col)
963 i = array_index(nrows, row, col)
955 if i >= size: break
964 if i >= size: break
956 x = items[i]
965 x = items[i]
957 colwidth = max(colwidth, len(x))
966 colwidth = max(colwidth, len(x))
958 colwidths.append(colwidth)
967 colwidths.append(colwidth)
959 totwidth += colwidth + len(separator)
968 totwidth += colwidth + len(separator)
960 if totwidth > displaywidth:
969 if totwidth > displaywidth:
961 break
970 break
962 if totwidth <= displaywidth:
971 if totwidth <= displaywidth:
963 break
972 break
964
973
965 # The smallest number of rows computed and the max widths for each
974 # The smallest number of rows computed and the max widths for each
966 # column has been obtained. Now we just have to format each of the rows.
975 # column has been obtained. Now we just have to format each of the rows.
967 string = ''
976 string = ''
968 for row in range(nrows):
977 for row in range(nrows):
969 texts = []
978 texts = []
970 for col in range(ncols):
979 for col in range(ncols):
971 i = row + nrows*col
980 i = row + nrows*col
972 if i >= size:
981 if i >= size:
973 texts.append('')
982 texts.append('')
974 else:
983 else:
975 texts.append(items[i])
984 texts.append(items[i])
976 while texts and not texts[-1]:
985 while texts and not texts[-1]:
977 del texts[-1]
986 del texts[-1]
978 for col in range(len(texts)):
987 for col in range(len(texts)):
979 texts[col] = texts[col].ljust(colwidths[col])
988 texts[col] = texts[col].ljust(colwidths[col])
980 string += '%s\n' % str(separator.join(texts))
989 string += '%s\n' % str(separator.join(texts))
981 return string
990 return string
982
991
983 def _get_block_plain_text(self, block):
992 def _get_block_plain_text(self, block):
984 """ Given a QTextBlock, return its unformatted text.
993 """ Given a QTextBlock, return its unformatted text.
985 """
994 """
986 cursor = QtGui.QTextCursor(block)
995 cursor = QtGui.QTextCursor(block)
987 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
996 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
988 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
997 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
989 QtGui.QTextCursor.KeepAnchor)
998 QtGui.QTextCursor.KeepAnchor)
990 return str(cursor.selection().toPlainText())
999 return str(cursor.selection().toPlainText())
991
1000
992 def _get_cursor(self):
1001 def _get_cursor(self):
993 """ Convenience method that returns a cursor for the current position.
1002 """ Convenience method that returns a cursor for the current position.
994 """
1003 """
995 return self._control.textCursor()
1004 return self._control.textCursor()
996
1005
997 def _get_end_cursor(self):
1006 def _get_end_cursor(self):
998 """ Convenience method that returns a cursor for the last character.
1007 """ Convenience method that returns a cursor for the last character.
999 """
1008 """
1000 cursor = self._control.textCursor()
1009 cursor = self._control.textCursor()
1001 cursor.movePosition(QtGui.QTextCursor.End)
1010 cursor.movePosition(QtGui.QTextCursor.End)
1002 return cursor
1011 return cursor
1003
1012
1004 def _get_input_buffer_cursor_column(self):
1013 def _get_input_buffer_cursor_column(self):
1005 """ Returns the column of the cursor in the input buffer, excluding the
1014 """ Returns the column of the cursor in the input buffer, excluding the
1006 contribution by the prompt, or -1 if there is no such column.
1015 contribution by the prompt, or -1 if there is no such column.
1007 """
1016 """
1008 prompt = self._get_input_buffer_cursor_prompt()
1017 prompt = self._get_input_buffer_cursor_prompt()
1009 if prompt is None:
1018 if prompt is None:
1010 return -1
1019 return -1
1011 else:
1020 else:
1012 cursor = self._control.textCursor()
1021 cursor = self._control.textCursor()
1013 return cursor.columnNumber() - len(prompt)
1022 return cursor.columnNumber() - len(prompt)
1014
1023
1015 def _get_input_buffer_cursor_line(self):
1024 def _get_input_buffer_cursor_line(self):
1016 """ Returns line of the input buffer that contains the cursor, or None
1025 """ Returns line of the input buffer that contains the cursor, or None
1017 if there is no such line.
1026 if there is no such line.
1018 """
1027 """
1019 prompt = self._get_input_buffer_cursor_prompt()
1028 prompt = self._get_input_buffer_cursor_prompt()
1020 if prompt is None:
1029 if prompt is None:
1021 return None
1030 return None
1022 else:
1031 else:
1023 cursor = self._control.textCursor()
1032 cursor = self._control.textCursor()
1024 text = self._get_block_plain_text(cursor.block())
1033 text = self._get_block_plain_text(cursor.block())
1025 return text[len(prompt):]
1034 return text[len(prompt):]
1026
1035
1027 def _get_input_buffer_cursor_prompt(self):
1036 def _get_input_buffer_cursor_prompt(self):
1028 """ Returns the (plain text) prompt for line of the input buffer that
1037 """ Returns the (plain text) prompt for line of the input buffer that
1029 contains the cursor, or None if there is no such line.
1038 contains the cursor, or None if there is no such line.
1030 """
1039 """
1031 if self._executing:
1040 if self._executing:
1032 return None
1041 return None
1033 cursor = self._control.textCursor()
1042 cursor = self._control.textCursor()
1034 if cursor.position() >= self._prompt_pos:
1043 if cursor.position() >= self._prompt_pos:
1035 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1044 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1036 return self._prompt
1045 return self._prompt
1037 else:
1046 else:
1038 return self._continuation_prompt
1047 return self._continuation_prompt
1039 else:
1048 else:
1040 return None
1049 return None
1041
1050
1042 def _get_prompt_cursor(self):
1051 def _get_prompt_cursor(self):
1043 """ Convenience method that returns a cursor for the prompt position.
1052 """ Convenience method that returns a cursor for the prompt position.
1044 """
1053 """
1045 cursor = self._control.textCursor()
1054 cursor = self._control.textCursor()
1046 cursor.setPosition(self._prompt_pos)
1055 cursor.setPosition(self._prompt_pos)
1047 return cursor
1056 return cursor
1048
1057
1049 def _get_selection_cursor(self, start, end):
1058 def _get_selection_cursor(self, start, end):
1050 """ Convenience method that returns a cursor with text selected between
1059 """ Convenience method that returns a cursor with text selected between
1051 the positions 'start' and 'end'.
1060 the positions 'start' and 'end'.
1052 """
1061 """
1053 cursor = self._control.textCursor()
1062 cursor = self._control.textCursor()
1054 cursor.setPosition(start)
1063 cursor.setPosition(start)
1055 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1064 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1056 return cursor
1065 return cursor
1057
1066
1058 def _get_word_start_cursor(self, position):
1067 def _get_word_start_cursor(self, position):
1059 """ Find the start of the word to the left the given position. If a
1068 """ Find the start of the word to the left the given position. If a
1060 sequence of non-word characters precedes the first word, skip over
1069 sequence of non-word characters precedes the first word, skip over
1061 them. (This emulates the behavior of bash, emacs, etc.)
1070 them. (This emulates the behavior of bash, emacs, etc.)
1062 """
1071 """
1063 document = self._control.document()
1072 document = self._control.document()
1064 position -= 1
1073 position -= 1
1065 while position >= self._prompt_pos and \
1074 while position >= self._prompt_pos and \
1066 not document.characterAt(position).isLetterOrNumber():
1075 not document.characterAt(position).isLetterOrNumber():
1067 position -= 1
1076 position -= 1
1068 while position >= self._prompt_pos and \
1077 while position >= self._prompt_pos and \
1069 document.characterAt(position).isLetterOrNumber():
1078 document.characterAt(position).isLetterOrNumber():
1070 position -= 1
1079 position -= 1
1071 cursor = self._control.textCursor()
1080 cursor = self._control.textCursor()
1072 cursor.setPosition(position + 1)
1081 cursor.setPosition(position + 1)
1073 return cursor
1082 return cursor
1074
1083
1075 def _get_word_end_cursor(self, position):
1084 def _get_word_end_cursor(self, position):
1076 """ Find the end of the word to the right the given position. If a
1085 """ Find the end of the word to the right the given position. If a
1077 sequence of non-word characters precedes the first word, skip over
1086 sequence of non-word characters precedes the first word, skip over
1078 them. (This emulates the behavior of bash, emacs, etc.)
1087 them. (This emulates the behavior of bash, emacs, etc.)
1079 """
1088 """
1080 document = self._control.document()
1089 document = self._control.document()
1081 end = self._get_end_cursor().position()
1090 end = self._get_end_cursor().position()
1082 while position < end and \
1091 while position < end and \
1083 not document.characterAt(position).isLetterOrNumber():
1092 not document.characterAt(position).isLetterOrNumber():
1084 position += 1
1093 position += 1
1085 while position < end and \
1094 while position < end and \
1086 document.characterAt(position).isLetterOrNumber():
1095 document.characterAt(position).isLetterOrNumber():
1087 position += 1
1096 position += 1
1088 cursor = self._control.textCursor()
1097 cursor = self._control.textCursor()
1089 cursor.setPosition(position)
1098 cursor.setPosition(position)
1090 return cursor
1099 return cursor
1091
1100
1092 def _insert_continuation_prompt(self, cursor):
1101 def _insert_continuation_prompt(self, cursor):
1093 """ Inserts new continuation prompt using the specified cursor.
1102 """ Inserts new continuation prompt using the specified cursor.
1094 """
1103 """
1095 if self._continuation_prompt_html is None:
1104 if self._continuation_prompt_html is None:
1096 self._insert_plain_text(cursor, self._continuation_prompt)
1105 self._insert_plain_text(cursor, self._continuation_prompt)
1097 else:
1106 else:
1098 self._continuation_prompt = self._insert_html_fetching_plain_text(
1107 self._continuation_prompt = self._insert_html_fetching_plain_text(
1099 cursor, self._continuation_prompt_html)
1108 cursor, self._continuation_prompt_html)
1100
1109
1101 def _insert_html(self, cursor, html):
1110 def _insert_html(self, cursor, html):
1102 """ Inserts HTML using the specified cursor in such a way that future
1111 """ Inserts HTML using the specified cursor in such a way that future
1103 formatting is unaffected.
1112 formatting is unaffected.
1104 """
1113 """
1105 cursor.beginEditBlock()
1114 cursor.beginEditBlock()
1106 cursor.insertHtml(html)
1115 cursor.insertHtml(html)
1107
1116
1108 # After inserting HTML, the text document "remembers" it's in "html
1117 # After inserting HTML, the text document "remembers" it's in "html
1109 # mode", which means that subsequent calls adding plain text will result
1118 # mode", which means that subsequent calls adding plain text will result
1110 # in unwanted formatting, lost tab characters, etc. The following code
1119 # in unwanted formatting, lost tab characters, etc. The following code
1111 # hacks around this behavior, which I consider to be a bug in Qt, by
1120 # hacks around this behavior, which I consider to be a bug in Qt, by
1112 # (crudely) resetting the document's style state.
1121 # (crudely) resetting the document's style state.
1113 cursor.movePosition(QtGui.QTextCursor.Left,
1122 cursor.movePosition(QtGui.QTextCursor.Left,
1114 QtGui.QTextCursor.KeepAnchor)
1123 QtGui.QTextCursor.KeepAnchor)
1115 if cursor.selection().toPlainText() == ' ':
1124 if cursor.selection().toPlainText() == ' ':
1116 cursor.removeSelectedText()
1125 cursor.removeSelectedText()
1117 else:
1126 else:
1118 cursor.movePosition(QtGui.QTextCursor.Right)
1127 cursor.movePosition(QtGui.QTextCursor.Right)
1119 cursor.insertText(' ', QtGui.QTextCharFormat())
1128 cursor.insertText(' ', QtGui.QTextCharFormat())
1120 cursor.endEditBlock()
1129 cursor.endEditBlock()
1121
1130
1122 def _insert_html_fetching_plain_text(self, cursor, html):
1131 def _insert_html_fetching_plain_text(self, cursor, html):
1123 """ Inserts HTML using the specified cursor, then returns its plain text
1132 """ Inserts HTML using the specified cursor, then returns its plain text
1124 version.
1133 version.
1125 """
1134 """
1126 cursor.beginEditBlock()
1135 cursor.beginEditBlock()
1127 cursor.removeSelectedText()
1136 cursor.removeSelectedText()
1128
1137
1129 start = cursor.position()
1138 start = cursor.position()
1130 self._insert_html(cursor, html)
1139 self._insert_html(cursor, html)
1131 end = cursor.position()
1140 end = cursor.position()
1132 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1141 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1133 text = str(cursor.selection().toPlainText())
1142 text = str(cursor.selection().toPlainText())
1134
1143
1135 cursor.setPosition(end)
1144 cursor.setPosition(end)
1136 cursor.endEditBlock()
1145 cursor.endEditBlock()
1137 return text
1146 return text
1138
1147
1139 def _insert_plain_text(self, cursor, text):
1148 def _insert_plain_text(self, cursor, text):
1140 """ Inserts plain text using the specified cursor, processing ANSI codes
1149 """ Inserts plain text using the specified cursor, processing ANSI codes
1141 if enabled.
1150 if enabled.
1142 """
1151 """
1143 cursor.beginEditBlock()
1152 cursor.beginEditBlock()
1144 if self.ansi_codes:
1153 if self.ansi_codes:
1145 for substring in self._ansi_processor.split_string(text):
1154 for substring in self._ansi_processor.split_string(text):
1146 for action in self._ansi_processor.actions:
1155 for action in self._ansi_processor.actions:
1147 if action.kind == 'erase' and action.area == 'screen':
1156 if action.kind == 'erase' and action.area == 'screen':
1148 cursor.select(QtGui.QTextCursor.Document)
1157 cursor.select(QtGui.QTextCursor.Document)
1149 cursor.removeSelectedText()
1158 cursor.removeSelectedText()
1150 format = self._ansi_processor.get_format()
1159 format = self._ansi_processor.get_format()
1151 cursor.insertText(substring, format)
1160 cursor.insertText(substring, format)
1152 else:
1161 else:
1153 cursor.insertText(text)
1162 cursor.insertText(text)
1154 cursor.endEditBlock()
1163 cursor.endEditBlock()
1155
1164
1156 def _insert_plain_text_into_buffer(self, text):
1165 def _insert_plain_text_into_buffer(self, text):
1157 """ Inserts text into the input buffer at the current cursor position,
1166 """ Inserts text into the input buffer at the current cursor position,
1158 ensuring that continuation prompts are inserted as necessary.
1167 ensuring that continuation prompts are inserted as necessary.
1159 """
1168 """
1160 lines = str(text).splitlines(True)
1169 lines = str(text).splitlines(True)
1161 if lines:
1170 if lines:
1162 self._keep_cursor_in_buffer()
1171 self._keep_cursor_in_buffer()
1163 cursor = self._control.textCursor()
1172 cursor = self._control.textCursor()
1164 cursor.beginEditBlock()
1173 cursor.beginEditBlock()
1165 cursor.insertText(lines[0])
1174 cursor.insertText(lines[0])
1166 for line in lines[1:]:
1175 for line in lines[1:]:
1167 if self._continuation_prompt_html is None:
1176 if self._continuation_prompt_html is None:
1168 cursor.insertText(self._continuation_prompt)
1177 cursor.insertText(self._continuation_prompt)
1169 else:
1178 else:
1170 self._continuation_prompt = \
1179 self._continuation_prompt = \
1171 self._insert_html_fetching_plain_text(
1180 self._insert_html_fetching_plain_text(
1172 cursor, self._continuation_prompt_html)
1181 cursor, self._continuation_prompt_html)
1173 cursor.insertText(line)
1182 cursor.insertText(line)
1174 cursor.endEditBlock()
1183 cursor.endEditBlock()
1175 self._control.setTextCursor(cursor)
1184 self._control.setTextCursor(cursor)
1176
1185
1177 def _in_buffer(self, position=None):
1186 def _in_buffer(self, position=None):
1178 """ Returns whether the current cursor (or, if specified, a position) is
1187 """ Returns whether the current cursor (or, if specified, a position) is
1179 inside the editing region.
1188 inside the editing region.
1180 """
1189 """
1181 cursor = self._control.textCursor()
1190 cursor = self._control.textCursor()
1182 if position is None:
1191 if position is None:
1183 position = cursor.position()
1192 position = cursor.position()
1184 else:
1193 else:
1185 cursor.setPosition(position)
1194 cursor.setPosition(position)
1186 line = cursor.blockNumber()
1195 line = cursor.blockNumber()
1187 prompt_line = self._get_prompt_cursor().blockNumber()
1196 prompt_line = self._get_prompt_cursor().blockNumber()
1188 if line == prompt_line:
1197 if line == prompt_line:
1189 return position >= self._prompt_pos
1198 return position >= self._prompt_pos
1190 elif line > prompt_line:
1199 elif line > prompt_line:
1191 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1200 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1192 prompt_pos = cursor.position() + len(self._continuation_prompt)
1201 prompt_pos = cursor.position() + len(self._continuation_prompt)
1193 return position >= prompt_pos
1202 return position >= prompt_pos
1194 return False
1203 return False
1195
1204
1196 def _keep_cursor_in_buffer(self):
1205 def _keep_cursor_in_buffer(self):
1197 """ Ensures that the cursor is inside the editing region. Returns
1206 """ Ensures that the cursor is inside the editing region. Returns
1198 whether the cursor was moved.
1207 whether the cursor was moved.
1199 """
1208 """
1200 moved = not self._in_buffer()
1209 moved = not self._in_buffer()
1201 if moved:
1210 if moved:
1202 cursor = self._control.textCursor()
1211 cursor = self._control.textCursor()
1203 cursor.movePosition(QtGui.QTextCursor.End)
1212 cursor.movePosition(QtGui.QTextCursor.End)
1204 self._control.setTextCursor(cursor)
1213 self._control.setTextCursor(cursor)
1205 return moved
1214 return moved
1215
1216 def _keyboard_quit(self):
1217 """ Cancels the current editing task ala Ctrl-G in Emacs.
1218 """
1219 if self._text_completing_pos:
1220 self._clear_temporary_buffer()
1221 self._text_completing_pos = 0
1222 else:
1223 self.input_buffer = ''
1206
1224
1207 def _page(self, text):
1225 def _page(self, text):
1208 """ Displays text using the pager if it exceeds the height of the
1226 """ Displays text using the pager if it exceeds the height of the
1209 visible area.
1227 visible area.
1210 """
1228 """
1211 if self.paging == 'none':
1229 if self.paging == 'none':
1212 self._append_plain_text(text)
1230 self._append_plain_text(text)
1213 else:
1231 else:
1214 line_height = QtGui.QFontMetrics(self.font).height()
1232 line_height = QtGui.QFontMetrics(self.font).height()
1215 minlines = self._control.viewport().height() / line_height
1233 minlines = self._control.viewport().height() / line_height
1216 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1234 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1217 if self.paging == 'custom':
1235 if self.paging == 'custom':
1218 self.custom_page_requested.emit(text)
1236 self.custom_page_requested.emit(text)
1219 else:
1237 else:
1220 self._page_control.clear()
1238 self._page_control.clear()
1221 cursor = self._page_control.textCursor()
1239 cursor = self._page_control.textCursor()
1222 self._insert_plain_text(cursor, text)
1240 self._insert_plain_text(cursor, text)
1223 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1241 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1224
1242
1225 self._page_control.viewport().resize(self._control.size())
1243 self._page_control.viewport().resize(self._control.size())
1226 if self._splitter:
1244 if self._splitter:
1227 self._page_control.show()
1245 self._page_control.show()
1228 self._page_control.setFocus()
1246 self._page_control.setFocus()
1229 else:
1247 else:
1230 self.layout().setCurrentWidget(self._page_control)
1248 self.layout().setCurrentWidget(self._page_control)
1231 else:
1249 else:
1232 self._append_plain_text(text)
1250 self._append_plain_text(text)
1233
1251
1234 def _prompt_started(self):
1252 def _prompt_started(self):
1235 """ Called immediately after a new prompt is displayed.
1253 """ Called immediately after a new prompt is displayed.
1236 """
1254 """
1237 # Temporarily disable the maximum block count to permit undo/redo and
1255 # Temporarily disable the maximum block count to permit undo/redo and
1238 # to ensure that the prompt position does not change due to truncation.
1256 # to ensure that the prompt position does not change due to truncation.
1239 # Because setting this property clears the undo/redo history, we only
1257 # Because setting this property clears the undo/redo history, we only
1240 # set it if we have to.
1258 # set it if we have to.
1241 if self._control.document().maximumBlockCount() > 0:
1259 if self._control.document().maximumBlockCount() > 0:
1242 self._control.document().setMaximumBlockCount(0)
1260 self._control.document().setMaximumBlockCount(0)
1243 self._control.setUndoRedoEnabled(True)
1261 self._control.setUndoRedoEnabled(True)
1244
1262
1245 self._control.setReadOnly(False)
1263 self._control.setReadOnly(False)
1246 self._control.moveCursor(QtGui.QTextCursor.End)
1264 self._control.moveCursor(QtGui.QTextCursor.End)
1247
1265
1248 self._executing = False
1266 self._executing = False
1249 self._prompt_started_hook()
1267 self._prompt_started_hook()
1250
1268
1251 def _prompt_finished(self):
1269 def _prompt_finished(self):
1252 """ Called immediately after a prompt is finished, i.e. when some input
1270 """ Called immediately after a prompt is finished, i.e. when some input
1253 will be processed and a new prompt displayed.
1271 will be processed and a new prompt displayed.
1254 """
1272 """
1255 self._control.setReadOnly(True)
1273 self._control.setReadOnly(True)
1256 self._prompt_finished_hook()
1274 self._prompt_finished_hook()
1257
1275
1258 def _readline(self, prompt='', callback=None):
1276 def _readline(self, prompt='', callback=None):
1259 """ Reads one line of input from the user.
1277 """ Reads one line of input from the user.
1260
1278
1261 Parameters
1279 Parameters
1262 ----------
1280 ----------
1263 prompt : str, optional
1281 prompt : str, optional
1264 The prompt to print before reading the line.
1282 The prompt to print before reading the line.
1265
1283
1266 callback : callable, optional
1284 callback : callable, optional
1267 A callback to execute with the read line. If not specified, input is
1285 A callback to execute with the read line. If not specified, input is
1268 read *synchronously* and this method does not return until it has
1286 read *synchronously* and this method does not return until it has
1269 been read.
1287 been read.
1270
1288
1271 Returns
1289 Returns
1272 -------
1290 -------
1273 If a callback is specified, returns nothing. Otherwise, returns the
1291 If a callback is specified, returns nothing. Otherwise, returns the
1274 input string with the trailing newline stripped.
1292 input string with the trailing newline stripped.
1275 """
1293 """
1276 if self._reading:
1294 if self._reading:
1277 raise RuntimeError('Cannot read a line. Widget is already reading.')
1295 raise RuntimeError('Cannot read a line. Widget is already reading.')
1278
1296
1279 if not callback and not self.isVisible():
1297 if not callback and not self.isVisible():
1280 # If the user cannot see the widget, this function cannot return.
1298 # If the user cannot see the widget, this function cannot return.
1281 raise RuntimeError('Cannot synchronously read a line if the widget '
1299 raise RuntimeError('Cannot synchronously read a line if the widget '
1282 'is not visible!')
1300 'is not visible!')
1283
1301
1284 self._reading = True
1302 self._reading = True
1285 self._show_prompt(prompt, newline=False)
1303 self._show_prompt(prompt, newline=False)
1286
1304
1287 if callback is None:
1305 if callback is None:
1288 self._reading_callback = None
1306 self._reading_callback = None
1289 while self._reading:
1307 while self._reading:
1290 QtCore.QCoreApplication.processEvents()
1308 QtCore.QCoreApplication.processEvents()
1291 return self.input_buffer.rstrip('\n')
1309 return self.input_buffer.rstrip('\n')
1292
1310
1293 else:
1311 else:
1294 self._reading_callback = lambda: \
1312 self._reading_callback = lambda: \
1295 callback(self.input_buffer.rstrip('\n'))
1313 callback(self.input_buffer.rstrip('\n'))
1296
1314
1297 def _set_continuation_prompt(self, prompt, html=False):
1315 def _set_continuation_prompt(self, prompt, html=False):
1298 """ Sets the continuation prompt.
1316 """ Sets the continuation prompt.
1299
1317
1300 Parameters
1318 Parameters
1301 ----------
1319 ----------
1302 prompt : str
1320 prompt : str
1303 The prompt to show when more input is needed.
1321 The prompt to show when more input is needed.
1304
1322
1305 html : bool, optional (default False)
1323 html : bool, optional (default False)
1306 If set, the prompt will be inserted as formatted HTML. Otherwise,
1324 If set, the prompt will be inserted as formatted HTML. Otherwise,
1307 the prompt will be treated as plain text, though ANSI color codes
1325 the prompt will be treated as plain text, though ANSI color codes
1308 will be handled.
1326 will be handled.
1309 """
1327 """
1310 if html:
1328 if html:
1311 self._continuation_prompt_html = prompt
1329 self._continuation_prompt_html = prompt
1312 else:
1330 else:
1313 self._continuation_prompt = prompt
1331 self._continuation_prompt = prompt
1314 self._continuation_prompt_html = None
1332 self._continuation_prompt_html = None
1315
1333
1316 def _set_cursor(self, cursor):
1334 def _set_cursor(self, cursor):
1317 """ Convenience method to set the current cursor.
1335 """ Convenience method to set the current cursor.
1318 """
1336 """
1319 self._control.setTextCursor(cursor)
1337 self._control.setTextCursor(cursor)
1320
1338
1321 def _show_context_menu(self, pos):
1339 def _show_context_menu(self, pos):
1322 """ Shows a context menu at the given QPoint (in widget coordinates).
1340 """ Shows a context menu at the given QPoint (in widget coordinates).
1323 """
1341 """
1324 menu = QtGui.QMenu()
1342 menu = QtGui.QMenu()
1325
1343
1326 copy_action = menu.addAction('Copy', self.copy)
1344 copy_action = menu.addAction('Copy', self.copy)
1327 copy_action.setEnabled(self._get_cursor().hasSelection())
1345 copy_action.setEnabled(self._get_cursor().hasSelection())
1328 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1346 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1329
1347
1330 paste_action = menu.addAction('Paste', self.paste)
1348 paste_action = menu.addAction('Paste', self.paste)
1331 paste_action.setEnabled(self.can_paste())
1349 paste_action.setEnabled(self.can_paste())
1332 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1350 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1333
1351
1334 menu.addSeparator()
1352 menu.addSeparator()
1335 menu.addAction('Select All', self.select_all)
1353 menu.addAction('Select All', self.select_all)
1336
1354
1337 menu.exec_(self._control.mapToGlobal(pos))
1355 menu.exec_(self._control.mapToGlobal(pos))
1338
1356
1339 def _show_prompt(self, prompt=None, html=False, newline=True):
1357 def _show_prompt(self, prompt=None, html=False, newline=True):
1340 """ Writes a new prompt at the end of the buffer.
1358 """ Writes a new prompt at the end of the buffer.
1341
1359
1342 Parameters
1360 Parameters
1343 ----------
1361 ----------
1344 prompt : str, optional
1362 prompt : str, optional
1345 The prompt to show. If not specified, the previous prompt is used.
1363 The prompt to show. If not specified, the previous prompt is used.
1346
1364
1347 html : bool, optional (default False)
1365 html : bool, optional (default False)
1348 Only relevant when a prompt is specified. If set, the prompt will
1366 Only relevant when a prompt is specified. If set, the prompt will
1349 be inserted as formatted HTML. Otherwise, the prompt will be treated
1367 be inserted as formatted HTML. Otherwise, the prompt will be treated
1350 as plain text, though ANSI color codes will be handled.
1368 as plain text, though ANSI color codes will be handled.
1351
1369
1352 newline : bool, optional (default True)
1370 newline : bool, optional (default True)
1353 If set, a new line will be written before showing the prompt if
1371 If set, a new line will be written before showing the prompt if
1354 there is not already a newline at the end of the buffer.
1372 there is not already a newline at the end of the buffer.
1355 """
1373 """
1356 # Insert a preliminary newline, if necessary.
1374 # Insert a preliminary newline, if necessary.
1357 if newline:
1375 if newline:
1358 cursor = self._get_end_cursor()
1376 cursor = self._get_end_cursor()
1359 if cursor.position() > 0:
1377 if cursor.position() > 0:
1360 cursor.movePosition(QtGui.QTextCursor.Left,
1378 cursor.movePosition(QtGui.QTextCursor.Left,
1361 QtGui.QTextCursor.KeepAnchor)
1379 QtGui.QTextCursor.KeepAnchor)
1362 if str(cursor.selection().toPlainText()) != '\n':
1380 if str(cursor.selection().toPlainText()) != '\n':
1363 self._append_plain_text('\n')
1381 self._append_plain_text('\n')
1364
1382
1365 # Write the prompt.
1383 # Write the prompt.
1366 self._append_plain_text(self._prompt_sep)
1384 self._append_plain_text(self._prompt_sep)
1367 if prompt is None:
1385 if prompt is None:
1368 if self._prompt_html is None:
1386 if self._prompt_html is None:
1369 self._append_plain_text(self._prompt)
1387 self._append_plain_text(self._prompt)
1370 else:
1388 else:
1371 self._append_html(self._prompt_html)
1389 self._append_html(self._prompt_html)
1372 else:
1390 else:
1373 if html:
1391 if html:
1374 self._prompt = self._append_html_fetching_plain_text(prompt)
1392 self._prompt = self._append_html_fetching_plain_text(prompt)
1375 self._prompt_html = prompt
1393 self._prompt_html = prompt
1376 else:
1394 else:
1377 self._append_plain_text(prompt)
1395 self._append_plain_text(prompt)
1378 self._prompt = prompt
1396 self._prompt = prompt
1379 self._prompt_html = None
1397 self._prompt_html = None
1380
1398
1381 self._prompt_pos = self._get_end_cursor().position()
1399 self._prompt_pos = self._get_end_cursor().position()
1382 self._prompt_started()
1400 self._prompt_started()
1383
1401
1384 #------ Signal handlers ----------------------------------------------------
1402 #------ Signal handlers ----------------------------------------------------
1385
1403
1386 def _cursor_position_changed(self):
1404 def _cursor_position_changed(self):
1387 """ Clears the temporary buffer based on the cursor position.
1405 """ Clears the temporary buffer based on the cursor position.
1388 """
1406 """
1389 if self._text_completing_pos:
1407 if self._text_completing_pos:
1390 document = self._control.document()
1408 document = self._control.document()
1391 if self._text_completing_pos < document.characterCount():
1409 if self._text_completing_pos < document.characterCount():
1392 cursor = self._control.textCursor()
1410 cursor = self._control.textCursor()
1393 pos = cursor.position()
1411 pos = cursor.position()
1394 text_cursor = self._control.textCursor()
1412 text_cursor = self._control.textCursor()
1395 text_cursor.setPosition(self._text_completing_pos)
1413 text_cursor.setPosition(self._text_completing_pos)
1396 if pos < self._text_completing_pos or \
1414 if pos < self._text_completing_pos or \
1397 cursor.blockNumber() > text_cursor.blockNumber():
1415 cursor.blockNumber() > text_cursor.blockNumber():
1398 self._clear_temporary_buffer()
1416 self._clear_temporary_buffer()
1399 self._text_completing_pos = 0
1417 self._text_completing_pos = 0
1400 else:
1418 else:
1401 self._clear_temporary_buffer()
1419 self._clear_temporary_buffer()
1402 self._text_completing_pos = 0
1420 self._text_completing_pos = 0
1403
1421
1404
1422
1405 class HistoryConsoleWidget(ConsoleWidget):
1423 class HistoryConsoleWidget(ConsoleWidget):
1406 """ A ConsoleWidget that keeps a history of the commands that have been
1424 """ A ConsoleWidget that keeps a history of the commands that have been
1407 executed.
1425 executed.
1408 """
1426 """
1409
1427
1410 #---------------------------------------------------------------------------
1428 #---------------------------------------------------------------------------
1411 # 'object' interface
1429 # 'object' interface
1412 #---------------------------------------------------------------------------
1430 #---------------------------------------------------------------------------
1413
1431
1414 def __init__(self, *args, **kw):
1432 def __init__(self, *args, **kw):
1415 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1433 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1416 self._history = []
1434 self._history = []
1417 self._history_index = 0
1435 self._history_index = 0
1418
1436
1419 #---------------------------------------------------------------------------
1437 #---------------------------------------------------------------------------
1420 # 'ConsoleWidget' public interface
1438 # 'ConsoleWidget' public interface
1421 #---------------------------------------------------------------------------
1439 #---------------------------------------------------------------------------
1422
1440
1423 def execute(self, source=None, hidden=False, interactive=False):
1441 def execute(self, source=None, hidden=False, interactive=False):
1424 """ Reimplemented to the store history.
1442 """ Reimplemented to the store history.
1425 """
1443 """
1426 if not hidden:
1444 if not hidden:
1427 history = self.input_buffer if source is None else source
1445 history = self.input_buffer if source is None else source
1428
1446
1429 executed = super(HistoryConsoleWidget, self).execute(
1447 executed = super(HistoryConsoleWidget, self).execute(
1430 source, hidden, interactive)
1448 source, hidden, interactive)
1431
1449
1432 if executed and not hidden:
1450 if executed and not hidden:
1433 # Save the command unless it was a blank line.
1451 # Save the command unless it was a blank line.
1434 history = history.rstrip()
1452 history = history.rstrip()
1435 if history:
1453 if history:
1436 self._history.append(history)
1454 self._history.append(history)
1437 self._history_index = len(self._history)
1455 self._history_index = len(self._history)
1438
1456
1439 return executed
1457 return executed
1440
1458
1441 #---------------------------------------------------------------------------
1459 #---------------------------------------------------------------------------
1442 # 'ConsoleWidget' abstract interface
1460 # 'ConsoleWidget' abstract interface
1443 #---------------------------------------------------------------------------
1461 #---------------------------------------------------------------------------
1444
1462
1445 def _up_pressed(self):
1463 def _up_pressed(self):
1446 """ Called when the up key is pressed. Returns whether to continue
1464 """ Called when the up key is pressed. Returns whether to continue
1447 processing the event.
1465 processing the event.
1448 """
1466 """
1449 prompt_cursor = self._get_prompt_cursor()
1467 prompt_cursor = self._get_prompt_cursor()
1450 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1468 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1451 self.history_previous()
1469 self.history_previous()
1452
1470
1453 # Go to the first line of prompt for seemless history scrolling.
1471 # Go to the first line of prompt for seemless history scrolling.
1454 cursor = self._get_prompt_cursor()
1472 cursor = self._get_prompt_cursor()
1455 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1473 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1456 self._set_cursor(cursor)
1474 self._set_cursor(cursor)
1457
1475
1458 return False
1476 return False
1459 return True
1477 return True
1460
1478
1461 def _down_pressed(self):
1479 def _down_pressed(self):
1462 """ Called when the down key is pressed. Returns whether to continue
1480 """ Called when the down key is pressed. Returns whether to continue
1463 processing the event.
1481 processing the event.
1464 """
1482 """
1465 end_cursor = self._get_end_cursor()
1483 end_cursor = self._get_end_cursor()
1466 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1484 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1467 self.history_next()
1485 self.history_next()
1468 return False
1486 return False
1469 return True
1487 return True
1470
1488
1471 #---------------------------------------------------------------------------
1489 #---------------------------------------------------------------------------
1472 # 'HistoryConsoleWidget' public interface
1490 # 'HistoryConsoleWidget' public interface
1473 #---------------------------------------------------------------------------
1491 #---------------------------------------------------------------------------
1474
1492
1475 def history_previous(self):
1493 def history_previous(self):
1476 """ If possible, set the input buffer to the previous item in the
1494 """ If possible, set the input buffer to the previous item in the
1477 history.
1495 history.
1478 """
1496 """
1479 if self._history_index > 0:
1497 if self._history_index > 0:
1480 self._history_index -= 1
1498 self._history_index -= 1
1481 self.input_buffer = self._history[self._history_index]
1499 self.input_buffer = self._history[self._history_index]
1482
1500
1483 def history_next(self):
1501 def history_next(self):
1484 """ Set the input buffer to the next item in the history, or a blank
1502 """ Set the input buffer to the next item in the history, or a blank
1485 line if there is no subsequent item.
1503 line if there is no subsequent item.
1486 """
1504 """
1487 if self._history_index < len(self._history):
1505 if self._history_index < len(self._history):
1488 self._history_index += 1
1506 self._history_index += 1
1489 if self._history_index < len(self._history):
1507 if self._history_index < len(self._history):
1490 self.input_buffer = self._history[self._history_index]
1508 self.input_buffer = self._history[self._history_index]
1491 else:
1509 else:
1492 self.input_buffer = ''
1510 self.input_buffer = ''
1493
1511
1494 #---------------------------------------------------------------------------
1512 #---------------------------------------------------------------------------
1495 # 'HistoryConsoleWidget' protected interface
1513 # 'HistoryConsoleWidget' protected interface
1496 #---------------------------------------------------------------------------
1514 #---------------------------------------------------------------------------
1497
1515
1498 def _set_history(self, history):
1516 def _set_history(self, history):
1499 """ Replace the current history with a sequence of history items.
1517 """ Replace the current history with a sequence of history items.
1500 """
1518 """
1501 self._history = list(history)
1519 self._history = list(history)
1502 self._history_index = len(self._history)
1520 self._history_index = len(self._history)
General Comments 0
You need to be logged in to leave comments. Login now