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