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