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