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