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