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