##// END OF EJS Templates
Fixed numerous bugs with tab completion, including the one recently reported by fperez.
epatters -
Show More
@@ -1,1554 +1,1566 b''
1 1 # Standard library imports
2 2 from os.path import commonprefix
3 3 import re
4 4 import sys
5 5 from textwrap import dedent
6 6
7 7 # System library imports
8 8 from PyQt4 import QtCore, QtGui
9 9
10 10 # Local imports
11 11 from IPython.config.configurable import Configurable
12 12 from IPython.frontend.qt.util import MetaQObjectHasTraits
13 13 from IPython.utils.traitlets import Bool, Enum, Int
14 14 from ansi_code_processor import QtAnsiCodeProcessor
15 15 from completion_widget import CompletionWidget
16 16
17 17
18 18 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
19 19 """ A QPlainTextEdit suitable for use with ConsoleWidget.
20 20 """
21 21 # Prevents text from being moved by drag and drop. Note that is not, for
22 22 # some reason, sufficient to catch drag events in the ConsoleWidget's
23 23 # event filter.
24 24 def dragEnterEvent(self, event): pass
25 25 def dragLeaveEvent(self, event): pass
26 26 def dragMoveEvent(self, event): pass
27 27 def dropEvent(self, event): pass
28 28
29 29 class ConsoleTextEdit(QtGui.QTextEdit):
30 30 """ A QTextEdit suitable for use with ConsoleWidget.
31 31 """
32 32 # See above.
33 33 def dragEnterEvent(self, event): pass
34 34 def dragLeaveEvent(self, event): pass
35 35 def dragMoveEvent(self, event): pass
36 36 def dropEvent(self, event): pass
37 37
38 38
39 39 class ConsoleWidget(Configurable, QtGui.QWidget):
40 40 """ An abstract base class for console-type widgets. This class has
41 41 functionality for:
42 42
43 43 * Maintaining a prompt and editing region
44 44 * Providing the traditional Unix-style console keyboard shortcuts
45 45 * Performing tab completion
46 46 * Paging text
47 47 * Handling ANSI escape codes
48 48
49 49 ConsoleWidget also provides a number of utility methods that will be
50 50 convenient to implementors of a console-style widget.
51 51 """
52 52 __metaclass__ = MetaQObjectHasTraits
53 53
54 54 # Whether to process ANSI escape codes.
55 55 ansi_codes = Bool(True, config=True)
56 56
57 57 # The maximum number of lines of text before truncation. Specifying a
58 58 # non-positive number disables text truncation (not recommended).
59 59 buffer_size = Int(500, config=True)
60 60
61 61 # Whether to use a list widget or plain text output for tab completion.
62 62 gui_completion = Bool(False, config=True)
63 63
64 64 # The type of underlying text widget to use. Valid values are 'plain', which
65 65 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
66 66 # NOTE: this value can only be specified during initialization.
67 67 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
68 68
69 69 # The type of paging to use. Valid values are:
70 70 # 'inside' : The widget pages like a traditional terminal pager.
71 71 # 'hsplit' : When paging is requested, the widget is split
72 72 # horizontally. The top pane contains the console, and the
73 73 # bottom pane contains the paged text.
74 74 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
75 75 # 'custom' : No action is taken by the widget beyond emitting a
76 76 # 'custom_page_requested(str)' signal.
77 77 # 'none' : The text is written directly to the console.
78 78 # NOTE: this value can only be specified during initialization.
79 79 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
80 80 default_value='inside', config=True)
81 81
82 82 # Whether to override ShortcutEvents for the keybindings defined by this
83 83 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
84 84 # priority (when it has focus) over, e.g., window-level menu shortcuts.
85 85 override_shortcuts = Bool(False)
86 86
87 87 # Signals that indicate ConsoleWidget state.
88 88 copy_available = QtCore.pyqtSignal(bool)
89 89 redo_available = QtCore.pyqtSignal(bool)
90 90 undo_available = QtCore.pyqtSignal(bool)
91 91
92 92 # Signal emitted when paging is needed and the paging style has been
93 93 # specified as 'custom'.
94 94 custom_page_requested = QtCore.pyqtSignal(object)
95 95
96 96 # Protected class variables.
97 97 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
98 98 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
99 99 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
100 100 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
101 101 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
102 102 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
103 103 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
104 104 _shortcuts = set(_ctrl_down_remap.keys() +
105 105 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
106 106 QtCore.Qt.Key_V ])
107 107
108 108 #---------------------------------------------------------------------------
109 109 # 'QObject' interface
110 110 #---------------------------------------------------------------------------
111 111
112 112 def __init__(self, parent=None, **kw):
113 113 """ Create a ConsoleWidget.
114 114
115 115 Parameters:
116 116 -----------
117 117 parent : QWidget, optional [default None]
118 118 The parent for this widget.
119 119 """
120 120 QtGui.QWidget.__init__(self, parent)
121 121 Configurable.__init__(self, **kw)
122 122
123 123 # Create the layout and underlying text widget.
124 124 layout = QtGui.QStackedLayout(self)
125 125 layout.setContentsMargins(0, 0, 0, 0)
126 126 self._control = self._create_control()
127 127 self._page_control = None
128 128 self._splitter = None
129 129 if self.paging in ('hsplit', 'vsplit'):
130 130 self._splitter = QtGui.QSplitter()
131 131 if self.paging == 'hsplit':
132 132 self._splitter.setOrientation(QtCore.Qt.Horizontal)
133 133 else:
134 134 self._splitter.setOrientation(QtCore.Qt.Vertical)
135 135 self._splitter.addWidget(self._control)
136 136 layout.addWidget(self._splitter)
137 137 else:
138 138 layout.addWidget(self._control)
139 139
140 140 # Create the paging widget, if necessary.
141 141 if self.paging in ('inside', 'hsplit', 'vsplit'):
142 142 self._page_control = self._create_page_control()
143 143 if self._splitter:
144 144 self._page_control.hide()
145 145 self._splitter.addWidget(self._page_control)
146 146 else:
147 147 layout.addWidget(self._page_control)
148 148
149 149 # Initialize protected variables. Some variables contain useful state
150 150 # information for subclasses; they should be considered read-only.
151 151 self._ansi_processor = QtAnsiCodeProcessor()
152 152 self._completion_widget = CompletionWidget(self._control)
153 153 self._continuation_prompt = '> '
154 154 self._continuation_prompt_html = None
155 155 self._executing = False
156 156 self._prompt = ''
157 157 self._prompt_html = None
158 158 self._prompt_pos = 0
159 159 self._prompt_sep = ''
160 160 self._reading = False
161 161 self._reading_callback = None
162 162 self._tab_width = 8
163 163 self._text_completing_pos = 0
164 164
165 165 # Set a monospaced font.
166 166 self.reset_font()
167 167
168 168 def eventFilter(self, obj, event):
169 169 """ Reimplemented to ensure a console-like behavior in the underlying
170 170 text widgets.
171 171 """
172 172 etype = event.type()
173 173 if etype == QtCore.QEvent.KeyPress:
174 174
175 175 # Re-map keys for all filtered widgets.
176 176 key = event.key()
177 177 if self._control_key_down(event.modifiers()) and \
178 178 key in self._ctrl_down_remap:
179 179 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
180 180 self._ctrl_down_remap[key],
181 181 QtCore.Qt.NoModifier)
182 182 QtGui.qApp.sendEvent(obj, new_event)
183 183 return True
184 184
185 185 elif obj == self._control:
186 186 return self._event_filter_console_keypress(event)
187 187
188 188 elif obj == self._page_control:
189 189 return self._event_filter_page_keypress(event)
190 190
191 191 # Make middle-click paste safe.
192 192 elif etype == QtCore.QEvent.MouseButtonRelease and \
193 193 event.button() == QtCore.Qt.MidButton and \
194 194 obj == self._control.viewport():
195 cursor = self._control.cursorForPosition(event.pos());
195 cursor = self._control.cursorForPosition(event.pos())
196 196 self._control.setTextCursor(cursor)
197 197 self.paste(QtGui.QClipboard.Selection)
198 198 return True
199 199
200 200 # Override shortucts for all filtered widgets. Note that on Mac OS it is
201 201 # always unnecessary to override shortcuts, hence the check below (users
202 202 # should just use the Control key instead of the Command key).
203 203 elif etype == QtCore.QEvent.ShortcutOverride and \
204 204 sys.platform != 'darwin' and \
205 205 self._control_key_down(event.modifiers()) and \
206 206 event.key() in self._shortcuts:
207 207 event.accept()
208 208 return False
209 209
210 210 return super(ConsoleWidget, self).eventFilter(obj, event)
211 211
212 212 #---------------------------------------------------------------------------
213 213 # 'QWidget' interface
214 214 #---------------------------------------------------------------------------
215 215
216 216 def sizeHint(self):
217 217 """ Reimplemented to suggest a size that is 80 characters wide and
218 218 25 lines high.
219 219 """
220 220 font_metrics = QtGui.QFontMetrics(self.font)
221 221 margin = (self._control.frameWidth() +
222 222 self._control.document().documentMargin()) * 2
223 223 style = self.style()
224 224 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
225 225
226 226 # Note 1: Despite my best efforts to take the various margins into
227 227 # account, the width is still coming out a bit too small, so we include
228 228 # a fudge factor of one character here.
229 229 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
230 230 # to a Qt bug on certain Mac OS systems where it returns 0.
231 231 width = font_metrics.width(' ') * 81 + margin
232 232 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
233 233 if self.paging == 'hsplit':
234 234 width = width * 2 + splitwidth
235 235
236 236 height = font_metrics.height() * 25 + margin
237 237 if self.paging == 'vsplit':
238 238 height = height * 2 + splitwidth
239 239
240 240 return QtCore.QSize(width, height)
241 241
242 242 #---------------------------------------------------------------------------
243 243 # 'ConsoleWidget' public interface
244 244 #---------------------------------------------------------------------------
245 245
246 246 def can_paste(self):
247 247 """ Returns whether text can be pasted from the clipboard.
248 248 """
249 249 # Only accept text that can be ASCII encoded.
250 250 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
251 251 text = QtGui.QApplication.clipboard().text()
252 252 if not text.isEmpty():
253 253 try:
254 254 str(text)
255 255 return True
256 256 except UnicodeEncodeError:
257 257 pass
258 258 return False
259 259
260 260 def clear(self, keep_input=True):
261 261 """ Clear the console, then write a new prompt. If 'keep_input' is set,
262 262 restores the old input buffer when the new prompt is written.
263 263 """
264 264 if keep_input:
265 265 input_buffer = self.input_buffer
266 266 self._control.clear()
267 267 self._show_prompt()
268 268 if keep_input:
269 269 self.input_buffer = input_buffer
270 270
271 271 def copy(self):
272 272 """ Copy the current selected text to the clipboard.
273 273 """
274 274 self._control.copy()
275 275
276 276 def execute(self, source=None, hidden=False, interactive=False):
277 277 """ Executes source or the input buffer, possibly prompting for more
278 278 input.
279 279
280 280 Parameters:
281 281 -----------
282 282 source : str, optional
283 283
284 284 The source to execute. If not specified, the input buffer will be
285 285 used. If specified and 'hidden' is False, the input buffer will be
286 286 replaced with the source before execution.
287 287
288 288 hidden : bool, optional (default False)
289 289
290 290 If set, no output will be shown and the prompt will not be modified.
291 291 In other words, it will be completely invisible to the user that
292 292 an execution has occurred.
293 293
294 294 interactive : bool, optional (default False)
295 295
296 296 Whether the console is to treat the source as having been manually
297 297 entered by the user. The effect of this parameter depends on the
298 298 subclass implementation.
299 299
300 300 Raises:
301 301 -------
302 302 RuntimeError
303 303 If incomplete input is given and 'hidden' is True. In this case,
304 304 it is not possible to prompt for more input.
305 305
306 306 Returns:
307 307 --------
308 308 A boolean indicating whether the source was executed.
309 309 """
310 310 # WARNING: The order in which things happen here is very particular, in
311 311 # large part because our syntax highlighting is fragile. If you change
312 312 # something, test carefully!
313 313
314 314 # Decide what to execute.
315 315 if source is None:
316 316 source = self.input_buffer
317 317 if not hidden:
318 318 # A newline is appended later, but it should be considered part
319 319 # of the input buffer.
320 320 source += '\n'
321 321 elif not hidden:
322 322 self.input_buffer = source
323 323
324 324 # Execute the source or show a continuation prompt if it is incomplete.
325 325 complete = self._is_complete(source, interactive)
326 326 if hidden:
327 327 if complete:
328 328 self._execute(source, hidden)
329 329 else:
330 330 error = 'Incomplete noninteractive input: "%s"'
331 331 raise RuntimeError(error % source)
332 332 else:
333 333 if complete:
334 334 self._append_plain_text('\n')
335 335 self._executing_input_buffer = self.input_buffer
336 336 self._executing = True
337 337 self._prompt_finished()
338 338
339 339 # The maximum block count is only in effect during execution.
340 340 # This ensures that _prompt_pos does not become invalid due to
341 341 # text truncation.
342 342 self._control.document().setMaximumBlockCount(self.buffer_size)
343 343
344 344 # Setting a positive maximum block count will automatically
345 345 # disable the undo/redo history, but just to be safe:
346 346 self._control.setUndoRedoEnabled(False)
347 347
348 348 self._execute(source, hidden)
349 349
350 350 else:
351 351 # Do this inside an edit block so continuation prompts are
352 352 # removed seamlessly via undo/redo.
353 353 cursor = self._get_end_cursor()
354 354 cursor.beginEditBlock()
355 355 cursor.insertText('\n')
356 356 self._insert_continuation_prompt(cursor)
357 357 cursor.endEditBlock()
358 358
359 359 # Do not do this inside the edit block. It works as expected
360 360 # when using a QPlainTextEdit control, but does not have an
361 361 # effect when using a QTextEdit. I believe this is a Qt bug.
362 362 self._control.moveCursor(QtGui.QTextCursor.End)
363 363
364 364 return complete
365 365
366 366 def _get_input_buffer(self):
367 367 """ The text that the user has entered entered at the current prompt.
368 368 """
369 369 # If we're executing, the input buffer may not even exist anymore due to
370 370 # the limit imposed by 'buffer_size'. Therefore, we store it.
371 371 if self._executing:
372 372 return self._executing_input_buffer
373 373
374 374 cursor = self._get_end_cursor()
375 375 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
376 376 input_buffer = str(cursor.selection().toPlainText())
377 377
378 378 # Strip out continuation prompts.
379 379 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
380 380
381 381 def _set_input_buffer(self, string):
382 382 """ Replaces the text in the input buffer with 'string'.
383 383 """
384 384 # For now, it is an error to modify the input buffer during execution.
385 385 if self._executing:
386 386 raise RuntimeError("Cannot change input buffer during execution.")
387 387
388 388 # Remove old text.
389 389 cursor = self._get_end_cursor()
390 390 cursor.beginEditBlock()
391 391 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
392 392 cursor.removeSelectedText()
393 393
394 394 # Insert new text with continuation prompts.
395 395 lines = string.splitlines(True)
396 396 if lines:
397 397 self._append_plain_text(lines[0])
398 398 for i in xrange(1, len(lines)):
399 399 if self._continuation_prompt_html is None:
400 400 self._append_plain_text(self._continuation_prompt)
401 401 else:
402 402 self._append_html(self._continuation_prompt_html)
403 403 self._append_plain_text(lines[i])
404 404 cursor.endEditBlock()
405 405 self._control.moveCursor(QtGui.QTextCursor.End)
406 406
407 407 input_buffer = property(_get_input_buffer, _set_input_buffer)
408 408
409 409 def _get_font(self):
410 410 """ The base font being used by the ConsoleWidget.
411 411 """
412 412 return self._control.document().defaultFont()
413 413
414 414 def _set_font(self, font):
415 415 """ Sets the base font for the ConsoleWidget to the specified QFont.
416 416 """
417 417 font_metrics = QtGui.QFontMetrics(font)
418 418 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
419 419
420 420 self._completion_widget.setFont(font)
421 421 self._control.document().setDefaultFont(font)
422 422 if self._page_control:
423 423 self._page_control.document().setDefaultFont(font)
424 424
425 425 font = property(_get_font, _set_font)
426 426
427 427 def paste(self, mode=QtGui.QClipboard.Clipboard):
428 428 """ Paste the contents of the clipboard into the input region.
429 429
430 430 Parameters:
431 431 -----------
432 432 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
433 433
434 434 Controls which part of the system clipboard is used. This can be
435 435 used to access the selection clipboard in X11 and the Find buffer
436 436 in Mac OS. By default, the regular clipboard is used.
437 437 """
438 438 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
439 439 try:
440 440 text = str(QtGui.QApplication.clipboard().text(mode))
441 441 except UnicodeEncodeError:
442 442 pass
443 443 else:
444 444 self._insert_plain_text_into_buffer(dedent(text))
445 445
446 446 def print_(self, printer):
447 447 """ Print the contents of the ConsoleWidget to the specified QPrinter.
448 448 """
449 449 self._control.print_(printer)
450 450
451 451 def redo(self):
452 452 """ Redo the last operation. If there is no operation to redo, nothing
453 453 happens.
454 454 """
455 455 self._control.redo()
456 456
457 457 def reset_font(self):
458 458 """ Sets the font to the default fixed-width font for this platform.
459 459 """
460 460 if sys.platform == 'win32':
461 461 # FIXME: we should test whether Consolas is available and use it
462 462 # first if it is. Consolas ships by default from Vista onwards,
463 463 # it's *vastly* more readable and prettier than Courier, and is
464 464 # often installed even on XP systems. So we should first check for
465 465 # it, and only fallback to Courier if absolutely necessary.
466 466 name = 'Courier'
467 467 elif sys.platform == 'darwin':
468 468 name = 'Monaco'
469 469 else:
470 470 name = 'Monospace'
471 471 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
472 472 font.setStyleHint(QtGui.QFont.TypeWriter)
473 473 self._set_font(font)
474 474
475 475 def select_all(self):
476 476 """ Selects all the text in the buffer.
477 477 """
478 478 self._control.selectAll()
479 479
480 480 def _get_tab_width(self):
481 481 """ The width (in terms of space characters) for tab characters.
482 482 """
483 483 return self._tab_width
484 484
485 485 def _set_tab_width(self, tab_width):
486 486 """ Sets the width (in terms of space characters) for tab characters.
487 487 """
488 488 font_metrics = QtGui.QFontMetrics(self.font)
489 489 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
490 490
491 491 self._tab_width = tab_width
492 492
493 493 tab_width = property(_get_tab_width, _set_tab_width)
494 494
495 495 def undo(self):
496 496 """ Undo the last operation. If there is no operation to undo, nothing
497 497 happens.
498 498 """
499 499 self._control.undo()
500 500
501 501 #---------------------------------------------------------------------------
502 502 # 'ConsoleWidget' abstract interface
503 503 #---------------------------------------------------------------------------
504 504
505 505 def _is_complete(self, source, interactive):
506 506 """ Returns whether 'source' can be executed. When triggered by an
507 507 Enter/Return key press, 'interactive' is True; otherwise, it is
508 508 False.
509 509 """
510 510 raise NotImplementedError
511 511
512 512 def _execute(self, source, hidden):
513 513 """ Execute 'source'. If 'hidden', do not show any output.
514 514 """
515 515 raise NotImplementedError
516 516
517 517 def _prompt_started_hook(self):
518 518 """ Called immediately after a new prompt is displayed.
519 519 """
520 520 pass
521 521
522 522 def _prompt_finished_hook(self):
523 523 """ Called immediately after a prompt is finished, i.e. when some input
524 524 will be processed and a new prompt displayed.
525 525 """
526 526 pass
527 527
528 528 def _up_pressed(self):
529 529 """ Called when the up key is pressed. Returns whether to continue
530 530 processing the event.
531 531 """
532 532 return True
533 533
534 534 def _down_pressed(self):
535 535 """ Called when the down key is pressed. Returns whether to continue
536 536 processing the event.
537 537 """
538 538 return True
539 539
540 540 def _tab_pressed(self):
541 541 """ Called when the tab key is pressed. Returns whether to continue
542 542 processing the event.
543 543 """
544 544 return False
545 545
546 546 #--------------------------------------------------------------------------
547 547 # 'ConsoleWidget' protected interface
548 548 #--------------------------------------------------------------------------
549 549
550 550 def _append_html(self, html):
551 551 """ Appends html at the end of the console buffer.
552 552 """
553 553 cursor = self._get_end_cursor()
554 554 self._insert_html(cursor, html)
555 555
556 556 def _append_html_fetching_plain_text(self, html):
557 557 """ Appends 'html', then returns the plain text version of it.
558 558 """
559 559 cursor = self._get_end_cursor()
560 560 return self._insert_html_fetching_plain_text(cursor, html)
561 561
562 562 def _append_plain_text(self, text):
563 563 """ Appends plain text at the end of the console buffer, processing
564 564 ANSI codes if enabled.
565 565 """
566 566 cursor = self._get_end_cursor()
567 567 self._insert_plain_text(cursor, text)
568 568
569 569 def _append_plain_text_keeping_prompt(self, text):
570 570 """ Writes 'text' after the current prompt, then restores the old prompt
571 571 with its old input buffer.
572 572 """
573 573 input_buffer = self.input_buffer
574 574 self._append_plain_text('\n')
575 575 self._prompt_finished()
576 576
577 577 self._append_plain_text(text)
578 578 self._show_prompt()
579 579 self.input_buffer = input_buffer
580 580
581 def _cancel_text_completion(self):
582 """ If text completion is progress, cancel it.
583 """
584 if self._text_completing_pos:
585 self._clear_temporary_buffer()
586 self._text_completing_pos = 0
587
581 588 def _clear_temporary_buffer(self):
582 589 """ Clears the "temporary text" buffer, i.e. all the text following
583 590 the prompt region.
584 591 """
592 # Select and remove all text below the input buffer.
585 593 cursor = self._get_prompt_cursor()
586 if cursor.movePosition(QtGui.QTextCursor.NextBlock):
587 prompt = self._continuation_prompt.lstrip()
588 while True:
589 temp_cursor = QtGui.QTextCursor(cursor)
590 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
591 text = str(temp_cursor.selection().toPlainText()).lstrip()
592 if not text.startswith(prompt) or \
593 not cursor.movePosition(QtGui.QTextCursor.NextBlock):
594 break
595 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
596 cursor.movePosition(QtGui.QTextCursor.End,
597 QtGui.QTextCursor.KeepAnchor)
598 cursor.removeSelectedText()
594 prompt = self._continuation_prompt.lstrip()
595 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
596 temp_cursor = QtGui.QTextCursor(cursor)
597 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
598 text = str(temp_cursor.selection().toPlainText()).lstrip()
599 if not text.startswith(prompt):
600 break
601 else:
602 # We've reached the end of the input buffer and no text follows.
603 return
604 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
605 cursor.movePosition(QtGui.QTextCursor.End,
606 QtGui.QTextCursor.KeepAnchor)
607 cursor.removeSelectedText()
608
609 # After doing this, we have no choice but to clear the undo/redo
610 # history. Otherwise, the text is not "temporary" at all, because it
611 # can be recalled with undo/redo. Unfortunately, Qt does not expose
612 # fine-grained control to the undo/redo system.
613 if self._control.isUndoRedoEnabled():
614 self._control.setUndoRedoEnabled(False)
615 self._control.setUndoRedoEnabled(True)
599 616
600 617 def _complete_with_items(self, cursor, items):
601 618 """ Performs completion with 'items' at the specified cursor location.
602 619 """
603 if self._text_completing_pos:
604 self._clear_temporary_buffer()
605 self._text_completing_pos = 0
620 self._cancel_text_completion()
606 621
607 622 if len(items) == 1:
608 623 cursor.setPosition(self._control.textCursor().position(),
609 624 QtGui.QTextCursor.KeepAnchor)
610 625 cursor.insertText(items[0])
611 626
612 627 elif len(items) > 1:
613 628 current_pos = self._control.textCursor().position()
614 629 prefix = commonprefix(items)
615 630 if prefix:
616 631 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
617 632 cursor.insertText(prefix)
618 633 current_pos = cursor.position()
619 634
620 635 if self.gui_completion:
621 636 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
622 637 self._completion_widget.show_items(cursor, items)
623 638 else:
624 639 cursor.beginEditBlock()
625 640 self._append_plain_text('\n')
626 641 self._page(self._format_as_columns(items))
627 642 cursor.endEditBlock()
628 643
629 644 cursor.setPosition(current_pos)
630 645 self._control.moveCursor(QtGui.QTextCursor.End)
631 646 self._control.setTextCursor(cursor)
632 647 self._text_completing_pos = current_pos
633 648
634 649 def _control_key_down(self, modifiers):
635 650 """ Given a KeyboardModifiers flags object, return whether the Control
636 651 key is down (on Mac OS, treat the Command key as a synonym for
637 652 Control).
638 653 """
639 654 down = bool(modifiers & QtCore.Qt.ControlModifier)
640 655
641 656 # Note: on Mac OS, ControlModifier corresponds to the Command key while
642 657 # MetaModifier corresponds to the Control key.
643 658 if sys.platform == 'darwin':
644 659 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
645 660
646 661 return down
647 662
648 663 def _create_control(self):
649 664 """ Creates and connects the underlying text widget.
650 665 """
651 666 # Create the underlying control.
652 667 if self.kind == 'plain':
653 668 control = ConsolePlainTextEdit()
654 669 elif self.kind == 'rich':
655 670 control = ConsoleTextEdit()
656 671 control.setAcceptRichText(False)
657 672
658 673 # Install event filters. The filter on the viewport is needed for
659 674 # mouse events.
660 675 control.installEventFilter(self)
661 676 control.viewport().installEventFilter(self)
662 677
663 678 # Connect signals.
664 679 control.cursorPositionChanged.connect(self._cursor_position_changed)
665 680 control.customContextMenuRequested.connect(self._show_context_menu)
666 681 control.copyAvailable.connect(self.copy_available)
667 682 control.redoAvailable.connect(self.redo_available)
668 683 control.undoAvailable.connect(self.undo_available)
669 684
670 685 # Configure the control.
671 686 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
672 687 control.setReadOnly(True)
673 688 control.setUndoRedoEnabled(False)
674 689 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
675 690 return control
676 691
677 692 def _create_page_control(self):
678 693 """ Creates and connects the underlying paging widget.
679 694 """
680 695 control = ConsolePlainTextEdit()
681 696 control.installEventFilter(self)
682 697 control.setReadOnly(True)
683 698 control.setUndoRedoEnabled(False)
684 699 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
685 700 return control
686 701
687 702 def _event_filter_console_keypress(self, event):
688 703 """ Filter key events for the underlying text widget to create a
689 704 console-like interface.
690 705 """
691 706 intercepted = False
692 707 cursor = self._control.textCursor()
693 708 position = cursor.position()
694 709 key = event.key()
695 710 ctrl_down = self._control_key_down(event.modifiers())
696 711 alt_down = event.modifiers() & QtCore.Qt.AltModifier
697 712 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
698 713
699 714 if event.matches(QtGui.QKeySequence.Paste):
700 715 # Call our paste instead of the underlying text widget's.
701 716 self.paste()
702 717 intercepted = True
703 718
704 719 elif ctrl_down:
705 720 if key == QtCore.Qt.Key_G:
706 721 self._keyboard_quit()
707 722 intercepted = True
708 723
709 724 elif key == QtCore.Qt.Key_K:
710 725 if self._in_buffer(position):
711 726 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
712 727 QtGui.QTextCursor.KeepAnchor)
713 728 if not cursor.hasSelection():
714 729 # Line deletion (remove continuation prompt)
715 730 cursor.movePosition(QtGui.QTextCursor.NextBlock,
716 731 QtGui.QTextCursor.KeepAnchor)
717 732 cursor.movePosition(QtGui.QTextCursor.Right,
718 733 QtGui.QTextCursor.KeepAnchor,
719 734 len(self._continuation_prompt))
720 735 cursor.removeSelectedText()
721 736 intercepted = True
722 737
723 738 elif key == QtCore.Qt.Key_L:
724 739 # It would be better to simply move the prompt block to the top
725 740 # of the control viewport. QPlainTextEdit has a private method
726 741 # to do this (setTopBlock), but it cannot be duplicated here
727 742 # because it requires access to the QTextControl that underlies
728 743 # both QPlainTextEdit and QTextEdit. In short, this can only be
729 744 # achieved by appending newlines after the prompt, which is a
730 745 # gigantic hack and likely to cause other problems.
731 746 self.clear()
732 747 intercepted = True
733 748
734 749 elif key == QtCore.Qt.Key_O:
735 750 if self._page_control and self._page_control.isVisible():
736 751 self._page_control.setFocus()
737 752 intercept = True
738 753
739 754 elif key == QtCore.Qt.Key_X:
740 755 # FIXME: Instead of disabling cut completely, only allow it
741 756 # when safe.
742 757 intercepted = True
743 758
744 759 elif key == QtCore.Qt.Key_Y:
745 760 self.paste()
746 761 intercepted = True
747 762
748 763 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
749 764 intercepted = True
750 765
751 766 elif alt_down:
752 767 if key == QtCore.Qt.Key_B:
753 768 self._set_cursor(self._get_word_start_cursor(position))
754 769 intercepted = True
755 770
756 771 elif key == QtCore.Qt.Key_F:
757 772 self._set_cursor(self._get_word_end_cursor(position))
758 773 intercepted = True
759 774
760 775 elif key == QtCore.Qt.Key_Backspace:
761 776 cursor = self._get_word_start_cursor(position)
762 777 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
763 778 cursor.removeSelectedText()
764 779 intercepted = True
765 780
766 781 elif key == QtCore.Qt.Key_D:
767 782 cursor = self._get_word_end_cursor(position)
768 783 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
769 784 cursor.removeSelectedText()
770 785 intercepted = True
771 786
772 787 elif key == QtCore.Qt.Key_Delete:
773 788 intercepted = True
774 789
775 790 elif key == QtCore.Qt.Key_Greater:
776 791 self._control.moveCursor(QtGui.QTextCursor.End)
777 792 intercepted = True
778 793
779 794 elif key == QtCore.Qt.Key_Less:
780 795 self._control.setTextCursor(self._get_prompt_cursor())
781 796 intercepted = True
782 797
783 798 else:
784 799 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
785 800 intercepted = True
786 801
787 802 # Special handling when tab completing in text mode.
788 if self._text_completing_pos:
789 self._clear_temporary_buffer()
790 self._text_completing_pos = 0
803 self._cancel_text_completion()
791 804
792 805 if self._in_buffer(position):
793 806 if self._reading:
794 807 self._append_plain_text('\n')
795 808 self._reading = False
796 809 if self._reading_callback:
797 810 self._reading_callback()
798 811
799 812 # If there is only whitespace after the cursor, execute.
800 813 # Otherwise, split the line with a continuation prompt.
801 814 elif not self._executing:
802 815 cursor.movePosition(QtGui.QTextCursor.End,
803 816 QtGui.QTextCursor.KeepAnchor)
804 817 at_end = cursor.selectedText().trimmed().isEmpty()
805 818 if at_end or shift_down:
806 819 self.execute(interactive = not shift_down)
807 820 else:
808 821 # Do this inside an edit block for clean undo/redo.
809 822 cursor.beginEditBlock()
810 823 cursor.setPosition(position)
811 824 cursor.insertText('\n')
812 825 self._insert_continuation_prompt(cursor)
813 826 cursor.endEditBlock()
814 827
815 828 # Ensure that the whole input buffer is visible.
816 829 # FIXME: This will not be usable if the input buffer
817 830 # is taller than the console widget.
818 831 self._control.moveCursor(QtGui.QTextCursor.End)
819 832 self._control.setTextCursor(cursor)
820 833
821 834 elif key == QtCore.Qt.Key_Escape:
822 835 self._keyboard_quit()
823 836 intercepted = True
824 837
825 838 elif key == QtCore.Qt.Key_Up:
826 839 if self._reading or not self._up_pressed():
827 840 intercepted = True
828 841 else:
829 842 prompt_line = self._get_prompt_cursor().blockNumber()
830 843 intercepted = cursor.blockNumber() <= prompt_line
831 844
832 845 elif key == QtCore.Qt.Key_Down:
833 846 if self._reading or not self._down_pressed():
834 847 intercepted = True
835 848 else:
836 849 end_line = self._get_end_cursor().blockNumber()
837 850 intercepted = cursor.blockNumber() == end_line
838 851
839 852 elif key == QtCore.Qt.Key_Tab:
840 853 if not self._reading:
841 854 intercepted = not self._tab_pressed()
842 855
843 856 elif key == QtCore.Qt.Key_Left:
844 857 intercepted = not self._in_buffer(position - 1)
845 858
846 859 elif key == QtCore.Qt.Key_Home:
847 860 start_line = cursor.blockNumber()
848 861 if start_line == self._get_prompt_cursor().blockNumber():
849 862 start_pos = self._prompt_pos
850 863 else:
851 864 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
852 865 QtGui.QTextCursor.KeepAnchor)
853 866 start_pos = cursor.position()
854 867 start_pos += len(self._continuation_prompt)
855 868 cursor.setPosition(position)
856 869 if shift_down and self._in_buffer(position):
857 870 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
858 871 else:
859 872 cursor.setPosition(start_pos)
860 873 self._set_cursor(cursor)
861 874 intercepted = True
862 875
863 876 elif key == QtCore.Qt.Key_Backspace:
864 877
865 878 # Line deletion (remove continuation prompt)
866 879 line, col = cursor.blockNumber(), cursor.columnNumber()
867 880 if not self._reading and \
868 881 col == len(self._continuation_prompt) and \
869 882 line > self._get_prompt_cursor().blockNumber():
870 883 cursor.beginEditBlock()
871 884 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
872 885 QtGui.QTextCursor.KeepAnchor)
873 886 cursor.removeSelectedText()
874 887 cursor.deletePreviousChar()
875 888 cursor.endEditBlock()
876 889 intercepted = True
877 890
878 891 # Regular backwards deletion
879 892 else:
880 893 anchor = cursor.anchor()
881 894 if anchor == position:
882 895 intercepted = not self._in_buffer(position - 1)
883 896 else:
884 897 intercepted = not self._in_buffer(min(anchor, position))
885 898
886 899 elif key == QtCore.Qt.Key_Delete:
887 900
888 901 # Line deletion (remove continuation prompt)
889 902 if not self._reading and self._in_buffer(position) and \
890 903 cursor.atBlockEnd() and not cursor.hasSelection():
891 904 cursor.movePosition(QtGui.QTextCursor.NextBlock,
892 905 QtGui.QTextCursor.KeepAnchor)
893 906 cursor.movePosition(QtGui.QTextCursor.Right,
894 907 QtGui.QTextCursor.KeepAnchor,
895 908 len(self._continuation_prompt))
896 909 cursor.removeSelectedText()
897 910 intercepted = True
898 911
899 912 # Regular forwards deletion:
900 913 else:
901 914 anchor = cursor.anchor()
902 915 intercepted = (not self._in_buffer(anchor) or
903 916 not self._in_buffer(position))
904 917
905 918 # Don't move the cursor if control is down to allow copy-paste using
906 919 # the keyboard in any part of the buffer.
907 920 if not ctrl_down:
908 921 self._keep_cursor_in_buffer()
909 922
910 923 return intercepted
911 924
912 925 def _event_filter_page_keypress(self, event):
913 926 """ Filter key events for the paging widget to create console-like
914 927 interface.
915 928 """
916 929 key = event.key()
917 930 ctrl_down = self._control_key_down(event.modifiers())
918 931 alt_down = event.modifiers() & QtCore.Qt.AltModifier
919 932
920 933 if ctrl_down:
921 934 if key == QtCore.Qt.Key_O:
922 935 self._control.setFocus()
923 936 intercept = True
924 937
925 938 elif alt_down:
926 939 if key == QtCore.Qt.Key_Greater:
927 940 self._page_control.moveCursor(QtGui.QTextCursor.End)
928 941 intercepted = True
929 942
930 943 elif key == QtCore.Qt.Key_Less:
931 944 self._page_control.moveCursor(QtGui.QTextCursor.Start)
932 945 intercepted = True
933 946
934 947 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
935 948 if self._splitter:
936 949 self._page_control.hide()
937 950 else:
938 951 self.layout().setCurrentWidget(self._control)
939 952 return True
940 953
941 954 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
942 955 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
943 956 QtCore.Qt.Key_PageDown,
944 957 QtCore.Qt.NoModifier)
945 958 QtGui.qApp.sendEvent(self._page_control, new_event)
946 959 return True
947 960
948 961 elif key == QtCore.Qt.Key_Backspace:
949 962 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
950 963 QtCore.Qt.Key_PageUp,
951 964 QtCore.Qt.NoModifier)
952 965 QtGui.qApp.sendEvent(self._page_control, new_event)
953 966 return True
954 967
955 968 return False
956 969
957 970 def _format_as_columns(self, items, separator=' '):
958 971 """ Transform a list of strings into a single string with columns.
959 972
960 973 Parameters
961 974 ----------
962 975 items : sequence of strings
963 976 The strings to process.
964 977
965 978 separator : str, optional [default is two spaces]
966 979 The string that separates columns.
967 980
968 981 Returns
969 982 -------
970 983 The formatted string.
971 984 """
972 985 # Note: this code is adapted from columnize 0.3.2.
973 986 # See http://code.google.com/p/pycolumnize/
974 987
975 988 # Calculate the number of characters available.
976 989 width = self._control.viewport().width()
977 990 char_width = QtGui.QFontMetrics(self.font).width(' ')
978 991 displaywidth = max(10, (width / char_width) - 1)
979 992
980 993 # Some degenerate cases.
981 994 size = len(items)
982 995 if size == 0:
983 996 return '\n'
984 997 elif size == 1:
985 998 return '%s\n' % str(items[0])
986 999
987 1000 # Try every row count from 1 upwards
988 1001 array_index = lambda nrows, row, col: nrows*col + row
989 1002 for nrows in range(1, size):
990 1003 ncols = (size + nrows - 1) // nrows
991 1004 colwidths = []
992 1005 totwidth = -len(separator)
993 1006 for col in range(ncols):
994 1007 # Get max column width for this column
995 1008 colwidth = 0
996 1009 for row in range(nrows):
997 1010 i = array_index(nrows, row, col)
998 1011 if i >= size: break
999 1012 x = items[i]
1000 1013 colwidth = max(colwidth, len(x))
1001 1014 colwidths.append(colwidth)
1002 1015 totwidth += colwidth + len(separator)
1003 1016 if totwidth > displaywidth:
1004 1017 break
1005 1018 if totwidth <= displaywidth:
1006 1019 break
1007 1020
1008 1021 # The smallest number of rows computed and the max widths for each
1009 1022 # column has been obtained. Now we just have to format each of the rows.
1010 1023 string = ''
1011 1024 for row in range(nrows):
1012 1025 texts = []
1013 1026 for col in range(ncols):
1014 1027 i = row + nrows*col
1015 1028 if i >= size:
1016 1029 texts.append('')
1017 1030 else:
1018 1031 texts.append(items[i])
1019 1032 while texts and not texts[-1]:
1020 1033 del texts[-1]
1021 1034 for col in range(len(texts)):
1022 1035 texts[col] = texts[col].ljust(colwidths[col])
1023 1036 string += '%s\n' % str(separator.join(texts))
1024 1037 return string
1025 1038
1026 1039 def _get_block_plain_text(self, block):
1027 1040 """ Given a QTextBlock, return its unformatted text.
1028 1041 """
1029 1042 cursor = QtGui.QTextCursor(block)
1030 1043 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1031 1044 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1032 1045 QtGui.QTextCursor.KeepAnchor)
1033 1046 return str(cursor.selection().toPlainText())
1034 1047
1035 1048 def _get_cursor(self):
1036 1049 """ Convenience method that returns a cursor for the current position.
1037 1050 """
1038 1051 return self._control.textCursor()
1039 1052
1040 1053 def _get_end_cursor(self):
1041 1054 """ Convenience method that returns a cursor for the last character.
1042 1055 """
1043 1056 cursor = self._control.textCursor()
1044 1057 cursor.movePosition(QtGui.QTextCursor.End)
1045 1058 return cursor
1046 1059
1047 1060 def _get_input_buffer_cursor_column(self):
1048 1061 """ Returns the column of the cursor in the input buffer, excluding the
1049 1062 contribution by the prompt, or -1 if there is no such column.
1050 1063 """
1051 1064 prompt = self._get_input_buffer_cursor_prompt()
1052 1065 if prompt is None:
1053 1066 return -1
1054 1067 else:
1055 1068 cursor = self._control.textCursor()
1056 1069 return cursor.columnNumber() - len(prompt)
1057 1070
1058 1071 def _get_input_buffer_cursor_line(self):
1059 1072 """ Returns line of the input buffer that contains the cursor, or None
1060 1073 if there is no such line.
1061 1074 """
1062 1075 prompt = self._get_input_buffer_cursor_prompt()
1063 1076 if prompt is None:
1064 1077 return None
1065 1078 else:
1066 1079 cursor = self._control.textCursor()
1067 1080 text = self._get_block_plain_text(cursor.block())
1068 1081 return text[len(prompt):]
1069 1082
1070 1083 def _get_input_buffer_cursor_prompt(self):
1071 1084 """ Returns the (plain text) prompt for line of the input buffer that
1072 1085 contains the cursor, or None if there is no such line.
1073 1086 """
1074 1087 if self._executing:
1075 1088 return None
1076 1089 cursor = self._control.textCursor()
1077 1090 if cursor.position() >= self._prompt_pos:
1078 1091 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1079 1092 return self._prompt
1080 1093 else:
1081 1094 return self._continuation_prompt
1082 1095 else:
1083 1096 return None
1084 1097
1085 1098 def _get_prompt_cursor(self):
1086 1099 """ Convenience method that returns a cursor for the prompt position.
1087 1100 """
1088 1101 cursor = self._control.textCursor()
1089 1102 cursor.setPosition(self._prompt_pos)
1090 1103 return cursor
1091 1104
1092 1105 def _get_selection_cursor(self, start, end):
1093 1106 """ Convenience method that returns a cursor with text selected between
1094 1107 the positions 'start' and 'end'.
1095 1108 """
1096 1109 cursor = self._control.textCursor()
1097 1110 cursor.setPosition(start)
1098 1111 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1099 1112 return cursor
1100 1113
1101 1114 def _get_word_start_cursor(self, position):
1102 1115 """ Find the start of the word to the left the given position. If a
1103 1116 sequence of non-word characters precedes the first word, skip over
1104 1117 them. (This emulates the behavior of bash, emacs, etc.)
1105 1118 """
1106 1119 document = self._control.document()
1107 1120 position -= 1
1108 1121 while position >= self._prompt_pos and \
1109 1122 not document.characterAt(position).isLetterOrNumber():
1110 1123 position -= 1
1111 1124 while position >= self._prompt_pos and \
1112 1125 document.characterAt(position).isLetterOrNumber():
1113 1126 position -= 1
1114 1127 cursor = self._control.textCursor()
1115 1128 cursor.setPosition(position + 1)
1116 1129 return cursor
1117 1130
1118 1131 def _get_word_end_cursor(self, position):
1119 1132 """ Find the end of the word to the right the given position. If a
1120 1133 sequence of non-word characters precedes the first word, skip over
1121 1134 them. (This emulates the behavior of bash, emacs, etc.)
1122 1135 """
1123 1136 document = self._control.document()
1124 1137 end = self._get_end_cursor().position()
1125 1138 while position < end and \
1126 1139 not document.characterAt(position).isLetterOrNumber():
1127 1140 position += 1
1128 1141 while position < end and \
1129 1142 document.characterAt(position).isLetterOrNumber():
1130 1143 position += 1
1131 1144 cursor = self._control.textCursor()
1132 1145 cursor.setPosition(position)
1133 1146 return cursor
1134 1147
1135 1148 def _insert_continuation_prompt(self, cursor):
1136 1149 """ Inserts new continuation prompt using the specified cursor.
1137 1150 """
1138 1151 if self._continuation_prompt_html is None:
1139 1152 self._insert_plain_text(cursor, self._continuation_prompt)
1140 1153 else:
1141 1154 self._continuation_prompt = self._insert_html_fetching_plain_text(
1142 1155 cursor, self._continuation_prompt_html)
1143 1156
1144 1157 def _insert_html(self, cursor, html):
1145 1158 """ Inserts HTML using the specified cursor in such a way that future
1146 1159 formatting is unaffected.
1147 1160 """
1148 1161 cursor.beginEditBlock()
1149 1162 cursor.insertHtml(html)
1150 1163
1151 1164 # After inserting HTML, the text document "remembers" it's in "html
1152 1165 # mode", which means that subsequent calls adding plain text will result
1153 1166 # in unwanted formatting, lost tab characters, etc. The following code
1154 1167 # hacks around this behavior, which I consider to be a bug in Qt, by
1155 1168 # (crudely) resetting the document's style state.
1156 1169 cursor.movePosition(QtGui.QTextCursor.Left,
1157 1170 QtGui.QTextCursor.KeepAnchor)
1158 1171 if cursor.selection().toPlainText() == ' ':
1159 1172 cursor.removeSelectedText()
1160 1173 else:
1161 1174 cursor.movePosition(QtGui.QTextCursor.Right)
1162 1175 cursor.insertText(' ', QtGui.QTextCharFormat())
1163 1176 cursor.endEditBlock()
1164 1177
1165 1178 def _insert_html_fetching_plain_text(self, cursor, html):
1166 1179 """ Inserts HTML using the specified cursor, then returns its plain text
1167 1180 version.
1168 1181 """
1169 1182 cursor.beginEditBlock()
1170 1183 cursor.removeSelectedText()
1171 1184
1172 1185 start = cursor.position()
1173 1186 self._insert_html(cursor, html)
1174 1187 end = cursor.position()
1175 1188 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1176 1189 text = str(cursor.selection().toPlainText())
1177 1190
1178 1191 cursor.setPosition(end)
1179 1192 cursor.endEditBlock()
1180 1193 return text
1181 1194
1182 1195 def _insert_plain_text(self, cursor, text):
1183 1196 """ Inserts plain text using the specified cursor, processing ANSI codes
1184 1197 if enabled.
1185 1198 """
1186 1199 cursor.beginEditBlock()
1187 1200 if self.ansi_codes:
1188 1201 for substring in self._ansi_processor.split_string(text):
1189 1202 for action in self._ansi_processor.actions:
1190 1203 if action.kind == 'erase' and action.area == 'screen':
1191 1204 cursor.select(QtGui.QTextCursor.Document)
1192 1205 cursor.removeSelectedText()
1193 1206 format = self._ansi_processor.get_format()
1194 1207 cursor.insertText(substring, format)
1195 1208 else:
1196 1209 cursor.insertText(text)
1197 1210 cursor.endEditBlock()
1198 1211
1199 1212 def _insert_plain_text_into_buffer(self, text):
1200 1213 """ Inserts text into the input buffer at the current cursor position,
1201 1214 ensuring that continuation prompts are inserted as necessary.
1202 1215 """
1203 1216 lines = str(text).splitlines(True)
1204 1217 if lines:
1205 1218 self._keep_cursor_in_buffer()
1206 1219 cursor = self._control.textCursor()
1207 1220 cursor.beginEditBlock()
1208 1221 cursor.insertText(lines[0])
1209 1222 for line in lines[1:]:
1210 1223 if self._continuation_prompt_html is None:
1211 1224 cursor.insertText(self._continuation_prompt)
1212 1225 else:
1213 1226 self._continuation_prompt = \
1214 1227 self._insert_html_fetching_plain_text(
1215 1228 cursor, self._continuation_prompt_html)
1216 1229 cursor.insertText(line)
1217 1230 cursor.endEditBlock()
1218 1231 self._control.setTextCursor(cursor)
1219 1232
1220 1233 def _in_buffer(self, position=None):
1221 1234 """ Returns whether the current cursor (or, if specified, a position) is
1222 1235 inside the editing region.
1223 1236 """
1224 1237 cursor = self._control.textCursor()
1225 1238 if position is None:
1226 1239 position = cursor.position()
1227 1240 else:
1228 1241 cursor.setPosition(position)
1229 1242 line = cursor.blockNumber()
1230 1243 prompt_line = self._get_prompt_cursor().blockNumber()
1231 1244 if line == prompt_line:
1232 1245 return position >= self._prompt_pos
1233 1246 elif line > prompt_line:
1234 1247 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1235 1248 prompt_pos = cursor.position() + len(self._continuation_prompt)
1236 1249 return position >= prompt_pos
1237 1250 return False
1238 1251
1239 1252 def _keep_cursor_in_buffer(self):
1240 1253 """ Ensures that the cursor is inside the editing region. Returns
1241 1254 whether the cursor was moved.
1242 1255 """
1243 1256 moved = not self._in_buffer()
1244 1257 if moved:
1245 1258 cursor = self._control.textCursor()
1246 1259 cursor.movePosition(QtGui.QTextCursor.End)
1247 1260 self._control.setTextCursor(cursor)
1248 1261 return moved
1249 1262
1250 1263 def _keyboard_quit(self):
1251 1264 """ Cancels the current editing task ala Ctrl-G in Emacs.
1252 1265 """
1253 1266 if self._text_completing_pos:
1254 self._clear_temporary_buffer()
1255 self._text_completing_pos = 0
1267 self._cancel_text_completion()
1256 1268 else:
1257 1269 self.input_buffer = ''
1258 1270
1259 1271 def _page(self, text):
1260 1272 """ Displays text using the pager if it exceeds the height of the
1261 1273 visible area.
1262 1274 """
1263 1275 if self.paging == 'none':
1264 1276 self._append_plain_text(text)
1265 1277 else:
1266 1278 line_height = QtGui.QFontMetrics(self.font).height()
1267 1279 minlines = self._control.viewport().height() / line_height
1268 1280 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1269 1281 if self.paging == 'custom':
1270 1282 self.custom_page_requested.emit(text)
1271 1283 else:
1272 1284 self._page_control.clear()
1273 1285 cursor = self._page_control.textCursor()
1274 1286 self._insert_plain_text(cursor, text)
1275 1287 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1276 1288
1277 1289 self._page_control.viewport().resize(self._control.size())
1278 1290 if self._splitter:
1279 1291 self._page_control.show()
1280 1292 self._page_control.setFocus()
1281 1293 else:
1282 1294 self.layout().setCurrentWidget(self._page_control)
1283 1295 else:
1284 1296 self._append_plain_text(text)
1285 1297
1286 1298 def _prompt_started(self):
1287 1299 """ Called immediately after a new prompt is displayed.
1288 1300 """
1289 1301 # Temporarily disable the maximum block count to permit undo/redo and
1290 1302 # to ensure that the prompt position does not change due to truncation.
1291 1303 # Because setting this property clears the undo/redo history, we only
1292 1304 # set it if we have to.
1293 1305 if self._control.document().maximumBlockCount() > 0:
1294 1306 self._control.document().setMaximumBlockCount(0)
1295 1307 self._control.setUndoRedoEnabled(True)
1296 1308
1297 1309 self._control.setReadOnly(False)
1298 1310 self._control.moveCursor(QtGui.QTextCursor.End)
1299 1311
1300 1312 self._executing = False
1301 1313 self._prompt_started_hook()
1302 1314
1303 1315 def _prompt_finished(self):
1304 1316 """ Called immediately after a prompt is finished, i.e. when some input
1305 1317 will be processed and a new prompt displayed.
1306 1318 """
1307 1319 self._control.setReadOnly(True)
1308 1320 self._prompt_finished_hook()
1309 1321
1310 1322 def _readline(self, prompt='', callback=None):
1311 1323 """ Reads one line of input from the user.
1312 1324
1313 1325 Parameters
1314 1326 ----------
1315 1327 prompt : str, optional
1316 1328 The prompt to print before reading the line.
1317 1329
1318 1330 callback : callable, optional
1319 1331 A callback to execute with the read line. If not specified, input is
1320 1332 read *synchronously* and this method does not return until it has
1321 1333 been read.
1322 1334
1323 1335 Returns
1324 1336 -------
1325 1337 If a callback is specified, returns nothing. Otherwise, returns the
1326 1338 input string with the trailing newline stripped.
1327 1339 """
1328 1340 if self._reading:
1329 1341 raise RuntimeError('Cannot read a line. Widget is already reading.')
1330 1342
1331 1343 if not callback and not self.isVisible():
1332 1344 # If the user cannot see the widget, this function cannot return.
1333 1345 raise RuntimeError('Cannot synchronously read a line if the widget '
1334 1346 'is not visible!')
1335 1347
1336 1348 self._reading = True
1337 1349 self._show_prompt(prompt, newline=False)
1338 1350
1339 1351 if callback is None:
1340 1352 self._reading_callback = None
1341 1353 while self._reading:
1342 1354 QtCore.QCoreApplication.processEvents()
1343 1355 return self.input_buffer.rstrip('\n')
1344 1356
1345 1357 else:
1346 1358 self._reading_callback = lambda: \
1347 1359 callback(self.input_buffer.rstrip('\n'))
1348 1360
1349 1361 def _set_continuation_prompt(self, prompt, html=False):
1350 1362 """ Sets the continuation prompt.
1351 1363
1352 1364 Parameters
1353 1365 ----------
1354 1366 prompt : str
1355 1367 The prompt to show when more input is needed.
1356 1368
1357 1369 html : bool, optional (default False)
1358 1370 If set, the prompt will be inserted as formatted HTML. Otherwise,
1359 1371 the prompt will be treated as plain text, though ANSI color codes
1360 1372 will be handled.
1361 1373 """
1362 1374 if html:
1363 1375 self._continuation_prompt_html = prompt
1364 1376 else:
1365 1377 self._continuation_prompt = prompt
1366 1378 self._continuation_prompt_html = None
1367 1379
1368 1380 def _set_cursor(self, cursor):
1369 1381 """ Convenience method to set the current cursor.
1370 1382 """
1371 1383 self._control.setTextCursor(cursor)
1372 1384
1373 1385 def _show_context_menu(self, pos):
1374 1386 """ Shows a context menu at the given QPoint (in widget coordinates).
1375 1387 """
1376 1388 menu = QtGui.QMenu()
1377 1389
1378 1390 copy_action = menu.addAction('Copy', self.copy)
1379 1391 copy_action.setEnabled(self._get_cursor().hasSelection())
1380 1392 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1381 1393
1382 1394 paste_action = menu.addAction('Paste', self.paste)
1383 1395 paste_action.setEnabled(self.can_paste())
1384 1396 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1385 1397
1386 1398 menu.addSeparator()
1387 1399 menu.addAction('Select All', self.select_all)
1388 1400
1389 1401 menu.exec_(self._control.mapToGlobal(pos))
1390 1402
1391 1403 def _show_prompt(self, prompt=None, html=False, newline=True):
1392 1404 """ Writes a new prompt at the end of the buffer.
1393 1405
1394 1406 Parameters
1395 1407 ----------
1396 1408 prompt : str, optional
1397 1409 The prompt to show. If not specified, the previous prompt is used.
1398 1410
1399 1411 html : bool, optional (default False)
1400 1412 Only relevant when a prompt is specified. If set, the prompt will
1401 1413 be inserted as formatted HTML. Otherwise, the prompt will be treated
1402 1414 as plain text, though ANSI color codes will be handled.
1403 1415
1404 1416 newline : bool, optional (default True)
1405 1417 If set, a new line will be written before showing the prompt if
1406 1418 there is not already a newline at the end of the buffer.
1407 1419 """
1408 1420 # Insert a preliminary newline, if necessary.
1409 1421 if newline:
1410 1422 cursor = self._get_end_cursor()
1411 1423 if cursor.position() > 0:
1412 1424 cursor.movePosition(QtGui.QTextCursor.Left,
1413 1425 QtGui.QTextCursor.KeepAnchor)
1414 1426 if str(cursor.selection().toPlainText()) != '\n':
1415 1427 self._append_plain_text('\n')
1416 1428
1417 1429 # Write the prompt.
1418 1430 self._append_plain_text(self._prompt_sep)
1419 1431 if prompt is None:
1420 1432 if self._prompt_html is None:
1421 1433 self._append_plain_text(self._prompt)
1422 1434 else:
1423 1435 self._append_html(self._prompt_html)
1424 1436 else:
1425 1437 if html:
1426 1438 self._prompt = self._append_html_fetching_plain_text(prompt)
1427 1439 self._prompt_html = prompt
1428 1440 else:
1429 1441 self._append_plain_text(prompt)
1430 1442 self._prompt = prompt
1431 1443 self._prompt_html = None
1432 1444
1433 1445 self._prompt_pos = self._get_end_cursor().position()
1434 1446 self._prompt_started()
1435 1447
1436 1448 #------ Signal handlers ----------------------------------------------------
1437 1449
1438 1450 def _cursor_position_changed(self):
1439 1451 """ Clears the temporary buffer based on the cursor position.
1440 1452 """
1441 1453 if self._text_completing_pos:
1442 1454 document = self._control.document()
1443 1455 if self._text_completing_pos < document.characterCount():
1444 1456 cursor = self._control.textCursor()
1445 1457 pos = cursor.position()
1446 1458 text_cursor = self._control.textCursor()
1447 1459 text_cursor.setPosition(self._text_completing_pos)
1448 1460 if pos < self._text_completing_pos or \
1449 1461 cursor.blockNumber() > text_cursor.blockNumber():
1450 1462 self._clear_temporary_buffer()
1451 1463 self._text_completing_pos = 0
1452 1464 else:
1453 1465 self._clear_temporary_buffer()
1454 1466 self._text_completing_pos = 0
1455 1467
1456 1468
1457 1469 class HistoryConsoleWidget(ConsoleWidget):
1458 1470 """ A ConsoleWidget that keeps a history of the commands that have been
1459 1471 executed.
1460 1472 """
1461 1473
1462 1474 #---------------------------------------------------------------------------
1463 1475 # 'object' interface
1464 1476 #---------------------------------------------------------------------------
1465 1477
1466 1478 def __init__(self, *args, **kw):
1467 1479 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1468 1480 self._history = []
1469 1481 self._history_index = 0
1470 1482
1471 1483 #---------------------------------------------------------------------------
1472 1484 # 'ConsoleWidget' public interface
1473 1485 #---------------------------------------------------------------------------
1474 1486
1475 1487 def execute(self, source=None, hidden=False, interactive=False):
1476 1488 """ Reimplemented to the store history.
1477 1489 """
1478 1490 if not hidden:
1479 1491 history = self.input_buffer if source is None else source
1480 1492
1481 1493 executed = super(HistoryConsoleWidget, self).execute(
1482 1494 source, hidden, interactive)
1483 1495
1484 1496 if executed and not hidden:
1485 1497 # Save the command unless it was a blank line.
1486 1498 history = history.rstrip()
1487 1499 if history:
1488 1500 self._history.append(history)
1489 1501 self._history_index = len(self._history)
1490 1502
1491 1503 return executed
1492 1504
1493 1505 #---------------------------------------------------------------------------
1494 1506 # 'ConsoleWidget' abstract interface
1495 1507 #---------------------------------------------------------------------------
1496 1508
1497 1509 def _up_pressed(self):
1498 1510 """ Called when the up key is pressed. Returns whether to continue
1499 1511 processing the event.
1500 1512 """
1501 1513 prompt_cursor = self._get_prompt_cursor()
1502 1514 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1503 1515 self.history_previous()
1504 1516
1505 1517 # Go to the first line of prompt for seemless history scrolling.
1506 1518 cursor = self._get_prompt_cursor()
1507 1519 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1508 1520 self._set_cursor(cursor)
1509 1521
1510 1522 return False
1511 1523 return True
1512 1524
1513 1525 def _down_pressed(self):
1514 1526 """ Called when the down key is pressed. Returns whether to continue
1515 1527 processing the event.
1516 1528 """
1517 1529 end_cursor = self._get_end_cursor()
1518 1530 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1519 1531 self.history_next()
1520 1532 return False
1521 1533 return True
1522 1534
1523 1535 #---------------------------------------------------------------------------
1524 1536 # 'HistoryConsoleWidget' public interface
1525 1537 #---------------------------------------------------------------------------
1526 1538
1527 1539 def history_previous(self):
1528 1540 """ If possible, set the input buffer to the previous item in the
1529 1541 history.
1530 1542 """
1531 1543 if self._history_index > 0:
1532 1544 self._history_index -= 1
1533 1545 self.input_buffer = self._history[self._history_index]
1534 1546
1535 1547 def history_next(self):
1536 1548 """ Set the input buffer to the next item in the history, or a blank
1537 1549 line if there is no subsequent item.
1538 1550 """
1539 1551 if self._history_index < len(self._history):
1540 1552 self._history_index += 1
1541 1553 if self._history_index < len(self._history):
1542 1554 self.input_buffer = self._history[self._history_index]
1543 1555 else:
1544 1556 self.input_buffer = ''
1545 1557
1546 1558 #---------------------------------------------------------------------------
1547 1559 # 'HistoryConsoleWidget' protected interface
1548 1560 #---------------------------------------------------------------------------
1549 1561
1550 1562 def _set_history(self, history):
1551 1563 """ Replace the current history with a sequence of history items.
1552 1564 """
1553 1565 self._history = list(history)
1554 1566 self._history_index = len(self._history)
@@ -1,414 +1,419 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3
4 4 TODO: Add support for retrieving the system default editor. Requires code
5 5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 6 Linux (use the xdg system).
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Imports
11 11 #-----------------------------------------------------------------------------
12 12
13 13 # Standard library imports
14 14 from collections import namedtuple
15 15 import re
16 16 from subprocess import Popen
17 17
18 18 # System library imports
19 19 from PyQt4 import QtCore, QtGui
20 20
21 21 # Local imports
22 22 from IPython.core.inputsplitter import IPythonInputSplitter
23 23 from IPython.core.usage import default_banner
24 24 from IPython.utils.traitlets import Bool, Str
25 25 from frontend_widget import FrontendWidget
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Constants
29 29 #-----------------------------------------------------------------------------
30 30
31 31 # The default light style sheet: black text on a white background.
32 32 default_light_style_sheet = '''
33 33 .error { color: red; }
34 34 .in-prompt { color: navy; }
35 35 .in-prompt-number { font-weight: bold; }
36 36 .out-prompt { color: darkred; }
37 37 .out-prompt-number { font-weight: bold; }
38 38 '''
39 39 default_light_syntax_style = 'default'
40 40
41 41 # The default dark style sheet: white text on a black background.
42 42 default_dark_style_sheet = '''
43 43 QPlainTextEdit, QTextEdit { background-color: black; color: white }
44 44 QFrame { border: 1px solid grey; }
45 45 .error { color: red; }
46 46 .in-prompt { color: lime; }
47 47 .in-prompt-number { color: lime; font-weight: bold; }
48 48 .out-prompt { color: red; }
49 49 .out-prompt-number { color: red; font-weight: bold; }
50 50 '''
51 51 default_dark_syntax_style = 'monokai'
52 52
53 53 # Default strings to build and display input and output prompts (and separators
54 54 # in between)
55 55 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
56 56 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
57 57 default_input_sep = '\n'
58 58 default_output_sep = ''
59 59 default_output_sep2 = ''
60 60
61 61 #-----------------------------------------------------------------------------
62 62 # IPythonWidget class
63 63 #-----------------------------------------------------------------------------
64 64
65 65 class IPythonWidget(FrontendWidget):
66 66 """ A FrontendWidget for an IPython kernel.
67 67 """
68 68
69 69 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
70 70 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
71 71 # settings.
72 72 custom_edit = Bool(False)
73 73 custom_edit_requested = QtCore.pyqtSignal(object, object)
74 74
75 75 # A command for invoking a system text editor. If the string contains a
76 76 # {filename} format specifier, it will be used. Otherwise, the filename will
77 77 # be appended to the end the command.
78 78 editor = Str('default', config=True)
79 79
80 80 # The editor command to use when a specific line number is requested. The
81 81 # string should contain two format specifiers: {line} and {filename}. If
82 82 # this parameter is not specified, the line number option to the %edit magic
83 83 # will be ignored.
84 84 editor_line = Str(config=True)
85 85
86 86 # A CSS stylesheet. The stylesheet can contain classes for:
87 87 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
88 88 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
89 89 # 3. IPython: .error, .in-prompt, .out-prompt, etc
90 90 style_sheet = Str(config=True)
91 91
92 92 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
93 93 # the style sheet is queried for Pygments style information.
94 94 syntax_style = Str(config=True)
95 95
96 96 # Prompts.
97 97 in_prompt = Str(default_in_prompt, config=True)
98 98 out_prompt = Str(default_out_prompt, config=True)
99 99 input_sep = Str(default_input_sep, config=True)
100 100 output_sep = Str(default_output_sep, config=True)
101 101 output_sep2 = Str(default_output_sep2, config=True)
102 102
103 103 # FrontendWidget protected class variables.
104 104 _input_splitter_class = IPythonInputSplitter
105 105
106 106 # IPythonWidget protected class variables.
107 107 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
108 108 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
109 109 _payload_source_page = 'IPython.zmq.page.page'
110 110
111 111 #---------------------------------------------------------------------------
112 112 # 'object' interface
113 113 #---------------------------------------------------------------------------
114 114
115 115 def __init__(self, *args, **kw):
116 116 super(IPythonWidget, self).__init__(*args, **kw)
117 117
118 118 # IPythonWidget protected variables.
119 119 self._previous_prompt_obj = None
120 120
121 121 # Initialize widget styling.
122 122 if self.style_sheet:
123 123 self._style_sheet_changed()
124 124 self._syntax_style_changed()
125 125 else:
126 126 self.set_default_style()
127 127
128 128 #---------------------------------------------------------------------------
129 129 # 'BaseFrontendMixin' abstract interface
130 130 #---------------------------------------------------------------------------
131 131
132 132 def _handle_complete_reply(self, rep):
133 133 """ Reimplemented to support IPython's improved completion machinery.
134 134 """
135 135 cursor = self._get_cursor()
136 136 info = self._request_info.get('complete')
137 137 if info and info.id == rep['parent_header']['msg_id'] and \
138 138 info.pos == cursor.position():
139 139 matches = rep['content']['matches']
140 140 text = rep['content']['matched_text']
141
142 # Clean up matches with '.'s and path separators.
143 parts = re.split(r'[./\\]', text)
144 sep_count = len(parts) - 1
145 if sep_count:
146 chop_length = sum(map(len, parts[:sep_count])) + sep_count
147 matches = [ match[chop_length:] for match in matches ]
148 text = text[chop_length:]
141 offset = len(text)
142
143 # Clean up matches with period and path separators if the matched
144 # text has not been transformed. This is done by truncating all
145 # but the last component and then suitably decreasing the offset
146 # between the current cursor position and the start of completion.
147 if len(matches) > 1 and matches[0][:offset] == text:
148 parts = re.split(r'[./\\]', text)
149 sep_count = len(parts) - 1
150 if sep_count:
151 chop_length = sum(map(len, parts[:sep_count])) + sep_count
152 matches = [ match[chop_length:] for match in matches ]
153 offset -= chop_length
149 154
150 155 # Move the cursor to the start of the match and complete.
151 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
156 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
152 157 self._complete_with_items(cursor, matches)
153 158
154 159 def _handle_execute_reply(self, msg):
155 160 """ Reimplemented to support prompt requests.
156 161 """
157 162 info = self._request_info.get('execute')
158 163 if info and info.id == msg['parent_header']['msg_id']:
159 164 if info.kind == 'prompt':
160 165 number = msg['content']['execution_count'] + 1
161 166 self._show_interpreter_prompt(number)
162 167 else:
163 168 super(IPythonWidget, self)._handle_execute_reply(msg)
164 169
165 170 def _handle_history_reply(self, msg):
166 171 """ Implemented to handle history replies, which are only supported by
167 172 the IPython kernel.
168 173 """
169 174 history_dict = msg['content']['history']
170 175 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
171 176 self._set_history(items)
172 177
173 178 def _handle_pyout(self, msg):
174 179 """ Reimplemented for IPython-style "display hook".
175 180 """
176 181 if not self._hidden and self._is_from_this_session(msg):
177 182 content = msg['content']
178 183 prompt_number = content['execution_count']
179 184 self._append_plain_text(self.output_sep)
180 185 self._append_html(self._make_out_prompt(prompt_number))
181 186 self._append_plain_text(content['data']+self.output_sep2)
182 187
183 188 def _started_channels(self):
184 189 """ Reimplemented to make a history request.
185 190 """
186 191 super(IPythonWidget, self)._started_channels()
187 192 # FIXME: Disabled until history requests are properly implemented.
188 193 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
189 194
190 195 #---------------------------------------------------------------------------
191 196 # 'FrontendWidget' interface
192 197 #---------------------------------------------------------------------------
193 198
194 199 def execute_file(self, path, hidden=False):
195 200 """ Reimplemented to use the 'run' magic.
196 201 """
197 202 self.execute('%%run %s' % path, hidden=hidden)
198 203
199 204 #---------------------------------------------------------------------------
200 205 # 'FrontendWidget' protected interface
201 206 #---------------------------------------------------------------------------
202 207
203 208 def _complete(self):
204 209 """ Reimplemented to support IPython's improved completion machinery.
205 210 """
206 211 # We let the kernel split the input line, so we *always* send an empty
207 212 # text field. Readline-based frontends do get a real text field which
208 213 # they can use.
209 214 text = ''
210 215
211 216 # Send the completion request to the kernel
212 217 msg_id = self.kernel_manager.xreq_channel.complete(
213 218 text, # text
214 219 self._get_input_buffer_cursor_line(), # line
215 220 self._get_input_buffer_cursor_column(), # cursor_pos
216 221 self.input_buffer) # block
217 222 pos = self._get_cursor().position()
218 223 info = self._CompletionRequest(msg_id, pos)
219 224 self._request_info['complete'] = info
220 225
221 226 def _get_banner(self):
222 227 """ Reimplemented to return IPython's default banner.
223 228 """
224 229 return default_banner + '\n'
225 230
226 231 def _process_execute_error(self, msg):
227 232 """ Reimplemented for IPython-style traceback formatting.
228 233 """
229 234 content = msg['content']
230 235 traceback = '\n'.join(content['traceback']) + '\n'
231 236 if False:
232 237 # FIXME: For now, tracebacks come as plain text, so we can't use
233 238 # the html renderer yet. Once we refactor ultratb to produce
234 239 # properly styled tracebacks, this branch should be the default
235 240 traceback = traceback.replace(' ', '&nbsp;')
236 241 traceback = traceback.replace('\n', '<br/>')
237 242
238 243 ename = content['ename']
239 244 ename_styled = '<span class="error">%s</span>' % ename
240 245 traceback = traceback.replace(ename, ename_styled)
241 246
242 247 self._append_html(traceback)
243 248 else:
244 249 # This is the fallback for now, using plain text with ansi escapes
245 250 self._append_plain_text(traceback)
246 251
247 252 def _process_execute_payload(self, item):
248 253 """ Reimplemented to handle %edit and paging payloads.
249 254 """
250 255 if item['source'] == self._payload_source_edit:
251 256 self._edit(item['filename'], item['line_number'])
252 257 return True
253 258 elif item['source'] == self._payload_source_page:
254 259 self._page(item['data'])
255 260 return True
256 261 else:
257 262 return False
258 263
259 264 def _show_interpreter_prompt(self, number=None):
260 265 """ Reimplemented for IPython-style prompts.
261 266 """
262 267 # If a number was not specified, make a prompt number request.
263 268 if number is None:
264 269 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
265 270 info = self._ExecutionRequest(msg_id, 'prompt')
266 271 self._request_info['execute'] = info
267 272 return
268 273
269 274 # Show a new prompt and save information about it so that it can be
270 275 # updated later if the prompt number turns out to be wrong.
271 276 self._prompt_sep = self.input_sep
272 277 self._show_prompt(self._make_in_prompt(number), html=True)
273 278 block = self._control.document().lastBlock()
274 279 length = len(self._prompt)
275 280 self._previous_prompt_obj = self._PromptBlock(block, length, number)
276 281
277 282 # Update continuation prompt to reflect (possibly) new prompt length.
278 283 self._set_continuation_prompt(
279 284 self._make_continuation_prompt(self._prompt), html=True)
280 285
281 286 def _show_interpreter_prompt_for_reply(self, msg):
282 287 """ Reimplemented for IPython-style prompts.
283 288 """
284 289 # Update the old prompt number if necessary.
285 290 content = msg['content']
286 291 previous_prompt_number = content['execution_count']
287 292 if self._previous_prompt_obj and \
288 293 self._previous_prompt_obj.number != previous_prompt_number:
289 294 block = self._previous_prompt_obj.block
290 295
291 296 # Make sure the prompt block has not been erased.
292 297 if block.isValid() and not block.text().isEmpty():
293 298
294 299 # Remove the old prompt and insert a new prompt.
295 300 cursor = QtGui.QTextCursor(block)
296 301 cursor.movePosition(QtGui.QTextCursor.Right,
297 302 QtGui.QTextCursor.KeepAnchor,
298 303 self._previous_prompt_obj.length)
299 304 prompt = self._make_in_prompt(previous_prompt_number)
300 305 self._prompt = self._insert_html_fetching_plain_text(
301 306 cursor, prompt)
302 307
303 308 # When the HTML is inserted, Qt blows away the syntax
304 309 # highlighting for the line, so we need to rehighlight it.
305 310 self._highlighter.rehighlightBlock(cursor.block())
306 311
307 312 self._previous_prompt_obj = None
308 313
309 314 # Show a new prompt with the kernel's estimated prompt number.
310 315 self._show_interpreter_prompt(previous_prompt_number+1)
311 316
312 317 #---------------------------------------------------------------------------
313 318 # 'IPythonWidget' interface
314 319 #---------------------------------------------------------------------------
315 320
316 321 def set_default_style(self, lightbg=True):
317 322 """ Sets the widget style to the class defaults.
318 323
319 324 Parameters:
320 325 -----------
321 326 lightbg : bool, optional (default True)
322 327 Whether to use the default IPython light background or dark
323 328 background style.
324 329 """
325 330 if lightbg:
326 331 self.style_sheet = default_light_style_sheet
327 332 self.syntax_style = default_light_syntax_style
328 333 else:
329 334 self.style_sheet = default_dark_style_sheet
330 335 self.syntax_style = default_dark_syntax_style
331 336
332 337 #---------------------------------------------------------------------------
333 338 # 'IPythonWidget' protected interface
334 339 #---------------------------------------------------------------------------
335 340
336 341 def _edit(self, filename, line=None):
337 342 """ Opens a Python script for editing.
338 343
339 344 Parameters:
340 345 -----------
341 346 filename : str
342 347 A path to a local system file.
343 348
344 349 line : int, optional
345 350 A line of interest in the file.
346 351 """
347 352 if self.custom_edit:
348 353 self.custom_edit_requested.emit(filename, line)
349 354 elif self.editor == 'default':
350 355 self._append_plain_text('No default editor available.\n')
351 356 else:
352 357 try:
353 358 filename = '"%s"' % filename
354 359 if line and self.editor_line:
355 360 command = self.editor_line.format(filename=filename,
356 361 line=line)
357 362 else:
358 363 try:
359 364 command = self.editor.format()
360 365 except KeyError:
361 366 command = self.editor.format(filename=filename)
362 367 else:
363 368 command += ' ' + filename
364 369 except KeyError:
365 370 self._append_plain_text('Invalid editor command.\n')
366 371 else:
367 372 try:
368 373 Popen(command, shell=True)
369 374 except OSError:
370 375 msg = 'Opening editor with command "%s" failed.\n'
371 376 self._append_plain_text(msg % command)
372 377
373 378 def _make_in_prompt(self, number):
374 379 """ Given a prompt number, returns an HTML In prompt.
375 380 """
376 381 body = self.in_prompt % number
377 382 return '<span class="in-prompt">%s</span>' % body
378 383
379 384 def _make_continuation_prompt(self, prompt):
380 385 """ Given a plain text version of an In prompt, returns an HTML
381 386 continuation prompt.
382 387 """
383 388 end_chars = '...: '
384 389 space_count = len(prompt.lstrip('\n')) - len(end_chars)
385 390 body = '&nbsp;' * space_count + end_chars
386 391 return '<span class="in-prompt">%s</span>' % body
387 392
388 393 def _make_out_prompt(self, number):
389 394 """ Given a prompt number, returns an HTML Out prompt.
390 395 """
391 396 body = self.out_prompt % number
392 397 return '<span class="out-prompt">%s</span>' % body
393 398
394 399 #------ Trait change handlers ---------------------------------------------
395 400
396 401 def _style_sheet_changed(self):
397 402 """ Set the style sheets of the underlying widgets.
398 403 """
399 404 self.setStyleSheet(self.style_sheet)
400 405 self._control.document().setDefaultStyleSheet(self.style_sheet)
401 406 if self._page_control:
402 407 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
403 408
404 409 bg_color = self._control.palette().background().color()
405 410 self._ansi_processor.set_background_color(bg_color)
406 411
407 412 def _syntax_style_changed(self):
408 413 """ Set the style for the syntax highlighter.
409 414 """
410 415 if self.syntax_style:
411 416 self._highlighter.set_style(self.syntax_style)
412 417 else:
413 418 self._highlighter.set_style_sheet(self.style_sheet)
414 419
General Comments 0
You need to be logged in to leave comments. Login now