##// END OF EJS Templates
* Fixed history breakage due to recent refactoring....
epatters -
Show More
@@ -1,817 +1,823 b''
1 1 # Standard library imports
2 2 import re
3 3 import sys
4 4
5 5 # System library imports
6 6 from PyQt4 import QtCore, QtGui
7 7
8 8 # Local imports
9 9 from completion_widget import CompletionWidget
10 10
11 11
12 12 class AnsiCodeProcessor(object):
13 13 """ Translates ANSI escape codes into readable attributes.
14 14 """
15 15
16 16 def __init__(self):
17 17 self.ansi_colors = ( # Normal, Bright/Light
18 18 ('#000000', '#7f7f7f'), # 0: black
19 19 ('#cd0000', '#ff0000'), # 1: red
20 20 ('#00cd00', '#00ff00'), # 2: green
21 21 ('#cdcd00', '#ffff00'), # 3: yellow
22 22 ('#0000ee', '#0000ff'), # 4: blue
23 23 ('#cd00cd', '#ff00ff'), # 5: magenta
24 24 ('#00cdcd', '#00ffff'), # 6: cyan
25 25 ('#e5e5e5', '#ffffff')) # 7: white
26 26 self.reset()
27 27
28 28 def set_code(self, code):
29 29 """ Set attributes based on code.
30 30 """
31 31 if code == 0:
32 32 self.reset()
33 33 elif code == 1:
34 34 self.intensity = 1
35 35 self.bold = True
36 36 elif code == 3:
37 37 self.italic = True
38 38 elif code == 4:
39 39 self.underline = True
40 40 elif code == 22:
41 41 self.intensity = 0
42 42 self.bold = False
43 43 elif code == 23:
44 44 self.italic = False
45 45 elif code == 24:
46 46 self.underline = False
47 47 elif code >= 30 and code <= 37:
48 48 self.foreground_color = code - 30
49 49 elif code == 39:
50 50 self.foreground_color = None
51 51 elif code >= 40 and code <= 47:
52 52 self.background_color = code - 40
53 53 elif code == 49:
54 54 self.background_color = None
55 55
56 56 def reset(self):
57 57 """ Reset attributs to their default values.
58 58 """
59 59 self.intensity = 0
60 60 self.italic = False
61 61 self.bold = False
62 62 self.underline = False
63 63 self.foreground_color = None
64 64 self.background_color = None
65 65
66 66
67 67 class QtAnsiCodeProcessor(AnsiCodeProcessor):
68 68 """ Translates ANSI escape codes into QTextCharFormats.
69 69 """
70 70
71 71 def get_format(self):
72 72 """ Returns a QTextCharFormat that encodes the current style attributes.
73 73 """
74 74 format = QtGui.QTextCharFormat()
75 75
76 76 # Set foreground color
77 77 if self.foreground_color is not None:
78 78 color = self.ansi_colors[self.foreground_color][self.intensity]
79 79 format.setForeground(QtGui.QColor(color))
80 80
81 81 # Set background color
82 82 if self.background_color is not None:
83 83 color = self.ansi_colors[self.background_color][self.intensity]
84 84 format.setBackground(QtGui.QColor(color))
85 85
86 86 # Set font weight/style options
87 87 if self.bold:
88 88 format.setFontWeight(QtGui.QFont.Bold)
89 89 else:
90 90 format.setFontWeight(QtGui.QFont.Normal)
91 91 format.setFontItalic(self.italic)
92 92 format.setFontUnderline(self.underline)
93 93
94 94 return format
95 95
96 96
97 97 class ConsoleWidget(QtGui.QPlainTextEdit):
98 98 """ Base class for console-type widgets. This class is mainly concerned with
99 99 dealing with the prompt, keeping the cursor inside the editing line, and
100 100 handling ANSI escape sequences.
101 101 """
102 102
103 103 # Whether to process ANSI escape codes.
104 104 ansi_codes = True
105 105
106 106 # The maximum number of lines of text before truncation.
107 107 buffer_size = 500
108 108
109 109 # Whether to use a CompletionWidget or plain text output for tab completion.
110 110 gui_completion = True
111 111
112 112 # Whether to override ShortcutEvents for the keybindings defined by this
113 113 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
114 114 # priority (when it has focus) over, e.g., window-level menu shortcuts.
115 115 override_shortcuts = False
116 116
117 117 # Protected class variables.
118 118 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
119 119 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
120 120 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
121 121 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
122 122 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
123 123 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
124 124 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
125 125 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
126 126 _shortcuts = set(_ctrl_down_remap.keys() +
127 127 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
128 128
129 129 #---------------------------------------------------------------------------
130 130 # 'QObject' interface
131 131 #---------------------------------------------------------------------------
132 132
133 133 def __init__(self, parent=None):
134 134 QtGui.QPlainTextEdit.__init__(self, parent)
135 135
136 136 # Initialize protected variables.
137 137 self._ansi_processor = QtAnsiCodeProcessor()
138 138 self._completion_widget = CompletionWidget(self)
139 139 self._continuation_prompt = '> '
140 140 self._executing = False
141 141 self._prompt = ''
142 142 self._prompt_pos = 0
143 143 self._reading = False
144 144
145 145 # Set a monospaced font.
146 146 self.reset_font()
147 147
148 148 # Define a custom context menu.
149 149 self._context_menu = QtGui.QMenu(self)
150 150
151 151 copy_action = QtGui.QAction('Copy', self)
152 152 copy_action.triggered.connect(self.copy)
153 153 self.copyAvailable.connect(copy_action.setEnabled)
154 154 self._context_menu.addAction(copy_action)
155 155
156 156 self._paste_action = QtGui.QAction('Paste', self)
157 157 self._paste_action.triggered.connect(self.paste)
158 158 self._context_menu.addAction(self._paste_action)
159 159 self._context_menu.addSeparator()
160 160
161 161 select_all_action = QtGui.QAction('Select All', self)
162 162 select_all_action.triggered.connect(self.selectAll)
163 163 self._context_menu.addAction(select_all_action)
164 164
165 165 def event(self, event):
166 166 """ Reimplemented to override shortcuts, if necessary.
167 167 """
168 168 # On Mac OS, it is always unnecessary to override shortcuts, hence the
169 169 # check below. Users should just use the Control key instead of the
170 170 # Command key.
171 171 if self.override_shortcuts and \
172 172 sys.platform != 'darwin' and \
173 173 event.type() == QtCore.QEvent.ShortcutOverride and \
174 174 self._control_down(event.modifiers()) and \
175 175 event.key() in self._shortcuts:
176 176 event.accept()
177 177 return True
178 178 else:
179 179 return QtGui.QPlainTextEdit.event(self, event)
180 180
181 181 #---------------------------------------------------------------------------
182 182 # 'QWidget' interface
183 183 #---------------------------------------------------------------------------
184 184
185 185 def contextMenuEvent(self, event):
186 186 """ Reimplemented to create a menu without destructive actions like
187 187 'Cut' and 'Delete'.
188 188 """
189 189 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
190 190 self._paste_action.setEnabled(not clipboard_empty)
191 191
192 192 self._context_menu.exec_(event.globalPos())
193 193
194 194 def keyPressEvent(self, event):
195 195 """ Reimplemented to create a console-like interface.
196 196 """
197 197 intercepted = False
198 198 cursor = self.textCursor()
199 199 position = cursor.position()
200 200 key = event.key()
201 201 ctrl_down = self._control_down(event.modifiers())
202 202 alt_down = event.modifiers() & QtCore.Qt.AltModifier
203 203 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
204 204
205 205 # Even though we have reimplemented 'paste', the C++ level slot is still
206 206 # called by Qt. So we intercept the key press here.
207 207 if event.matches(QtGui.QKeySequence.Paste):
208 208 self.paste()
209 209 intercepted = True
210 210
211 211 elif ctrl_down:
212 212 if key in self._ctrl_down_remap:
213 213 ctrl_down = False
214 214 key = self._ctrl_down_remap[key]
215 215 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
216 216 QtCore.Qt.NoModifier)
217 217
218 218 elif key == QtCore.Qt.Key_K:
219 219 if self._in_buffer(position):
220 220 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
221 221 QtGui.QTextCursor.KeepAnchor)
222 222 cursor.removeSelectedText()
223 223 intercepted = True
224 224
225 225 elif key == QtCore.Qt.Key_X:
226 226 intercepted = True
227 227
228 228 elif key == QtCore.Qt.Key_Y:
229 229 self.paste()
230 230 intercepted = True
231 231
232 232 elif alt_down:
233 233 if key == QtCore.Qt.Key_B:
234 234 self.setTextCursor(self._get_word_start_cursor(position))
235 235 intercepted = True
236 236
237 237 elif key == QtCore.Qt.Key_F:
238 238 self.setTextCursor(self._get_word_end_cursor(position))
239 239 intercepted = True
240 240
241 241 elif key == QtCore.Qt.Key_Backspace:
242 242 cursor = self._get_word_start_cursor(position)
243 243 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
244 244 cursor.removeSelectedText()
245 245 intercepted = True
246 246
247 247 elif key == QtCore.Qt.Key_D:
248 248 cursor = self._get_word_end_cursor(position)
249 249 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
250 250 cursor.removeSelectedText()
251 251 intercepted = True
252 252
253 253 if self._completion_widget.isVisible():
254 254 self._completion_widget.keyPressEvent(event)
255 255 intercepted = event.isAccepted()
256 256
257 257 else:
258 258 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
259 259 if self._reading:
260 260 self._reading = False
261 261 elif not self._executing:
262 262 self.execute(interactive=True)
263 263 intercepted = True
264 264
265 265 elif key == QtCore.Qt.Key_Up:
266 266 if self._reading or not self._up_pressed():
267 267 intercepted = True
268 268 else:
269 269 prompt_line = self._get_prompt_cursor().blockNumber()
270 270 intercepted = cursor.blockNumber() <= prompt_line
271 271
272 272 elif key == QtCore.Qt.Key_Down:
273 273 if self._reading or not self._down_pressed():
274 274 intercepted = True
275 275 else:
276 276 end_line = self._get_end_cursor().blockNumber()
277 277 intercepted = cursor.blockNumber() == end_line
278 278
279 279 elif key == QtCore.Qt.Key_Tab:
280 280 if self._reading:
281 281 intercepted = False
282 282 else:
283 283 intercepted = not self._tab_pressed()
284 284
285 285 elif key == QtCore.Qt.Key_Left:
286 286 intercepted = not self._in_buffer(position - 1)
287 287
288 288 elif key == QtCore.Qt.Key_Home:
289 289 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
290 290 start_pos = cursor.position()
291 291 start_line = cursor.blockNumber()
292 292 if start_line == self._get_prompt_cursor().blockNumber():
293 293 start_pos += len(self._prompt)
294 294 else:
295 295 start_pos += len(self._continuation_prompt)
296 296 if shift_down and self._in_buffer(position):
297 297 self._set_selection(position, start_pos)
298 298 else:
299 299 self._set_position(start_pos)
300 300 intercepted = True
301 301
302 302 elif key == QtCore.Qt.Key_Backspace and not alt_down:
303 303
304 304 # Line deletion (remove continuation prompt)
305 305 len_prompt = len(self._continuation_prompt)
306 306 if cursor.columnNumber() == len_prompt and \
307 307 position != self._prompt_pos:
308 308 cursor.setPosition(position - len_prompt,
309 309 QtGui.QTextCursor.KeepAnchor)
310 310 cursor.removeSelectedText()
311 311
312 312 # Regular backwards deletion
313 313 else:
314 314 anchor = cursor.anchor()
315 315 if anchor == position:
316 316 intercepted = not self._in_buffer(position - 1)
317 317 else:
318 318 intercepted = not self._in_buffer(min(anchor, position))
319 319
320 320 elif key == QtCore.Qt.Key_Delete:
321 321 anchor = cursor.anchor()
322 322 intercepted = not self._in_buffer(min(anchor, position))
323 323
324 324 # Don't move cursor if control is down to allow copy-paste using
325 325 # the keyboard in any part of the buffer.
326 326 if not ctrl_down:
327 327 self._keep_cursor_in_buffer()
328 328
329 329 if not intercepted:
330 330 QtGui.QPlainTextEdit.keyPressEvent(self, event)
331 331
332 332 #--------------------------------------------------------------------------
333 333 # 'QPlainTextEdit' interface
334 334 #--------------------------------------------------------------------------
335 335
336 336 def appendPlainText(self, text):
337 337 """ Reimplemented to not append text as a new paragraph, which doesn't
338 338 make sense for a console widget. Also, if enabled, handle ANSI
339 339 codes.
340 340 """
341 341 cursor = self.textCursor()
342 342 cursor.movePosition(QtGui.QTextCursor.End)
343 343
344 344 if self.ansi_codes:
345 345 format = QtGui.QTextCharFormat()
346 346 previous_end = 0
347 347 for match in self._ansi_pattern.finditer(text):
348 348 cursor.insertText(text[previous_end:match.start()], format)
349 349 previous_end = match.end()
350 350 for code in match.group(1).split(';'):
351 351 self._ansi_processor.set_code(int(code))
352 352 format = self._ansi_processor.get_format()
353 353 cursor.insertText(text[previous_end:], format)
354 354 else:
355 355 cursor.insertText(text)
356 356
357 357 def clear(self, keep_input=False):
358 358 """ Reimplemented to write a new prompt. If 'keep_input' is set,
359 359 restores the old input buffer when the new prompt is written.
360 360 """
361 361 super(ConsoleWidget, self).clear()
362 362
363 363 if keep_input:
364 364 input_buffer = self.input_buffer
365 365 self._show_prompt()
366 366 if keep_input:
367 367 self.input_buffer = input_buffer
368 368
369 369 def paste(self):
370 370 """ Reimplemented to ensure that text is pasted in the editing region.
371 371 """
372 372 self._keep_cursor_in_buffer()
373 373 QtGui.QPlainTextEdit.paste(self)
374 374
375 375 def print_(self, printer):
376 376 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
377 377 slot has the wrong signature.
378 378 """
379 379 QtGui.QPlainTextEdit.print_(self, printer)
380 380
381 381 #---------------------------------------------------------------------------
382 382 # 'ConsoleWidget' public interface
383 383 #---------------------------------------------------------------------------
384 384
385 385 def execute(self, source=None, hidden=False, interactive=False):
386 386 """ Executes source or the input buffer, possibly prompting for more
387 387 input.
388 388
389 389 Parameters:
390 390 -----------
391 391 source : str, optional
392 392
393 393 The source to execute. If not specified, the input buffer will be
394 394 used. If specified and 'hidden' is False, the input buffer will be
395 395 replaced with the source before execution.
396 396
397 397 hidden : bool, optional (default False)
398 398
399 399 If set, no output will be shown and the prompt will not be modified.
400 400 In other words, it will be completely invisible to the user that
401 401 an execution has occurred.
402 402
403 403 interactive : bool, optional (default False)
404 404
405 405 Whether the console is to treat the source as having been manually
406 406 entered by the user. The effect of this parameter depends on the
407 407 subclass implementation.
408 408
409 Raises:
410 -------
411 RuntimeError
412 If incomplete input is given and 'hidden' is True. In this case,
413 it not possible to prompt for more input.
414
409 415 Returns:
410 416 --------
411 417 A boolean indicating whether the source was executed.
412 418 """
413 419 if not hidden:
414 420 if source is not None:
415 421 self.input_buffer = source
416 422
417 423 self.appendPlainText('\n')
418 424 self._executing_input_buffer = self.input_buffer
419 425 self._executing = True
420 426 self._prompt_finished()
421 427
422 428 real_source = self.input_buffer if source is None else source
423 429 complete = self._is_complete(real_source, interactive)
424 430 if complete:
425 431 if not hidden:
426 432 # The maximum block count is only in effect during execution.
427 433 # This ensures that _prompt_pos does not become invalid due to
428 434 # text truncation.
429 435 self.setMaximumBlockCount(self.buffer_size)
430 436 self._execute(real_source, hidden)
431 437 elif hidden:
432 438 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
433 439 else:
434 440 self._show_continuation_prompt()
435 441
436 442 return complete
437 443
438 444 def _get_input_buffer(self):
439 445 """ The text that the user has entered entered at the current prompt.
440 446 """
441 447 # If we're executing, the input buffer may not even exist anymore due to
442 448 # the limit imposed by 'buffer_size'. Therefore, we store it.
443 449 if self._executing:
444 450 return self._executing_input_buffer
445 451
446 452 cursor = self._get_end_cursor()
447 453 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
448 454
449 455 # Use QTextDocumentFragment intermediate object because it strips
450 456 # out the Unicode line break characters that Qt insists on inserting.
451 457 input_buffer = str(cursor.selection().toPlainText())
452 458
453 459 # Strip out continuation prompts.
454 460 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
455 461
456 462 def _set_input_buffer(self, string):
457 463 """ Replaces the text in the input buffer with 'string'.
458 464 """
459 465 # Add continuation prompts where necessary.
460 466 lines = string.splitlines()
461 467 for i in xrange(1, len(lines)):
462 468 lines[i] = self._continuation_prompt + lines[i]
463 469 string = '\n'.join(lines)
464 470
465 471 # Replace buffer with new text.
466 472 cursor = self._get_end_cursor()
467 473 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
468 474 cursor.insertText(string)
469 475 self.moveCursor(QtGui.QTextCursor.End)
470 476
471 477 input_buffer = property(_get_input_buffer, _set_input_buffer)
472 478
473 479 def _get_input_buffer_cursor_line(self):
474 480 """ The text in the line of the input buffer in which the user's cursor
475 481 rests. Returns a string if there is such a line; otherwise, None.
476 482 """
477 483 if self._executing:
478 484 return None
479 485 cursor = self.textCursor()
480 486 if cursor.position() >= self._prompt_pos:
481 487 text = str(cursor.block().text())
482 488 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
483 489 return text[len(self._prompt):]
484 490 else:
485 491 return text[len(self._continuation_prompt):]
486 492 else:
487 493 return None
488 494
489 495 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
490 496
491 497 def _get_font(self):
492 498 """ The base font being used by the ConsoleWidget.
493 499 """
494 500 return self.document().defaultFont()
495 501
496 502 def _set_font(self, font):
497 503 """ Sets the base font for the ConsoleWidget to the specified QFont.
498 504 """
499 505 self._completion_widget.setFont(font)
500 506 self.document().setDefaultFont(font)
501 507
502 508 font = property(_get_font, _set_font)
503 509
504 510 def reset_font(self):
505 511 """ Sets the font to the default fixed-width font for this platform.
506 512 """
507 513 if sys.platform == 'win32':
508 514 name = 'Courier'
509 515 elif sys.platform == 'darwin':
510 516 name = 'Monaco'
511 517 else:
512 518 name = 'Monospace'
513 519 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
514 520 font.setStyleHint(QtGui.QFont.TypeWriter)
515 521 self._set_font(font)
516 522
517 523 #---------------------------------------------------------------------------
518 524 # 'ConsoleWidget' abstract interface
519 525 #---------------------------------------------------------------------------
520 526
521 527 def _is_complete(self, source, interactive):
522 528 """ Returns whether 'source' can be executed. When triggered by an
523 529 Enter/Return key press, 'interactive' is True; otherwise, it is
524 530 False.
525 531 """
526 532 raise NotImplementedError
527 533
528 534 def _execute(self, source, hidden):
529 535 """ Execute 'source'. If 'hidden', do not show any output.
530 536 """
531 537 raise NotImplementedError
532 538
533 539 def _prompt_started_hook(self):
534 540 """ Called immediately after a new prompt is displayed.
535 541 """
536 542 pass
537 543
538 544 def _prompt_finished_hook(self):
539 545 """ Called immediately after a prompt is finished, i.e. when some input
540 546 will be processed and a new prompt displayed.
541 547 """
542 548 pass
543 549
544 550 def _up_pressed(self):
545 551 """ Called when the up key is pressed. Returns whether to continue
546 552 processing the event.
547 553 """
548 554 return True
549 555
550 556 def _down_pressed(self):
551 557 """ Called when the down key is pressed. Returns whether to continue
552 558 processing the event.
553 559 """
554 560 return True
555 561
556 562 def _tab_pressed(self):
557 563 """ Called when the tab key is pressed. Returns whether to continue
558 564 processing the event.
559 565 """
560 566 return False
561 567
562 568 #--------------------------------------------------------------------------
563 569 # 'ConsoleWidget' protected interface
564 570 #--------------------------------------------------------------------------
565 571
566 572 def _control_down(self, modifiers):
567 573 """ Given a KeyboardModifiers flags object, return whether the Control
568 574 key is down (on Mac OS, treat the Command key as a synonym for
569 575 Control).
570 576 """
571 577 down = bool(modifiers & QtCore.Qt.ControlModifier)
572 578
573 579 # Note: on Mac OS, ControlModifier corresponds to the Command key while
574 580 # MetaModifier corresponds to the Control key.
575 581 if sys.platform == 'darwin':
576 582 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
577 583
578 584 return down
579 585
580 586 def _complete_with_items(self, cursor, items):
581 587 """ Performs completion with 'items' at the specified cursor location.
582 588 """
583 589 if len(items) == 1:
584 590 cursor.setPosition(self.textCursor().position(),
585 591 QtGui.QTextCursor.KeepAnchor)
586 592 cursor.insertText(items[0])
587 593 elif len(items) > 1:
588 594 if self.gui_completion:
589 595 self._completion_widget.show_items(cursor, items)
590 596 else:
591 597 text = '\n'.join(items) + '\n'
592 598 self._write_text_keeping_prompt(text)
593 599
594 600 def _get_end_cursor(self):
595 601 """ Convenience method that returns a cursor for the last character.
596 602 """
597 603 cursor = self.textCursor()
598 604 cursor.movePosition(QtGui.QTextCursor.End)
599 605 return cursor
600 606
601 607 def _get_prompt_cursor(self):
602 608 """ Convenience method that returns a cursor for the prompt position.
603 609 """
604 610 cursor = self.textCursor()
605 611 cursor.setPosition(self._prompt_pos)
606 612 return cursor
607 613
608 614 def _get_selection_cursor(self, start, end):
609 615 """ Convenience method that returns a cursor with text selected between
610 616 the positions 'start' and 'end'.
611 617 """
612 618 cursor = self.textCursor()
613 619 cursor.setPosition(start)
614 620 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
615 621 return cursor
616 622
617 623 def _get_word_start_cursor(self, position):
618 624 """ Find the start of the word to the left the given position. If a
619 625 sequence of non-word characters precedes the first word, skip over
620 626 them. (This emulates the behavior of bash, emacs, etc.)
621 627 """
622 628 document = self.document()
623 629 position -= 1
624 630 while self._in_buffer(position) and \
625 631 not document.characterAt(position).isLetterOrNumber():
626 632 position -= 1
627 633 while self._in_buffer(position) and \
628 634 document.characterAt(position).isLetterOrNumber():
629 635 position -= 1
630 636 cursor = self.textCursor()
631 637 cursor.setPosition(position + 1)
632 638 return cursor
633 639
634 640 def _get_word_end_cursor(self, position):
635 641 """ Find the end of the word to the right the given position. If a
636 642 sequence of non-word characters precedes the first word, skip over
637 643 them. (This emulates the behavior of bash, emacs, etc.)
638 644 """
639 645 document = self.document()
640 646 end = self._get_end_cursor().position()
641 647 while position < end and \
642 648 not document.characterAt(position).isLetterOrNumber():
643 649 position += 1
644 650 while position < end and \
645 651 document.characterAt(position).isLetterOrNumber():
646 652 position += 1
647 653 cursor = self.textCursor()
648 654 cursor.setPosition(position)
649 655 return cursor
650 656
651 657 def _prompt_started(self):
652 658 """ Called immediately after a new prompt is displayed.
653 659 """
654 660 # Temporarily disable the maximum block count to permit undo/redo and
655 661 # to ensure that the prompt position does not change due to truncation.
656 662 self.setMaximumBlockCount(0)
657 663 self.setUndoRedoEnabled(True)
658 664
659 665 self.setReadOnly(False)
660 666 self.moveCursor(QtGui.QTextCursor.End)
661 667 self.centerCursor()
662 668
663 669 self._executing = False
664 670 self._prompt_started_hook()
665 671
666 672 def _prompt_finished(self):
667 673 """ Called immediately after a prompt is finished, i.e. when some input
668 674 will be processed and a new prompt displayed.
669 675 """
670 676 self.setUndoRedoEnabled(False)
671 677 self.setReadOnly(True)
672 678 self._prompt_finished_hook()
673 679
674 680 def _set_position(self, position):
675 681 """ Convenience method to set the position of the cursor.
676 682 """
677 683 cursor = self.textCursor()
678 684 cursor.setPosition(position)
679 685 self.setTextCursor(cursor)
680 686
681 687 def _set_selection(self, start, end):
682 688 """ Convenience method to set the current selected text.
683 689 """
684 690 self.setTextCursor(self._get_selection_cursor(start, end))
685 691
686 692 def _show_prompt(self, prompt=None):
687 693 """ Writes a new prompt at the end of the buffer. If 'prompt' is not
688 694 specified, uses the previous prompt.
689 695 """
690 696 if prompt is not None:
691 697 self._prompt = prompt
692 698 self.appendPlainText('\n' + self._prompt)
693 699 self._prompt_pos = self._get_end_cursor().position()
694 700 self._prompt_started()
695 701
696 702 def _show_continuation_prompt(self):
697 703 """ Writes a new continuation prompt at the end of the buffer.
698 704 """
699 705 self.appendPlainText(self._continuation_prompt)
700 706 self._prompt_started()
701 707
702 708 def _write_text_keeping_prompt(self, text):
703 709 """ Writes 'text' after the current prompt, then restores the old prompt
704 710 with its old input buffer.
705 711 """
706 712 input_buffer = self.input_buffer
707 713 self.appendPlainText('\n')
708 714 self._prompt_finished()
709 715
710 716 self.appendPlainText(text)
711 717 self._show_prompt()
712 718 self.input_buffer = input_buffer
713 719
714 720 def _in_buffer(self, position):
715 721 """ Returns whether the given position is inside the editing region.
716 722 """
717 723 return position >= self._prompt_pos
718 724
719 725 def _keep_cursor_in_buffer(self):
720 726 """ Ensures that the cursor is inside the editing region. Returns
721 727 whether the cursor was moved.
722 728 """
723 729 cursor = self.textCursor()
724 730 if cursor.position() < self._prompt_pos:
725 731 cursor.movePosition(QtGui.QTextCursor.End)
726 732 self.setTextCursor(cursor)
727 733 return True
728 734 else:
729 735 return False
730 736
731 737
732 738 class HistoryConsoleWidget(ConsoleWidget):
733 739 """ A ConsoleWidget that keeps a history of the commands that have been
734 740 executed.
735 741 """
736 742
737 743 #---------------------------------------------------------------------------
738 744 # 'QObject' interface
739 745 #---------------------------------------------------------------------------
740 746
741 747 def __init__(self, parent=None):
742 748 super(HistoryConsoleWidget, self).__init__(parent)
743 749
744 750 self._history = []
745 751 self._history_index = 0
746 752
747 753 #---------------------------------------------------------------------------
748 754 # 'ConsoleWidget' public interface
749 755 #---------------------------------------------------------------------------
750 756
751 757 def execute(self, source=None, hidden=False, interactive=False):
752 758 """ Reimplemented to the store history.
753 759 """
754 if source is None and not hidden:
755 history = self.input_buffer.rstrip()
760 if not hidden:
761 history = self.input_buffer if source is None else source
756 762
757 763 executed = super(HistoryConsoleWidget, self).execute(
758 764 source, hidden, interactive)
759 765
760 766 if executed and not hidden:
761 self._history.append(history)
767 self._history.append(history.rstrip())
762 768 self._history_index = len(self._history)
763 769
764 770 return executed
765 771
766 772 #---------------------------------------------------------------------------
767 773 # 'ConsoleWidget' abstract interface
768 774 #---------------------------------------------------------------------------
769 775
770 776 def _up_pressed(self):
771 777 """ Called when the up key is pressed. Returns whether to continue
772 778 processing the event.
773 779 """
774 780 prompt_cursor = self._get_prompt_cursor()
775 781 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
776 782 self.history_previous()
777 783
778 784 # Go to the first line of prompt for seemless history scrolling.
779 785 cursor = self._get_prompt_cursor()
780 786 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
781 787 self.setTextCursor(cursor)
782 788
783 789 return False
784 790 return True
785 791
786 792 def _down_pressed(self):
787 793 """ Called when the down key is pressed. Returns whether to continue
788 794 processing the event.
789 795 """
790 796 end_cursor = self._get_end_cursor()
791 797 if self.textCursor().blockNumber() == end_cursor.blockNumber():
792 798 self.history_next()
793 799 return False
794 800 return True
795 801
796 802 #---------------------------------------------------------------------------
797 803 # 'HistoryConsoleWidget' interface
798 804 #---------------------------------------------------------------------------
799 805
800 806 def history_previous(self):
801 807 """ If possible, set the input buffer to the previous item in the
802 808 history.
803 809 """
804 810 if self._history_index > 0:
805 811 self._history_index -= 1
806 812 self.input_buffer = self._history[self._history_index]
807 813
808 814 def history_next(self):
809 815 """ Set the input buffer to the next item in the history, or a blank
810 816 line if there is no subsequent item.
811 817 """
812 818 if self._history_index < len(self._history):
813 819 self._history_index += 1
814 820 if self._history_index < len(self._history):
815 821 self.input_buffer = self._history[self._history_index]
816 822 else:
817 823 self.input_buffer = ''
@@ -1,107 +1,105 b''
1 1 # System library imports
2 2 from PyQt4 import QtCore, QtGui
3 3
4 4 # Local imports
5 5 from frontend_widget import FrontendWidget
6 6
7 7
8 8 class IPythonWidget(FrontendWidget):
9 9 """ A FrontendWidget for an IPython kernel.
10 10 """
11 11
12 12 #---------------------------------------------------------------------------
13 13 # 'QObject' interface
14 14 #---------------------------------------------------------------------------
15 15
16 16 def __init__(self, parent=None):
17 17 super(IPythonWidget, self).__init__(parent)
18 18
19 19 self._magic_overrides = {}
20 20
21 21 #---------------------------------------------------------------------------
22 22 # 'ConsoleWidget' abstract interface
23 23 #---------------------------------------------------------------------------
24 24
25 25 def _execute(self, source, hidden):
26 26 """ Reimplemented to override magic commands.
27 27 """
28 28 magic_source = source.strip()
29 29 if magic_source.startswith('%'):
30 30 magic_source = magic_source[1:]
31 31 magic, sep, arguments = magic_source.partition(' ')
32 32 if not magic:
33 33 magic = magic_source
34 34
35 35 callback = self._magic_overrides.get(magic)
36 36 if callback:
37 37 output = callback(arguments)
38 38 if output:
39 39 self.appendPlainText(output)
40 40 self._show_prompt()
41 41 else:
42 42 super(IPythonWidget, self)._execute(source, hidden)
43 43
44 44 #---------------------------------------------------------------------------
45 45 # 'FrontendWidget' interface
46 46 #---------------------------------------------------------------------------
47 47
48 48 def execute_file(self, path, hidden=False):
49 49 """ Reimplemented to use the 'run' magic.
50 50 """
51 51 self.execute('run %s' % path, hidden=hidden)
52 52
53 53 #---------------------------------------------------------------------------
54 54 # 'IPythonWidget' interface
55 55 #---------------------------------------------------------------------------
56 56
57 57 def set_magic_override(self, magic, callback):
58 58 """ Overrides an IPython magic command. This magic will be intercepted
59 59 by the frontend rather than passed on to the kernel and 'callback'
60 60 will be called with a single argument: a string of argument(s) for
61 61 the magic. The callback can (optionally) return text to print to the
62 62 console.
63 63 """
64 64 self._magic_overrides[magic] = callback
65 65
66 66 def remove_magic_override(self, magic):
67 67 """ Removes the override for the specified magic, if there is one.
68 68 """
69 69 try:
70 70 del self._magic_overrides[magic]
71 71 except KeyError:
72 72 pass
73 73
74 74
75 75 if __name__ == '__main__':
76 76 import signal
77 77 from IPython.frontend.qt.kernelmanager import QtKernelManager
78 78
79 79 # Create a KernelManager.
80 80 kernel_manager = QtKernelManager()
81 81 kernel_manager.start_kernel()
82 82 kernel_manager.start_listening()
83 83
84 84 # Don't let Qt or ZMQ swallow KeyboardInterupts.
85 85 # FIXME: Gah, ZMQ swallows even custom signal handlers. So for now we leave
86 86 # behind a kernel process when Ctrl-C is pressed.
87 87 #def sigint_hook(signum, frame):
88 88 # QtGui.qApp.quit()
89 89 #signal.signal(signal.SIGINT, sigint_hook)
90 90 signal.signal(signal.SIGINT, signal.SIG_DFL)
91 91
92 92 # Create the application, making sure to clean up nicely when we exit.
93 93 app = QtGui.QApplication([])
94 94 def quit_hook():
95 95 kernel_manager.stop_listening()
96 96 kernel_manager.kill_kernel()
97 97 app.aboutToQuit.connect(quit_hook)
98 98
99 99 # Launch the application.
100 100 widget = IPythonWidget()
101 101 widget.kernel_manager = kernel_manager
102 102 widget.setWindowTitle('Python')
103 103 widget.resize(640, 480)
104 104 widget.show()
105 105 app.exec_()
106
107
General Comments 0
You need to be logged in to leave comments. Login now