##// END OF EJS Templates
Fix to ensure that the paging widget is styled appropriately.
epatters -
Show More
@@ -1,1229 +1,1231 b''
1 1 # Standard library imports
2 2 import sys
3 3 from textwrap import dedent
4 4
5 5 # System library imports
6 6 from PyQt4 import QtCore, QtGui
7 7
8 8 # Local imports
9 9 from ansi_code_processor import QtAnsiCodeProcessor
10 10 from completion_widget import CompletionWidget
11 11
12 12
13 13 class ConsoleWidget(QtGui.QWidget):
14 14 """ An abstract base class for console-type widgets. This class has
15 15 functionality for:
16 16
17 17 * Maintaining a prompt and editing region
18 18 * Providing the traditional Unix-style console keyboard shortcuts
19 19 * Performing tab completion
20 20 * Paging text
21 21 * Handling ANSI escape codes
22 22
23 23 ConsoleWidget also provides a number of utility methods that will be
24 24 convenient to implementors of a console-style widget.
25 25 """
26 26
27 27 # Whether to process ANSI escape codes.
28 28 ansi_codes = True
29 29
30 30 # The maximum number of lines of text before truncation.
31 31 buffer_size = 500
32 32
33 33 # Whether to use a list widget or plain text output for tab completion.
34 34 gui_completion = True
35 35
36 36 # Whether to override ShortcutEvents for the keybindings defined by this
37 37 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
38 38 # priority (when it has focus) over, e.g., window-level menu shortcuts.
39 39 override_shortcuts = False
40 40
41 41 # Signals that indicate ConsoleWidget state.
42 42 copy_available = QtCore.pyqtSignal(bool)
43 43 redo_available = QtCore.pyqtSignal(bool)
44 44 undo_available = QtCore.pyqtSignal(bool)
45 45
46 46 # Signal emitted when paging is needed and the paging style has been
47 47 # specified as 'custom'.
48 48 custom_page_requested = QtCore.pyqtSignal(QtCore.QString)
49 49
50 50 # Protected class variables.
51 51 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
52 52 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
53 53 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
54 54 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
55 55 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
56 56 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
57 57 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
58 58 _shortcuts = set(_ctrl_down_remap.keys() +
59 59 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
60 60
61 61 #---------------------------------------------------------------------------
62 62 # 'QObject' interface
63 63 #---------------------------------------------------------------------------
64 64
65 65 def __init__(self, kind='plain', paging='inside', parent=None):
66 66 """ Create a ConsoleWidget.
67 67
68 68 Parameters
69 69 ----------
70 70 kind : str, optional [default 'plain']
71 71 The type of underlying text widget to use. Valid values are 'plain',
72 72 which specifies a QPlainTextEdit, and 'rich', which specifies a
73 73 QTextEdit.
74 74
75 75 paging : str, optional [default 'inside']
76 76 The type of paging to use. Valid values are:
77 77 'inside' : The widget pages like a traditional terminal pager.
78 78 'hsplit' : When paging is requested, the widget is split
79 79 horizontally. The top pane contains the console,
80 80 and the bottom pane contains the paged text.
81 81 'vsplit' : Similar to 'hsplit', except that a vertical splitter
82 82 used.
83 83 'custom' : No action is taken by the widget beyond emitting a
84 84 'custom_page_requested(QString)' signal.
85 85 'none' : The text is written directly to the console.
86 86
87 87 parent : QWidget, optional [default None]
88 88 The parent for this widget.
89 89 """
90 90 super(ConsoleWidget, self).__init__(parent)
91 91
92 92 # Create the layout and underlying text widget.
93 93 layout = QtGui.QStackedLayout(self)
94 94 layout.setMargin(0)
95 95 self._control = self._create_control(kind)
96 96 self._page_control = None
97 97 self._splitter = None
98 98 if paging in ('hsplit', 'vsplit'):
99 99 self._splitter = QtGui.QSplitter()
100 100 if paging == 'hsplit':
101 101 self._splitter.setOrientation(QtCore.Qt.Horizontal)
102 102 else:
103 103 self._splitter.setOrientation(QtCore.Qt.Vertical)
104 104 self._splitter.addWidget(self._control)
105 105 layout.addWidget(self._splitter)
106 106 else:
107 107 layout.addWidget(self._control)
108 108
109 109 # Create the paging widget, if necessary.
110 110 self._page_style = paging
111 111 if paging in ('inside', 'hsplit', 'vsplit'):
112 112 self._page_control = self._create_page_control()
113 113 if self._splitter:
114 114 self._page_control.hide()
115 115 self._splitter.addWidget(self._page_control)
116 116 else:
117 117 layout.addWidget(self._page_control)
118 118 elif paging not in ('custom', 'none'):
119 119 raise ValueError('Paging style %s unknown.' % repr(paging))
120 120
121 121 # Initialize protected variables. Some variables contain useful state
122 122 # information for subclasses; they should be considered read-only.
123 123 self._ansi_processor = QtAnsiCodeProcessor()
124 124 self._completion_widget = CompletionWidget(self._control)
125 125 self._continuation_prompt = '> '
126 126 self._continuation_prompt_html = None
127 127 self._executing = False
128 128 self._prompt = ''
129 129 self._prompt_html = None
130 130 self._prompt_pos = 0
131 131 self._reading = False
132 132 self._reading_callback = None
133 133 self._tab_width = 8
134 134
135 135 # Set a monospaced font.
136 136 self.reset_font()
137 137
138 138 def eventFilter(self, obj, event):
139 139 """ Reimplemented to ensure a console-like behavior in the underlying
140 140 text widget.
141 141 """
142 142 # Re-map keys for all filtered widgets.
143 143 etype = event.type()
144 144 if etype == QtCore.QEvent.KeyPress and \
145 145 self._control_key_down(event.modifiers()) and \
146 146 event.key() in self._ctrl_down_remap:
147 147 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
148 148 self._ctrl_down_remap[event.key()],
149 149 QtCore.Qt.NoModifier)
150 150 QtGui.qApp.sendEvent(obj, new_event)
151 151 return True
152 152
153 153 # Override shortucts for all filtered widgets. Note that on Mac OS it is
154 154 # always unnecessary to override shortcuts, hence the check below (users
155 155 # should just use the Control key instead of the Command key).
156 156 elif etype == QtCore.QEvent.ShortcutOverride and \
157 157 sys.platform != 'darwin' and \
158 158 self._control_key_down(event.modifiers()) and \
159 159 event.key() in self._shortcuts:
160 160 event.accept()
161 161 return False
162 162
163 163 elif obj == self._control:
164 164 # Disable moving text by drag and drop.
165 165 if etype == QtCore.QEvent.DragMove:
166 166 return True
167 167
168 168 elif etype == QtCore.QEvent.KeyPress:
169 169 return self._event_filter_console_keypress(event)
170 170
171 171 elif obj == self._page_control:
172 172 if etype == QtCore.QEvent.KeyPress:
173 173 return self._event_filter_page_keypress(event)
174 174
175 175 return super(ConsoleWidget, self).eventFilter(obj, event)
176 176
177 177 #---------------------------------------------------------------------------
178 178 # 'QWidget' interface
179 179 #---------------------------------------------------------------------------
180 180
181 181 def sizeHint(self):
182 182 """ Reimplemented to suggest a size that is 80 characters wide and
183 183 25 lines high.
184 184 """
185 185 style = self.style()
186 186 opt = QtGui.QStyleOptionHeader()
187 187 font_metrics = QtGui.QFontMetrics(self.font)
188 188 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
189 189
190 190 width = font_metrics.width(' ') * 80
191 191 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
192 192 if self._page_style == 'hsplit':
193 193 width = width * 2 + splitwidth
194 194
195 195 height = font_metrics.height() * 25
196 196 if self._page_style == 'vsplit':
197 197 height = height * 2 + splitwidth
198 198
199 199 return QtCore.QSize(width, height)
200 200
201 201 #---------------------------------------------------------------------------
202 202 # 'ConsoleWidget' public interface
203 203 #---------------------------------------------------------------------------
204 204
205 205 def can_paste(self):
206 206 """ Returns whether text can be pasted from the clipboard.
207 207 """
208 208 # Accept only text that can be ASCII encoded.
209 209 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
210 210 text = QtGui.QApplication.clipboard().text()
211 211 if not text.isEmpty():
212 212 try:
213 213 str(text)
214 214 return True
215 215 except UnicodeEncodeError:
216 216 pass
217 217 return False
218 218
219 219 def clear(self, keep_input=False):
220 220 """ Clear the console, then write a new prompt. If 'keep_input' is set,
221 221 restores the old input buffer when the new prompt is written.
222 222 """
223 223 self._control.clear()
224 224 if keep_input:
225 225 input_buffer = self.input_buffer
226 226 self._show_prompt()
227 227 if keep_input:
228 228 self.input_buffer = input_buffer
229 229
230 230 def copy(self):
231 231 """ Copy the current selected text to the clipboard.
232 232 """
233 233 self._control.copy()
234 234
235 235 def execute(self, source=None, hidden=False, interactive=False):
236 236 """ Executes source or the input buffer, possibly prompting for more
237 237 input.
238 238
239 239 Parameters:
240 240 -----------
241 241 source : str, optional
242 242
243 243 The source to execute. If not specified, the input buffer will be
244 244 used. If specified and 'hidden' is False, the input buffer will be
245 245 replaced with the source before execution.
246 246
247 247 hidden : bool, optional (default False)
248 248
249 249 If set, no output will be shown and the prompt will not be modified.
250 250 In other words, it will be completely invisible to the user that
251 251 an execution has occurred.
252 252
253 253 interactive : bool, optional (default False)
254 254
255 255 Whether the console is to treat the source as having been manually
256 256 entered by the user. The effect of this parameter depends on the
257 257 subclass implementation.
258 258
259 259 Raises:
260 260 -------
261 261 RuntimeError
262 262 If incomplete input is given and 'hidden' is True. In this case,
263 263 it is not possible to prompt for more input.
264 264
265 265 Returns:
266 266 --------
267 267 A boolean indicating whether the source was executed.
268 268 """
269 269 if not hidden:
270 270 if source is not None:
271 271 self.input_buffer = source
272 272
273 273 self._append_plain_text('\n')
274 274 self._executing_input_buffer = self.input_buffer
275 275 self._executing = True
276 276 self._prompt_finished()
277 277
278 278 real_source = self.input_buffer if source is None else source
279 279 complete = self._is_complete(real_source, interactive)
280 280 if complete:
281 281 if not hidden:
282 282 # The maximum block count is only in effect during execution.
283 283 # This ensures that _prompt_pos does not become invalid due to
284 284 # text truncation.
285 285 self._control.document().setMaximumBlockCount(self.buffer_size)
286 286 self._execute(real_source, hidden)
287 287 elif hidden:
288 288 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
289 289 else:
290 290 self._show_continuation_prompt()
291 291
292 292 return complete
293 293
294 294 def _get_input_buffer(self):
295 295 """ The text that the user has entered entered at the current prompt.
296 296 """
297 297 # If we're executing, the input buffer may not even exist anymore due to
298 298 # the limit imposed by 'buffer_size'. Therefore, we store it.
299 299 if self._executing:
300 300 return self._executing_input_buffer
301 301
302 302 cursor = self._get_end_cursor()
303 303 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
304 304 input_buffer = str(cursor.selection().toPlainText())
305 305
306 306 # Strip out continuation prompts.
307 307 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
308 308
309 309 def _set_input_buffer(self, string):
310 310 """ Replaces the text in the input buffer with 'string'.
311 311 """
312 312 # For now, it is an error to modify the input buffer during execution.
313 313 if self._executing:
314 314 raise RuntimeError("Cannot change input buffer during execution.")
315 315
316 316 # Remove old text.
317 317 cursor = self._get_end_cursor()
318 318 cursor.beginEditBlock()
319 319 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
320 320 cursor.removeSelectedText()
321 321
322 322 # Insert new text with continuation prompts.
323 323 lines = string.splitlines(True)
324 324 if lines:
325 325 self._append_plain_text(lines[0])
326 326 for i in xrange(1, len(lines)):
327 327 if self._continuation_prompt_html is None:
328 328 self._append_plain_text(self._continuation_prompt)
329 329 else:
330 330 self._append_html(self._continuation_prompt_html)
331 331 self._append_plain_text(lines[i])
332 332 cursor.endEditBlock()
333 333 self._control.moveCursor(QtGui.QTextCursor.End)
334 334
335 335 input_buffer = property(_get_input_buffer, _set_input_buffer)
336 336
337 337 def _get_font(self):
338 338 """ The base font being used by the ConsoleWidget.
339 339 """
340 340 return self._control.document().defaultFont()
341 341
342 342 def _set_font(self, font):
343 343 """ Sets the base font for the ConsoleWidget to the specified QFont.
344 344 """
345 345 font_metrics = QtGui.QFontMetrics(font)
346 346 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
347 347
348 348 self._completion_widget.setFont(font)
349 349 self._control.document().setDefaultFont(font)
350 if self._page_control:
351 self._page_control.document().setDefaultFont(font)
350 352
351 353 font = property(_get_font, _set_font)
352 354
353 355 def paste(self):
354 356 """ Paste the contents of the clipboard into the input region.
355 357 """
356 358 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
357 359 try:
358 360 text = str(QtGui.QApplication.clipboard().text())
359 361 except UnicodeEncodeError:
360 362 pass
361 363 else:
362 364 self._insert_into_buffer(dedent(text))
363 365
364 366 def print_(self, printer):
365 367 """ Print the contents of the ConsoleWidget to the specified QPrinter.
366 368 """
367 369 self._control.print_(printer)
368 370
369 371 def redo(self):
370 372 """ Redo the last operation. If there is no operation to redo, nothing
371 373 happens.
372 374 """
373 375 self._control.redo()
374 376
375 377 def reset_font(self):
376 378 """ Sets the font to the default fixed-width font for this platform.
377 379 """
378 380 if sys.platform == 'win32':
379 381 name = 'Courier'
380 382 elif sys.platform == 'darwin':
381 383 name = 'Monaco'
382 384 else:
383 385 name = 'Monospace'
384 386 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
385 387 font.setStyleHint(QtGui.QFont.TypeWriter)
386 388 self._set_font(font)
387 389
388 390 def select_all(self):
389 391 """ Selects all the text in the buffer.
390 392 """
391 393 self._control.selectAll()
392 394
393 395 def _get_tab_width(self):
394 396 """ The width (in terms of space characters) for tab characters.
395 397 """
396 398 return self._tab_width
397 399
398 400 def _set_tab_width(self, tab_width):
399 401 """ Sets the width (in terms of space characters) for tab characters.
400 402 """
401 403 font_metrics = QtGui.QFontMetrics(self.font)
402 404 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
403 405
404 406 self._tab_width = tab_width
405 407
406 408 tab_width = property(_get_tab_width, _set_tab_width)
407 409
408 410 def undo(self):
409 411 """ Undo the last operation. If there is no operation to undo, nothing
410 412 happens.
411 413 """
412 414 self._control.undo()
413 415
414 416 #---------------------------------------------------------------------------
415 417 # 'ConsoleWidget' abstract interface
416 418 #---------------------------------------------------------------------------
417 419
418 420 def _is_complete(self, source, interactive):
419 421 """ Returns whether 'source' can be executed. When triggered by an
420 422 Enter/Return key press, 'interactive' is True; otherwise, it is
421 423 False.
422 424 """
423 425 raise NotImplementedError
424 426
425 427 def _execute(self, source, hidden):
426 428 """ Execute 'source'. If 'hidden', do not show any output.
427 429 """
428 430 raise NotImplementedError
429 431
430 432 def _execute_interrupt(self):
431 433 """ Attempts to stop execution. Returns whether this method has an
432 434 implementation.
433 435 """
434 436 return False
435 437
436 438 def _prompt_started_hook(self):
437 439 """ Called immediately after a new prompt is displayed.
438 440 """
439 441 pass
440 442
441 443 def _prompt_finished_hook(self):
442 444 """ Called immediately after a prompt is finished, i.e. when some input
443 445 will be processed and a new prompt displayed.
444 446 """
445 447 pass
446 448
447 449 def _up_pressed(self):
448 450 """ Called when the up key is pressed. Returns whether to continue
449 451 processing the event.
450 452 """
451 453 return True
452 454
453 455 def _down_pressed(self):
454 456 """ Called when the down key is pressed. Returns whether to continue
455 457 processing the event.
456 458 """
457 459 return True
458 460
459 461 def _tab_pressed(self):
460 462 """ Called when the tab key is pressed. Returns whether to continue
461 463 processing the event.
462 464 """
463 465 return False
464 466
465 467 #--------------------------------------------------------------------------
466 468 # 'ConsoleWidget' protected interface
467 469 #--------------------------------------------------------------------------
468 470
469 471 def _append_html(self, html):
470 472 """ Appends html at the end of the console buffer.
471 473 """
472 474 cursor = self._get_end_cursor()
473 475 self._insert_html(cursor, html)
474 476
475 477 def _append_html_fetching_plain_text(self, html):
476 478 """ Appends 'html', then returns the plain text version of it.
477 479 """
478 480 anchor = self._get_end_cursor().position()
479 481 self._append_html(html)
480 482 cursor = self._get_end_cursor()
481 483 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
482 484 return str(cursor.selection().toPlainText())
483 485
484 486 def _append_plain_text(self, text):
485 487 """ Appends plain text at the end of the console buffer, processing
486 488 ANSI codes if enabled.
487 489 """
488 490 cursor = self._get_end_cursor()
489 491 self._insert_plain_text(cursor, text)
490 492
491 493 def _append_plain_text_keeping_prompt(self, text):
492 494 """ Writes 'text' after the current prompt, then restores the old prompt
493 495 with its old input buffer.
494 496 """
495 497 input_buffer = self.input_buffer
496 498 self._append_plain_text('\n')
497 499 self._prompt_finished()
498 500
499 501 self._append_plain_text(text)
500 502 self._show_prompt()
501 503 self.input_buffer = input_buffer
502 504
503 505 def _complete_with_items(self, cursor, items):
504 506 """ Performs completion with 'items' at the specified cursor location.
505 507 """
506 508 if len(items) == 1:
507 509 cursor.setPosition(self._control.textCursor().position(),
508 510 QtGui.QTextCursor.KeepAnchor)
509 511 cursor.insertText(items[0])
510 512 elif len(items) > 1:
511 513 if self.gui_completion:
512 514 self._completion_widget.show_items(cursor, items)
513 515 else:
514 516 text = self._format_as_columns(items)
515 517 self._append_plain_text_keeping_prompt(text)
516 518
517 519 def _control_key_down(self, modifiers):
518 520 """ Given a KeyboardModifiers flags object, return whether the Control
519 521 key is down (on Mac OS, treat the Command key as a synonym for
520 522 Control).
521 523 """
522 524 down = bool(modifiers & QtCore.Qt.ControlModifier)
523 525
524 526 # Note: on Mac OS, ControlModifier corresponds to the Command key while
525 527 # MetaModifier corresponds to the Control key.
526 528 if sys.platform == 'darwin':
527 529 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
528 530
529 531 return down
530 532
531 533 def _create_control(self, kind):
532 534 """ Creates and connects the underlying text widget.
533 535 """
534 536 if kind == 'plain':
535 537 control = QtGui.QPlainTextEdit()
536 538 elif kind == 'rich':
537 539 control = QtGui.QTextEdit()
538 540 control.setAcceptRichText(False)
539 541 else:
540 542 raise ValueError("Kind %s unknown." % repr(kind))
541 543 control.installEventFilter(self)
542 544 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
543 545 control.customContextMenuRequested.connect(self._show_context_menu)
544 546 control.copyAvailable.connect(self.copy_available)
545 547 control.redoAvailable.connect(self.redo_available)
546 548 control.undoAvailable.connect(self.undo_available)
547 549 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
548 550 return control
549 551
550 552 def _create_page_control(self):
551 553 """ Creates and connects the underlying paging widget.
552 554 """
553 555 control = QtGui.QPlainTextEdit()
554 556 control.installEventFilter(self)
555 557 control.setReadOnly(True)
556 558 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
557 559 return control
558 560
559 561 def _event_filter_console_keypress(self, event):
560 562 """ Filter key events for the underlying text widget to create a
561 563 console-like interface.
562 564 """
563 565 intercepted = False
564 566 cursor = self._control.textCursor()
565 567 position = cursor.position()
566 568 key = event.key()
567 569 ctrl_down = self._control_key_down(event.modifiers())
568 570 alt_down = event.modifiers() & QtCore.Qt.AltModifier
569 571 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
570 572
571 573 if event.matches(QtGui.QKeySequence.Paste):
572 574 # Call our paste instead of the underlying text widget's.
573 575 self.paste()
574 576 intercepted = True
575 577
576 578 elif ctrl_down:
577 579 if key == QtCore.Qt.Key_C:
578 580 intercepted = self._executing and self._execute_interrupt()
579 581
580 582 elif key == QtCore.Qt.Key_K:
581 583 if self._in_buffer(position):
582 584 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
583 585 QtGui.QTextCursor.KeepAnchor)
584 586 cursor.removeSelectedText()
585 587 intercepted = True
586 588
587 589 elif key == QtCore.Qt.Key_X:
588 590 intercepted = True
589 591
590 592 elif key == QtCore.Qt.Key_Y:
591 593 self.paste()
592 594 intercepted = True
593 595
594 596 elif alt_down:
595 597 if key == QtCore.Qt.Key_B:
596 598 self._set_cursor(self._get_word_start_cursor(position))
597 599 intercepted = True
598 600
599 601 elif key == QtCore.Qt.Key_F:
600 602 self._set_cursor(self._get_word_end_cursor(position))
601 603 intercepted = True
602 604
603 605 elif key == QtCore.Qt.Key_Backspace:
604 606 cursor = self._get_word_start_cursor(position)
605 607 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
606 608 cursor.removeSelectedText()
607 609 intercepted = True
608 610
609 611 elif key == QtCore.Qt.Key_D:
610 612 cursor = self._get_word_end_cursor(position)
611 613 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
612 614 cursor.removeSelectedText()
613 615 intercepted = True
614 616
615 617 else:
616 618 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
617 619 if self._reading:
618 620 self._append_plain_text('\n')
619 621 self._reading = False
620 622 if self._reading_callback:
621 623 self._reading_callback()
622 624 elif not self._executing:
623 625 self.execute(interactive=True)
624 626 intercepted = True
625 627
626 628 elif key == QtCore.Qt.Key_Up:
627 629 if self._reading or not self._up_pressed():
628 630 intercepted = True
629 631 else:
630 632 prompt_line = self._get_prompt_cursor().blockNumber()
631 633 intercepted = cursor.blockNumber() <= prompt_line
632 634
633 635 elif key == QtCore.Qt.Key_Down:
634 636 if self._reading or not self._down_pressed():
635 637 intercepted = True
636 638 else:
637 639 end_line = self._get_end_cursor().blockNumber()
638 640 intercepted = cursor.blockNumber() == end_line
639 641
640 642 elif key == QtCore.Qt.Key_Tab:
641 643 if self._reading:
642 644 intercepted = False
643 645 else:
644 646 intercepted = not self._tab_pressed()
645 647
646 648 elif key == QtCore.Qt.Key_Left:
647 649 intercepted = not self._in_buffer(position - 1)
648 650
649 651 elif key == QtCore.Qt.Key_Home:
650 652 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
651 653 start_line = cursor.blockNumber()
652 654 if start_line == self._get_prompt_cursor().blockNumber():
653 655 start_pos = self._prompt_pos
654 656 else:
655 657 start_pos = cursor.position()
656 658 start_pos += len(self._continuation_prompt)
657 659 if shift_down and self._in_buffer(position):
658 660 self._set_selection(position, start_pos)
659 661 else:
660 662 self._set_position(start_pos)
661 663 intercepted = True
662 664
663 665 elif key == QtCore.Qt.Key_Backspace:
664 666
665 667 # Line deletion (remove continuation prompt)
666 668 len_prompt = len(self._continuation_prompt)
667 669 if not self._reading and \
668 670 cursor.columnNumber() == len_prompt and \
669 671 position != self._prompt_pos:
670 672 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
671 673 QtGui.QTextCursor.KeepAnchor)
672 674 cursor.removeSelectedText()
673 675
674 676 # Regular backwards deletion
675 677 else:
676 678 anchor = cursor.anchor()
677 679 if anchor == position:
678 680 intercepted = not self._in_buffer(position - 1)
679 681 else:
680 682 intercepted = not self._in_buffer(min(anchor, position))
681 683
682 684 elif key == QtCore.Qt.Key_Delete:
683 685 anchor = cursor.anchor()
684 686 intercepted = not self._in_buffer(min(anchor, position))
685 687
686 688 # Don't move the cursor if control is down to allow copy-paste using
687 689 # the keyboard in any part of the buffer.
688 690 if not ctrl_down:
689 691 self._keep_cursor_in_buffer()
690 692
691 693 return intercepted
692 694
693 695 def _event_filter_page_keypress(self, event):
694 696 """ Filter key events for the paging widget to create console-like
695 697 interface.
696 698 """
697 699 key = event.key()
698 700
699 701 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
700 702 if self._splitter:
701 703 self._page_control.hide()
702 704 else:
703 705 self.layout().setCurrentWidget(self._control)
704 706 return True
705 707
706 708 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
707 709 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
708 710 QtCore.Qt.Key_Down,
709 711 QtCore.Qt.NoModifier)
710 712 QtGui.qApp.sendEvent(self._page_control, new_event)
711 713 return True
712 714
713 715 return False
714 716
715 717 def _format_as_columns(self, items, separator=' '):
716 718 """ Transform a list of strings into a single string with columns.
717 719
718 720 Parameters
719 721 ----------
720 722 items : sequence of strings
721 723 The strings to process.
722 724
723 725 separator : str, optional [default is two spaces]
724 726 The string that separates columns.
725 727
726 728 Returns
727 729 -------
728 730 The formatted string.
729 731 """
730 732 # Note: this code is adapted from columnize 0.3.2.
731 733 # See http://code.google.com/p/pycolumnize/
732 734
733 735 width = self._control.viewport().width()
734 736 char_width = QtGui.QFontMetrics(self.font).width(' ')
735 737 displaywidth = max(5, width / char_width)
736 738
737 739 # Some degenerate cases.
738 740 size = len(items)
739 741 if size == 0:
740 742 return '\n'
741 743 elif size == 1:
742 744 return '%s\n' % str(items[0])
743 745
744 746 # Try every row count from 1 upwards
745 747 array_index = lambda nrows, row, col: nrows*col + row
746 748 for nrows in range(1, size):
747 749 ncols = (size + nrows - 1) // nrows
748 750 colwidths = []
749 751 totwidth = -len(separator)
750 752 for col in range(ncols):
751 753 # Get max column width for this column
752 754 colwidth = 0
753 755 for row in range(nrows):
754 756 i = array_index(nrows, row, col)
755 757 if i >= size: break
756 758 x = items[i]
757 759 colwidth = max(colwidth, len(x))
758 760 colwidths.append(colwidth)
759 761 totwidth += colwidth + len(separator)
760 762 if totwidth > displaywidth:
761 763 break
762 764 if totwidth <= displaywidth:
763 765 break
764 766
765 767 # The smallest number of rows computed and the max widths for each
766 768 # column has been obtained. Now we just have to format each of the rows.
767 769 string = ''
768 770 for row in range(nrows):
769 771 texts = []
770 772 for col in range(ncols):
771 773 i = row + nrows*col
772 774 if i >= size:
773 775 texts.append('')
774 776 else:
775 777 texts.append(items[i])
776 778 while texts and not texts[-1]:
777 779 del texts[-1]
778 780 for col in range(len(texts)):
779 781 texts[col] = texts[col].ljust(colwidths[col])
780 782 string += '%s\n' % str(separator.join(texts))
781 783 return string
782 784
783 785 def _get_block_plain_text(self, block):
784 786 """ Given a QTextBlock, return its unformatted text.
785 787 """
786 788 cursor = QtGui.QTextCursor(block)
787 789 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
788 790 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
789 791 QtGui.QTextCursor.KeepAnchor)
790 792 return str(cursor.selection().toPlainText())
791 793
792 794 def _get_cursor(self):
793 795 """ Convenience method that returns a cursor for the current position.
794 796 """
795 797 return self._control.textCursor()
796 798
797 799 def _get_end_cursor(self):
798 800 """ Convenience method that returns a cursor for the last character.
799 801 """
800 802 cursor = self._control.textCursor()
801 803 cursor.movePosition(QtGui.QTextCursor.End)
802 804 return cursor
803 805
804 806 def _get_input_buffer_cursor_line(self):
805 807 """ The text in the line of the input buffer in which the user's cursor
806 808 rests. Returns a string if there is such a line; otherwise, None.
807 809 """
808 810 if self._executing:
809 811 return None
810 812 cursor = self._control.textCursor()
811 813 if cursor.position() >= self._prompt_pos:
812 814 text = self._get_block_plain_text(cursor.block())
813 815 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
814 816 return text[len(self._prompt):]
815 817 else:
816 818 return text[len(self._continuation_prompt):]
817 819 else:
818 820 return None
819 821
820 822 def _get_prompt_cursor(self):
821 823 """ Convenience method that returns a cursor for the prompt position.
822 824 """
823 825 cursor = self._control.textCursor()
824 826 cursor.setPosition(self._prompt_pos)
825 827 return cursor
826 828
827 829 def _get_selection_cursor(self, start, end):
828 830 """ Convenience method that returns a cursor with text selected between
829 831 the positions 'start' and 'end'.
830 832 """
831 833 cursor = self._control.textCursor()
832 834 cursor.setPosition(start)
833 835 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
834 836 return cursor
835 837
836 838 def _get_word_start_cursor(self, position):
837 839 """ Find the start of the word to the left the given position. If a
838 840 sequence of non-word characters precedes the first word, skip over
839 841 them. (This emulates the behavior of bash, emacs, etc.)
840 842 """
841 843 document = self._control.document()
842 844 position -= 1
843 845 while position >= self._prompt_pos and \
844 846 not document.characterAt(position).isLetterOrNumber():
845 847 position -= 1
846 848 while position >= self._prompt_pos and \
847 849 document.characterAt(position).isLetterOrNumber():
848 850 position -= 1
849 851 cursor = self._control.textCursor()
850 852 cursor.setPosition(position + 1)
851 853 return cursor
852 854
853 855 def _get_word_end_cursor(self, position):
854 856 """ Find the end of the word to the right the given position. If a
855 857 sequence of non-word characters precedes the first word, skip over
856 858 them. (This emulates the behavior of bash, emacs, etc.)
857 859 """
858 860 document = self._control.document()
859 861 end = self._get_end_cursor().position()
860 862 while position < end and \
861 863 not document.characterAt(position).isLetterOrNumber():
862 864 position += 1
863 865 while position < end and \
864 866 document.characterAt(position).isLetterOrNumber():
865 867 position += 1
866 868 cursor = self._control.textCursor()
867 869 cursor.setPosition(position)
868 870 return cursor
869 871
870 872 def _insert_html(self, cursor, html):
871 873 """ Insert HTML using the specified cursor in such a way that future
872 874 formatting is unaffected.
873 875 """
874 876 cursor.beginEditBlock()
875 877 cursor.insertHtml(html)
876 878
877 879 # After inserting HTML, the text document "remembers" it's in "html
878 880 # mode", which means that subsequent calls adding plain text will result
879 881 # in unwanted formatting, lost tab characters, etc. The following code
880 882 # hacks around this behavior, which I consider to be a bug in Qt.
881 883 cursor.movePosition(QtGui.QTextCursor.Left,
882 884 QtGui.QTextCursor.KeepAnchor)
883 885 if cursor.selection().toPlainText() == ' ':
884 886 cursor.removeSelectedText()
885 887 cursor.movePosition(QtGui.QTextCursor.Right)
886 888 cursor.insertText(' ', QtGui.QTextCharFormat())
887 889 cursor.endEditBlock()
888 890
889 891 def _insert_plain_text(self, cursor, text):
890 892 """ Inserts plain text using the specified cursor, processing ANSI codes
891 893 if enabled.
892 894 """
893 895 cursor.beginEditBlock()
894 896 if self.ansi_codes:
895 897 for substring in self._ansi_processor.split_string(text):
896 898 format = self._ansi_processor.get_format()
897 899 cursor.insertText(substring, format)
898 900 else:
899 901 cursor.insertText(text)
900 902 cursor.endEditBlock()
901 903
902 904 def _insert_into_buffer(self, text):
903 905 """ Inserts text into the input buffer at the current cursor position,
904 906 ensuring that continuation prompts are inserted as necessary.
905 907 """
906 908 lines = str(text).splitlines(True)
907 909 if lines:
908 910 self._keep_cursor_in_buffer()
909 911 cursor = self._control.textCursor()
910 912 cursor.beginEditBlock()
911 913 cursor.insertText(lines[0])
912 914 for line in lines[1:]:
913 915 if self._continuation_prompt_html is None:
914 916 cursor.insertText(self._continuation_prompt)
915 917 else:
916 918 self._insert_html(cursor, self._continuation_prompt_html)
917 919 cursor.insertText(line)
918 920 cursor.endEditBlock()
919 921 self._control.setTextCursor(cursor)
920 922
921 923 def _in_buffer(self, position):
922 924 """ Returns whether the given position is inside the editing region.
923 925 """
924 926 cursor = self._control.textCursor()
925 927 cursor.setPosition(position)
926 928 line = cursor.blockNumber()
927 929 prompt_line = self._get_prompt_cursor().blockNumber()
928 930 if line == prompt_line:
929 931 return position >= self._prompt_pos
930 932 elif line > prompt_line:
931 933 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
932 934 prompt_pos = cursor.position() + len(self._continuation_prompt)
933 935 return position >= prompt_pos
934 936 return False
935 937
936 938 def _keep_cursor_in_buffer(self):
937 939 """ Ensures that the cursor is inside the editing region. Returns
938 940 whether the cursor was moved.
939 941 """
940 942 cursor = self._control.textCursor()
941 943 if self._in_buffer(cursor.position()):
942 944 return False
943 945 else:
944 946 cursor.movePosition(QtGui.QTextCursor.End)
945 947 self._control.setTextCursor(cursor)
946 948 return True
947 949
948 950 def _page(self, text):
949 951 """ Displays text using the pager.
950 952 """
951 953 if self._page_style == 'custom':
952 954 self.custom_page_requested.emit(text)
953 955 elif self._page_style == 'none':
954 956 self._append_plain_text(text)
955 957 else:
956 958 self._page_control.clear()
957 959 cursor = self._page_control.textCursor()
958 960 self._insert_plain_text(cursor, text)
959 961 self._page_control.moveCursor(QtGui.QTextCursor.Start)
960 962
961 963 self._page_control.viewport().resize(self._control.size())
962 964 if self._splitter:
963 965 self._page_control.show()
964 966 self._page_control.setFocus()
965 967 else:
966 968 self.layout().setCurrentWidget(self._page_control)
967 969
968 970 def _prompt_started(self):
969 971 """ Called immediately after a new prompt is displayed.
970 972 """
971 973 # Temporarily disable the maximum block count to permit undo/redo and
972 974 # to ensure that the prompt position does not change due to truncation.
973 975 self._control.document().setMaximumBlockCount(0)
974 976 self._control.setUndoRedoEnabled(True)
975 977
976 978 self._control.setReadOnly(False)
977 979 self._control.moveCursor(QtGui.QTextCursor.End)
978 980
979 981 self._executing = False
980 982 self._prompt_started_hook()
981 983
982 984 def _prompt_finished(self):
983 985 """ Called immediately after a prompt is finished, i.e. when some input
984 986 will be processed and a new prompt displayed.
985 987 """
986 988 self._control.setUndoRedoEnabled(False)
987 989 self._control.setReadOnly(True)
988 990 self._prompt_finished_hook()
989 991
990 992 def _readline(self, prompt='', callback=None):
991 993 """ Reads one line of input from the user.
992 994
993 995 Parameters
994 996 ----------
995 997 prompt : str, optional
996 998 The prompt to print before reading the line.
997 999
998 1000 callback : callable, optional
999 1001 A callback to execute with the read line. If not specified, input is
1000 1002 read *synchronously* and this method does not return until it has
1001 1003 been read.
1002 1004
1003 1005 Returns
1004 1006 -------
1005 1007 If a callback is specified, returns nothing. Otherwise, returns the
1006 1008 input string with the trailing newline stripped.
1007 1009 """
1008 1010 if self._reading:
1009 1011 raise RuntimeError('Cannot read a line. Widget is already reading.')
1010 1012
1011 1013 if not callback and not self.isVisible():
1012 1014 # If the user cannot see the widget, this function cannot return.
1013 1015 raise RuntimeError('Cannot synchronously read a line if the widget'
1014 1016 'is not visible!')
1015 1017
1016 1018 self._reading = True
1017 1019 self._show_prompt(prompt, newline=False)
1018 1020
1019 1021 if callback is None:
1020 1022 self._reading_callback = None
1021 1023 while self._reading:
1022 1024 QtCore.QCoreApplication.processEvents()
1023 1025 return self.input_buffer.rstrip('\n')
1024 1026
1025 1027 else:
1026 1028 self._reading_callback = lambda: \
1027 1029 callback(self.input_buffer.rstrip('\n'))
1028 1030
1029 1031 def _reset(self):
1030 1032 """ Clears the console and resets internal state variables.
1031 1033 """
1032 1034 self._control.clear()
1033 1035 self._executing = self._reading = False
1034 1036
1035 1037 def _set_continuation_prompt(self, prompt, html=False):
1036 1038 """ Sets the continuation prompt.
1037 1039
1038 1040 Parameters
1039 1041 ----------
1040 1042 prompt : str
1041 1043 The prompt to show when more input is needed.
1042 1044
1043 1045 html : bool, optional (default False)
1044 1046 If set, the prompt will be inserted as formatted HTML. Otherwise,
1045 1047 the prompt will be treated as plain text, though ANSI color codes
1046 1048 will be handled.
1047 1049 """
1048 1050 if html:
1049 1051 self._continuation_prompt_html = prompt
1050 1052 else:
1051 1053 self._continuation_prompt = prompt
1052 1054 self._continuation_prompt_html = None
1053 1055
1054 1056 def _set_cursor(self, cursor):
1055 1057 """ Convenience method to set the current cursor.
1056 1058 """
1057 1059 self._control.setTextCursor(cursor)
1058 1060
1059 1061 def _set_position(self, position):
1060 1062 """ Convenience method to set the position of the cursor.
1061 1063 """
1062 1064 cursor = self._control.textCursor()
1063 1065 cursor.setPosition(position)
1064 1066 self._control.setTextCursor(cursor)
1065 1067
1066 1068 def _set_selection(self, start, end):
1067 1069 """ Convenience method to set the current selected text.
1068 1070 """
1069 1071 self._control.setTextCursor(self._get_selection_cursor(start, end))
1070 1072
1071 1073 def _show_context_menu(self, pos):
1072 1074 """ Shows a context menu at the given QPoint (in widget coordinates).
1073 1075 """
1074 1076 menu = QtGui.QMenu()
1075 1077
1076 1078 copy_action = menu.addAction('Copy', self.copy)
1077 1079 copy_action.setEnabled(self._get_cursor().hasSelection())
1078 1080 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1079 1081
1080 1082 paste_action = menu.addAction('Paste', self.paste)
1081 1083 paste_action.setEnabled(self.can_paste())
1082 1084 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1083 1085
1084 1086 menu.addSeparator()
1085 1087 menu.addAction('Select All', self.select_all)
1086 1088
1087 1089 menu.exec_(self._control.mapToGlobal(pos))
1088 1090
1089 1091 def _show_prompt(self, prompt=None, html=False, newline=True):
1090 1092 """ Writes a new prompt at the end of the buffer.
1091 1093
1092 1094 Parameters
1093 1095 ----------
1094 1096 prompt : str, optional
1095 1097 The prompt to show. If not specified, the previous prompt is used.
1096 1098
1097 1099 html : bool, optional (default False)
1098 1100 Only relevant when a prompt is specified. If set, the prompt will
1099 1101 be inserted as formatted HTML. Otherwise, the prompt will be treated
1100 1102 as plain text, though ANSI color codes will be handled.
1101 1103
1102 1104 newline : bool, optional (default True)
1103 1105 If set, a new line will be written before showing the prompt if
1104 1106 there is not already a newline at the end of the buffer.
1105 1107 """
1106 1108 # Insert a preliminary newline, if necessary.
1107 1109 if newline:
1108 1110 cursor = self._get_end_cursor()
1109 1111 if cursor.position() > 0:
1110 1112 cursor.movePosition(QtGui.QTextCursor.Left,
1111 1113 QtGui.QTextCursor.KeepAnchor)
1112 1114 if str(cursor.selection().toPlainText()) != '\n':
1113 1115 self._append_plain_text('\n')
1114 1116
1115 1117 # Write the prompt.
1116 1118 if prompt is None:
1117 1119 if self._prompt_html is None:
1118 1120 self._append_plain_text(self._prompt)
1119 1121 else:
1120 1122 self._append_html(self._prompt_html)
1121 1123 else:
1122 1124 if html:
1123 1125 self._prompt = self._append_html_fetching_plain_text(prompt)
1124 1126 self._prompt_html = prompt
1125 1127 else:
1126 1128 self._append_plain_text(prompt)
1127 1129 self._prompt = prompt
1128 1130 self._prompt_html = None
1129 1131
1130 1132 self._prompt_pos = self._get_end_cursor().position()
1131 1133 self._prompt_started()
1132 1134
1133 1135 def _show_continuation_prompt(self):
1134 1136 """ Writes a new continuation prompt at the end of the buffer.
1135 1137 """
1136 1138 if self._continuation_prompt_html is None:
1137 1139 self._append_plain_text(self._continuation_prompt)
1138 1140 else:
1139 1141 self._continuation_prompt = self._append_html_fetching_plain_text(
1140 1142 self._continuation_prompt_html)
1141 1143
1142 1144 self._prompt_started()
1143 1145
1144 1146
1145 1147 class HistoryConsoleWidget(ConsoleWidget):
1146 1148 """ A ConsoleWidget that keeps a history of the commands that have been
1147 1149 executed.
1148 1150 """
1149 1151
1150 1152 #---------------------------------------------------------------------------
1151 1153 # 'object' interface
1152 1154 #---------------------------------------------------------------------------
1153 1155
1154 1156 def __init__(self, *args, **kw):
1155 1157 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1156 1158 self._history = []
1157 1159 self._history_index = 0
1158 1160
1159 1161 #---------------------------------------------------------------------------
1160 1162 # 'ConsoleWidget' public interface
1161 1163 #---------------------------------------------------------------------------
1162 1164
1163 1165 def execute(self, source=None, hidden=False, interactive=False):
1164 1166 """ Reimplemented to the store history.
1165 1167 """
1166 1168 if not hidden:
1167 1169 history = self.input_buffer if source is None else source
1168 1170
1169 1171 executed = super(HistoryConsoleWidget, self).execute(
1170 1172 source, hidden, interactive)
1171 1173
1172 1174 if executed and not hidden:
1173 1175 self._history.append(history.rstrip())
1174 1176 self._history_index = len(self._history)
1175 1177
1176 1178 return executed
1177 1179
1178 1180 #---------------------------------------------------------------------------
1179 1181 # 'ConsoleWidget' abstract interface
1180 1182 #---------------------------------------------------------------------------
1181 1183
1182 1184 def _up_pressed(self):
1183 1185 """ Called when the up key is pressed. Returns whether to continue
1184 1186 processing the event.
1185 1187 """
1186 1188 prompt_cursor = self._get_prompt_cursor()
1187 1189 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1188 1190 self.history_previous()
1189 1191
1190 1192 # Go to the first line of prompt for seemless history scrolling.
1191 1193 cursor = self._get_prompt_cursor()
1192 1194 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1193 1195 self._set_cursor(cursor)
1194 1196
1195 1197 return False
1196 1198 return True
1197 1199
1198 1200 def _down_pressed(self):
1199 1201 """ Called when the down key is pressed. Returns whether to continue
1200 1202 processing the event.
1201 1203 """
1202 1204 end_cursor = self._get_end_cursor()
1203 1205 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1204 1206 self.history_next()
1205 1207 return False
1206 1208 return True
1207 1209
1208 1210 #---------------------------------------------------------------------------
1209 1211 # 'HistoryConsoleWidget' interface
1210 1212 #---------------------------------------------------------------------------
1211 1213
1212 1214 def history_previous(self):
1213 1215 """ If possible, set the input buffer to the previous item in the
1214 1216 history.
1215 1217 """
1216 1218 if self._history_index > 0:
1217 1219 self._history_index -= 1
1218 1220 self.input_buffer = self._history[self._history_index]
1219 1221
1220 1222 def history_next(self):
1221 1223 """ Set the input buffer to the next item in the history, or a blank
1222 1224 line if there is no subsequent item.
1223 1225 """
1224 1226 if self._history_index < len(self._history):
1225 1227 self._history_index += 1
1226 1228 if self._history_index < len(self._history):
1227 1229 self.input_buffer = self._history[self._history_index]
1228 1230 else:
1229 1231 self.input_buffer = ''
@@ -1,186 +1,188 b''
1 1 # System library imports
2 2 from PyQt4 import QtCore, QtGui
3 3
4 4 # Local imports
5 5 from IPython.core.usage import default_banner
6 6 from frontend_widget import FrontendWidget
7 7
8 8
9 9 class IPythonWidget(FrontendWidget):
10 10 """ A FrontendWidget for an IPython kernel.
11 11 """
12 12
13 13 # The default stylesheet: black text on a white background.
14 14 default_stylesheet = """
15 15 .error { color: red; }
16 16 .in-prompt { color: navy; }
17 17 .in-prompt-number { font-weight: bold; }
18 18 .out-prompt { color: darkred; }
19 19 .out-prompt-number { font-weight: bold; }
20 20 """
21 21
22 22 # A dark stylesheet: white text on a black background.
23 23 dark_stylesheet = """
24 24 QPlainTextEdit { background-color: black; color: white }
25 25 QFrame { border: 1px solid grey; }
26 26 .error { color: red; }
27 27 .in-prompt { color: lime; }
28 28 .in-prompt-number { color: lime; font-weight: bold; }
29 29 .out-prompt { color: red; }
30 30 .out-prompt-number { color: red; font-weight: bold; }
31 31 """
32 32
33 33 # Default prompts.
34 34 in_prompt = '<br/>In [<span class="in-prompt-number">%i</span>]: '
35 35 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36
37 37 #---------------------------------------------------------------------------
38 38 # 'object' interface
39 39 #---------------------------------------------------------------------------
40 40
41 41 def __init__(self, *args, **kw):
42 42 super(IPythonWidget, self).__init__(*args, **kw)
43 43
44 44 # Initialize protected variables.
45 45 self._previous_prompt_blocks = []
46 46 self._prompt_count = 0
47 47
48 48 # Set a default stylesheet.
49 49 self.reset_styling()
50 50
51 51 #---------------------------------------------------------------------------
52 52 # 'BaseFrontendMixin' abstract interface
53 53 #---------------------------------------------------------------------------
54 54
55 55 def _handle_pyout(self, msg):
56 56 """ Reimplemented for IPython-style "display hook".
57 57 """
58 58 self._append_html(self._make_out_prompt(self._prompt_count))
59 59 self._save_prompt_block()
60 60
61 61 self._append_plain_text(msg['content']['data'] + '\n')
62 62
63 63 #---------------------------------------------------------------------------
64 64 # 'FrontendWidget' interface
65 65 #---------------------------------------------------------------------------
66 66
67 67 def execute_file(self, path, hidden=False):
68 68 """ Reimplemented to use the 'run' magic.
69 69 """
70 70 self.execute('run %s' % path, hidden=hidden)
71 71
72 72 #---------------------------------------------------------------------------
73 73 # 'FrontendWidget' protected interface
74 74 #---------------------------------------------------------------------------
75 75
76 76 def _get_banner(self):
77 77 """ Reimplemented to return IPython's default banner.
78 78 """
79 79 return default_banner
80 80
81 81 def _process_execute_error(self, msg):
82 82 """ Reimplemented for IPython-style traceback formatting.
83 83 """
84 84 content = msg['content']
85 85 traceback_lines = content['traceback'][:]
86 86 traceback = ''.join(traceback_lines)
87 87 traceback = traceback.replace(' ', '&nbsp;')
88 88 traceback = traceback.replace('\n', '<br/>')
89 89
90 90 ename = content['ename']
91 91 ename_styled = '<span class="error">%s</span>' % ename
92 92 traceback = traceback.replace(ename, ename_styled)
93 93
94 94 self._append_html(traceback)
95 95
96 96 def _show_interpreter_prompt(self):
97 97 """ Reimplemented for IPython-style prompts.
98 98 """
99 99 # Update old prompt numbers if necessary.
100 100 previous_prompt_number = self._prompt_count
101 101 if previous_prompt_number != self._prompt_count:
102 102 for i, (block, length) in enumerate(self._previous_prompt_blocks):
103 103 if block.isValid():
104 104 cursor = QtGui.QTextCursor(block)
105 105 cursor.movePosition(QtGui.QTextCursor.Right,
106 106 QtGui.QTextCursor.KeepAnchor, length-1)
107 107 if i == 0:
108 108 prompt = self._make_in_prompt(previous_prompt_number)
109 109 else:
110 110 prompt = self._make_out_prompt(previous_prompt_number)
111 111 self._insert_html(cursor, prompt)
112 112 self._previous_prompt_blocks = []
113 113
114 114 # Show a new prompt.
115 115 self._prompt_count += 1
116 116 self._show_prompt(self._make_in_prompt(self._prompt_count), html=True)
117 117 self._save_prompt_block()
118 118
119 119 # Update continuation prompt to reflect (possibly) new prompt length.
120 120 self._set_continuation_prompt(
121 121 self._make_continuation_prompt(self._prompt), html=True)
122 122
123 123 #---------------------------------------------------------------------------
124 124 # 'IPythonWidget' interface
125 125 #---------------------------------------------------------------------------
126 126
127 127 def reset_styling(self):
128 128 """ Restores the default IPythonWidget styling.
129 129 """
130 130 self.set_styling(self.default_stylesheet, syntax_style='default')
131 131 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
132 132
133 133 def set_styling(self, stylesheet, syntax_style=None):
134 134 """ Sets the IPythonWidget styling.
135 135
136 136 Parameters:
137 137 -----------
138 138 stylesheet : str
139 139 A CSS stylesheet. The stylesheet can contain classes for:
140 140 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
141 141 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
142 142 3. IPython: .error, .in-prompt, .out-prompt, etc.
143 143
144 144 syntax_style : str or None [default None]
145 145 If specified, use the Pygments style with given name. Otherwise,
146 146 the stylesheet is queried for Pygments style information.
147 147 """
148 148 self.setStyleSheet(stylesheet)
149 149 self._control.document().setDefaultStyleSheet(stylesheet)
150 if self._page_control:
151 self._page_control.document().setDefaultStyleSheet(stylesheet)
150 152
151 153 if syntax_style is None:
152 154 self._highlighter.set_style_sheet(stylesheet)
153 155 else:
154 156 self._highlighter.set_style(syntax_style)
155 157
156 158 #---------------------------------------------------------------------------
157 159 # 'IPythonWidget' protected interface
158 160 #---------------------------------------------------------------------------
159 161
160 162 def _make_in_prompt(self, number):
161 163 """ Given a prompt number, returns an HTML In prompt.
162 164 """
163 165 body = self.in_prompt % number
164 166 return '<span class="in-prompt">%s</span>' % body
165 167
166 168 def _make_continuation_prompt(self, prompt):
167 169 """ Given a plain text version of an In prompt, returns an HTML
168 170 continuation prompt.
169 171 """
170 172 end_chars = '...: '
171 173 space_count = len(prompt.lstrip('\n')) - len(end_chars)
172 174 body = '&nbsp;' * space_count + end_chars
173 175 return '<span class="in-prompt">%s</span>' % body
174 176
175 177 def _make_out_prompt(self, number):
176 178 """ Given a prompt number, returns an HTML Out prompt.
177 179 """
178 180 body = self.out_prompt % number
179 181 return '<span class="out-prompt">%s</span>' % body
180 182
181 183 def _save_prompt_block(self):
182 184 """ Assuming a prompt has just been written at the end of the buffer,
183 185 store the QTextBlock that contains it and its length.
184 186 """
185 187 block = self._control.document().lastBlock()
186 188 self._previous_prompt_blocks.append((block, block.length()))
General Comments 0
You need to be logged in to leave comments. Login now