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