##// END OF EJS Templates
Clearing the ConsoleWidget now cancels reading, which is the intuitive behavior.
epatters -
Show More
@@ -1,890 +1,892 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 self._reading_callback = None
145 145
146 146 # Set a monospaced font.
147 147 self.reset_font()
148 148
149 149 # Define a custom context menu.
150 150 self._context_menu = QtGui.QMenu(self)
151 151
152 152 copy_action = QtGui.QAction('Copy', self)
153 153 copy_action.triggered.connect(self.copy)
154 154 self.copyAvailable.connect(copy_action.setEnabled)
155 155 self._context_menu.addAction(copy_action)
156 156
157 157 self._paste_action = QtGui.QAction('Paste', self)
158 158 self._paste_action.triggered.connect(self.paste)
159 159 self._context_menu.addAction(self._paste_action)
160 160 self._context_menu.addSeparator()
161 161
162 162 select_all_action = QtGui.QAction('Select All', self)
163 163 select_all_action.triggered.connect(self.selectAll)
164 164 self._context_menu.addAction(select_all_action)
165 165
166 166 def event(self, event):
167 167 """ Reimplemented to override shortcuts, if necessary.
168 168 """
169 169 # On Mac OS, it is always unnecessary to override shortcuts, hence the
170 170 # check below. Users should just use the Control key instead of the
171 171 # Command key.
172 172 if self.override_shortcuts and \
173 173 sys.platform != 'darwin' and \
174 174 event.type() == QtCore.QEvent.ShortcutOverride and \
175 175 self._control_down(event.modifiers()) and \
176 176 event.key() in self._shortcuts:
177 177 event.accept()
178 178 return True
179 179 else:
180 180 return QtGui.QPlainTextEdit.event(self, event)
181 181
182 182 #---------------------------------------------------------------------------
183 183 # 'QWidget' interface
184 184 #---------------------------------------------------------------------------
185 185
186 186 def contextMenuEvent(self, event):
187 187 """ Reimplemented to create a menu without destructive actions like
188 188 'Cut' and 'Delete'.
189 189 """
190 190 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
191 191 self._paste_action.setEnabled(not clipboard_empty)
192 192
193 193 self._context_menu.exec_(event.globalPos())
194 194
195 195 def dragMoveEvent(self, event):
196 196 """ Reimplemented to disable dropping text.
197 197 """
198 198 event.ignore()
199 199
200 200 def keyPressEvent(self, event):
201 201 """ Reimplemented to create a console-like interface.
202 202 """
203 203 intercepted = False
204 204 cursor = self.textCursor()
205 205 position = cursor.position()
206 206 key = event.key()
207 207 ctrl_down = self._control_down(event.modifiers())
208 208 alt_down = event.modifiers() & QtCore.Qt.AltModifier
209 209 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
210 210
211 211 # Even though we have reimplemented 'paste', the C++ level slot is still
212 212 # called by Qt. So we intercept the key press here.
213 213 if event.matches(QtGui.QKeySequence.Paste):
214 214 self.paste()
215 215 intercepted = True
216 216
217 217 elif ctrl_down:
218 218 if key in self._ctrl_down_remap:
219 219 ctrl_down = False
220 220 key = self._ctrl_down_remap[key]
221 221 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
222 222 QtCore.Qt.NoModifier)
223 223
224 224 elif key == QtCore.Qt.Key_K:
225 225 if self._in_buffer(position):
226 226 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
227 227 QtGui.QTextCursor.KeepAnchor)
228 228 cursor.removeSelectedText()
229 229 intercepted = True
230 230
231 231 elif key == QtCore.Qt.Key_X:
232 232 intercepted = True
233 233
234 234 elif key == QtCore.Qt.Key_Y:
235 235 self.paste()
236 236 intercepted = True
237 237
238 238 elif alt_down:
239 239 if key == QtCore.Qt.Key_B:
240 240 self.setTextCursor(self._get_word_start_cursor(position))
241 241 intercepted = True
242 242
243 243 elif key == QtCore.Qt.Key_F:
244 244 self.setTextCursor(self._get_word_end_cursor(position))
245 245 intercepted = True
246 246
247 247 elif key == QtCore.Qt.Key_Backspace:
248 248 cursor = self._get_word_start_cursor(position)
249 249 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
250 250 cursor.removeSelectedText()
251 251 intercepted = True
252 252
253 253 elif key == QtCore.Qt.Key_D:
254 254 cursor = self._get_word_end_cursor(position)
255 255 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
256 256 cursor.removeSelectedText()
257 257 intercepted = True
258 258
259 259 if self._completion_widget.isVisible():
260 260 self._completion_widget.keyPressEvent(event)
261 261 intercepted = event.isAccepted()
262 262
263 263 else:
264 264 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
265 265 if self._reading:
266 266 self.appendPlainText('\n')
267 267 self._reading = False
268 268 if self._reading_callback:
269 269 self._reading_callback()
270 self._reading_callback = None
271 270 elif not self._executing:
272 271 self.execute(interactive=True)
273 272 intercepted = True
274 273
275 274 elif key == QtCore.Qt.Key_Up:
276 275 if self._reading or not self._up_pressed():
277 276 intercepted = True
278 277 else:
279 278 prompt_line = self._get_prompt_cursor().blockNumber()
280 279 intercepted = cursor.blockNumber() <= prompt_line
281 280
282 281 elif key == QtCore.Qt.Key_Down:
283 282 if self._reading or not self._down_pressed():
284 283 intercepted = True
285 284 else:
286 285 end_line = self._get_end_cursor().blockNumber()
287 286 intercepted = cursor.blockNumber() == end_line
288 287
289 288 elif key == QtCore.Qt.Key_Tab:
290 289 if self._reading:
291 290 intercepted = False
292 291 else:
293 292 intercepted = not self._tab_pressed()
294 293
295 294 elif key == QtCore.Qt.Key_Left:
296 295 intercepted = not self._in_buffer(position - 1)
297 296
298 297 elif key == QtCore.Qt.Key_Home:
299 298 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
300 299 start_pos = cursor.position()
301 300 start_line = cursor.blockNumber()
302 301 if start_line == self._get_prompt_cursor().blockNumber():
303 302 start_pos += len(self._prompt)
304 303 else:
305 304 start_pos += len(self._continuation_prompt)
306 305 if shift_down and self._in_buffer(position):
307 306 self._set_selection(position, start_pos)
308 307 else:
309 308 self._set_position(start_pos)
310 309 intercepted = True
311 310
312 311 elif key == QtCore.Qt.Key_Backspace and not alt_down:
313 312
314 313 # Line deletion (remove continuation prompt)
315 314 len_prompt = len(self._continuation_prompt)
316 315 if not self._reading and \
317 316 cursor.columnNumber() == len_prompt and \
318 317 position != self._prompt_pos:
319 318 cursor.setPosition(position - len_prompt,
320 319 QtGui.QTextCursor.KeepAnchor)
321 320 cursor.removeSelectedText()
322 321
323 322 # Regular backwards deletion
324 323 else:
325 324 anchor = cursor.anchor()
326 325 if anchor == position:
327 326 intercepted = not self._in_buffer(position - 1)
328 327 else:
329 328 intercepted = not self._in_buffer(min(anchor, position))
330 329
331 330 elif key == QtCore.Qt.Key_Delete:
332 331 anchor = cursor.anchor()
333 332 intercepted = not self._in_buffer(min(anchor, position))
334 333
335 334 # Don't move cursor if control is down to allow copy-paste using
336 335 # the keyboard in any part of the buffer.
337 336 if not ctrl_down:
338 337 self._keep_cursor_in_buffer()
339 338
340 339 if not intercepted:
341 340 QtGui.QPlainTextEdit.keyPressEvent(self, event)
342 341
343 342 #--------------------------------------------------------------------------
344 343 # 'QPlainTextEdit' interface
345 344 #--------------------------------------------------------------------------
346 345
347 346 def appendPlainText(self, text):
348 347 """ Reimplemented to not append text as a new paragraph, which doesn't
349 348 make sense for a console widget. Also, if enabled, handle ANSI
350 349 codes.
351 350 """
352 351 cursor = self.textCursor()
353 352 cursor.movePosition(QtGui.QTextCursor.End)
354 353
355 354 if self.ansi_codes:
356 355 format = QtGui.QTextCharFormat()
357 356 previous_end = 0
358 357 for match in self._ansi_pattern.finditer(text):
359 358 cursor.insertText(text[previous_end:match.start()], format)
360 359 previous_end = match.end()
361 360 for code in match.group(1).split(';'):
362 361 self._ansi_processor.set_code(int(code))
363 362 format = self._ansi_processor.get_format()
364 363 cursor.insertText(text[previous_end:], format)
365 364 else:
366 365 cursor.insertText(text)
367 366
368 367 def clear(self, keep_input=False):
369 """ Reimplemented to write a new prompt. If 'keep_input' is set,
370 restores the old input buffer when the new prompt is written.
368 """ Reimplemented to cancel reading and write a new prompt. If
369 'keep_input' is set, restores the old input buffer when the new
370 prompt is written.
371 371 """
372 372 super(ConsoleWidget, self).clear()
373
374 if keep_input:
373
374 input_buffer = ''
375 if self._reading:
376 self._reading = False
377 elif keep_input:
375 378 input_buffer = self.input_buffer
376 379 self._show_prompt()
377 if keep_input:
378 self.input_buffer = input_buffer
380 self.input_buffer = input_buffer
379 381
380 382 def paste(self):
381 383 """ Reimplemented to ensure that text is pasted in the editing region.
382 384 """
383 385 self._keep_cursor_in_buffer()
384 386 QtGui.QPlainTextEdit.paste(self)
385 387
386 388 def print_(self, printer):
387 389 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
388 390 slot has the wrong signature.
389 391 """
390 392 QtGui.QPlainTextEdit.print_(self, printer)
391 393
392 394 #---------------------------------------------------------------------------
393 395 # 'ConsoleWidget' public interface
394 396 #---------------------------------------------------------------------------
395 397
396 398 def execute(self, source=None, hidden=False, interactive=False):
397 399 """ Executes source or the input buffer, possibly prompting for more
398 400 input.
399 401
400 402 Parameters:
401 403 -----------
402 404 source : str, optional
403 405
404 406 The source to execute. If not specified, the input buffer will be
405 407 used. If specified and 'hidden' is False, the input buffer will be
406 408 replaced with the source before execution.
407 409
408 410 hidden : bool, optional (default False)
409 411
410 412 If set, no output will be shown and the prompt will not be modified.
411 413 In other words, it will be completely invisible to the user that
412 414 an execution has occurred.
413 415
414 416 interactive : bool, optional (default False)
415 417
416 418 Whether the console is to treat the source as having been manually
417 419 entered by the user. The effect of this parameter depends on the
418 420 subclass implementation.
419 421
420 422 Raises:
421 423 -------
422 424 RuntimeError
423 425 If incomplete input is given and 'hidden' is True. In this case,
424 426 it not possible to prompt for more input.
425 427
426 428 Returns:
427 429 --------
428 430 A boolean indicating whether the source was executed.
429 431 """
430 432 if not hidden:
431 433 if source is not None:
432 434 self.input_buffer = source
433 435
434 436 self.appendPlainText('\n')
435 437 self._executing_input_buffer = self.input_buffer
436 438 self._executing = True
437 439 self._prompt_finished()
438 440
439 441 real_source = self.input_buffer if source is None else source
440 442 complete = self._is_complete(real_source, interactive)
441 443 if complete:
442 444 if not hidden:
443 445 # The maximum block count is only in effect during execution.
444 446 # This ensures that _prompt_pos does not become invalid due to
445 447 # text truncation.
446 448 self.setMaximumBlockCount(self.buffer_size)
447 449 self._execute(real_source, hidden)
448 450 elif hidden:
449 451 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
450 452 else:
451 453 self._show_continuation_prompt()
452 454
453 455 return complete
454 456
455 457 def _get_input_buffer(self):
456 458 """ The text that the user has entered entered at the current prompt.
457 459 """
458 460 # If we're executing, the input buffer may not even exist anymore due to
459 461 # the limit imposed by 'buffer_size'. Therefore, we store it.
460 462 if self._executing:
461 463 return self._executing_input_buffer
462 464
463 465 cursor = self._get_end_cursor()
464 466 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
465 467
466 468 # Use QTextDocumentFragment intermediate object because it strips
467 469 # out the Unicode line break characters that Qt insists on inserting.
468 470 input_buffer = str(cursor.selection().toPlainText())
469 471
470 472 # Strip out continuation prompts.
471 473 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
472 474
473 475 def _set_input_buffer(self, string):
474 476 """ Replaces the text in the input buffer with 'string'.
475 477 """
476 478 # Add continuation prompts where necessary.
477 479 lines = string.splitlines()
478 480 for i in xrange(1, len(lines)):
479 481 lines[i] = self._continuation_prompt + lines[i]
480 482 string = '\n'.join(lines)
481 483
482 484 # Replace buffer with new text.
483 485 cursor = self._get_end_cursor()
484 486 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
485 487 cursor.insertText(string)
486 488 self.moveCursor(QtGui.QTextCursor.End)
487 489
488 490 input_buffer = property(_get_input_buffer, _set_input_buffer)
489 491
490 492 def _get_input_buffer_cursor_line(self):
491 493 """ The text in the line of the input buffer in which the user's cursor
492 494 rests. Returns a string if there is such a line; otherwise, None.
493 495 """
494 496 if self._executing:
495 497 return None
496 498 cursor = self.textCursor()
497 499 if cursor.position() >= self._prompt_pos:
498 500 text = str(cursor.block().text())
499 501 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
500 502 return text[len(self._prompt):]
501 503 else:
502 504 return text[len(self._continuation_prompt):]
503 505 else:
504 506 return None
505 507
506 508 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
507 509
508 510 def _get_font(self):
509 511 """ The base font being used by the ConsoleWidget.
510 512 """
511 513 return self.document().defaultFont()
512 514
513 515 def _set_font(self, font):
514 516 """ Sets the base font for the ConsoleWidget to the specified QFont.
515 517 """
516 518 self._completion_widget.setFont(font)
517 519 self.document().setDefaultFont(font)
518 520
519 521 font = property(_get_font, _set_font)
520 522
521 523 def reset_font(self):
522 524 """ Sets the font to the default fixed-width font for this platform.
523 525 """
524 526 if sys.platform == 'win32':
525 527 name = 'Courier'
526 528 elif sys.platform == 'darwin':
527 529 name = 'Monaco'
528 530 else:
529 531 name = 'Monospace'
530 532 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
531 533 font.setStyleHint(QtGui.QFont.TypeWriter)
532 534 self._set_font(font)
533 535
534 536 #---------------------------------------------------------------------------
535 537 # 'ConsoleWidget' abstract interface
536 538 #---------------------------------------------------------------------------
537 539
538 540 def _is_complete(self, source, interactive):
539 541 """ Returns whether 'source' can be executed. When triggered by an
540 542 Enter/Return key press, 'interactive' is True; otherwise, it is
541 543 False.
542 544 """
543 545 raise NotImplementedError
544 546
545 547 def _execute(self, source, hidden):
546 548 """ Execute 'source'. If 'hidden', do not show any output.
547 549 """
548 550 raise NotImplementedError
549 551
550 552 def _prompt_started_hook(self):
551 553 """ Called immediately after a new prompt is displayed.
552 554 """
553 555 pass
554 556
555 557 def _prompt_finished_hook(self):
556 558 """ Called immediately after a prompt is finished, i.e. when some input
557 559 will be processed and a new prompt displayed.
558 560 """
559 561 pass
560 562
561 563 def _up_pressed(self):
562 564 """ Called when the up key is pressed. Returns whether to continue
563 565 processing the event.
564 566 """
565 567 return True
566 568
567 569 def _down_pressed(self):
568 570 """ Called when the down key is pressed. Returns whether to continue
569 571 processing the event.
570 572 """
571 573 return True
572 574
573 575 def _tab_pressed(self):
574 576 """ Called when the tab key is pressed. Returns whether to continue
575 577 processing the event.
576 578 """
577 579 return False
578 580
579 581 #--------------------------------------------------------------------------
580 582 # 'ConsoleWidget' protected interface
581 583 #--------------------------------------------------------------------------
582 584
583 585 def _control_down(self, modifiers):
584 586 """ Given a KeyboardModifiers flags object, return whether the Control
585 587 key is down (on Mac OS, treat the Command key as a synonym for
586 588 Control).
587 589 """
588 590 down = bool(modifiers & QtCore.Qt.ControlModifier)
589 591
590 592 # Note: on Mac OS, ControlModifier corresponds to the Command key while
591 593 # MetaModifier corresponds to the Control key.
592 594 if sys.platform == 'darwin':
593 595 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
594 596
595 597 return down
596 598
597 599 def _complete_with_items(self, cursor, items):
598 600 """ Performs completion with 'items' at the specified cursor location.
599 601 """
600 602 if len(items) == 1:
601 603 cursor.setPosition(self.textCursor().position(),
602 604 QtGui.QTextCursor.KeepAnchor)
603 605 cursor.insertText(items[0])
604 606 elif len(items) > 1:
605 607 if self.gui_completion:
606 608 self._completion_widget.show_items(cursor, items)
607 609 else:
608 610 text = '\n'.join(items) + '\n'
609 611 self._write_text_keeping_prompt(text)
610 612
611 613 def _get_end_cursor(self):
612 614 """ Convenience method that returns a cursor for the last character.
613 615 """
614 616 cursor = self.textCursor()
615 617 cursor.movePosition(QtGui.QTextCursor.End)
616 618 return cursor
617 619
618 620 def _get_prompt_cursor(self):
619 621 """ Convenience method that returns a cursor for the prompt position.
620 622 """
621 623 cursor = self.textCursor()
622 624 cursor.setPosition(self._prompt_pos)
623 625 return cursor
624 626
625 627 def _get_selection_cursor(self, start, end):
626 628 """ Convenience method that returns a cursor with text selected between
627 629 the positions 'start' and 'end'.
628 630 """
629 631 cursor = self.textCursor()
630 632 cursor.setPosition(start)
631 633 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
632 634 return cursor
633 635
634 636 def _get_word_start_cursor(self, position):
635 637 """ Find the start of the word to the left the given position. If a
636 638 sequence of non-word characters precedes the first word, skip over
637 639 them. (This emulates the behavior of bash, emacs, etc.)
638 640 """
639 641 document = self.document()
640 642 position -= 1
641 643 while self._in_buffer(position) and \
642 644 not document.characterAt(position).isLetterOrNumber():
643 645 position -= 1
644 646 while self._in_buffer(position) and \
645 647 document.characterAt(position).isLetterOrNumber():
646 648 position -= 1
647 649 cursor = self.textCursor()
648 650 cursor.setPosition(position + 1)
649 651 return cursor
650 652
651 653 def _get_word_end_cursor(self, position):
652 654 """ Find the end of the word to the right the given position. If a
653 655 sequence of non-word characters precedes the first word, skip over
654 656 them. (This emulates the behavior of bash, emacs, etc.)
655 657 """
656 658 document = self.document()
657 659 end = self._get_end_cursor().position()
658 660 while position < end and \
659 661 not document.characterAt(position).isLetterOrNumber():
660 662 position += 1
661 663 while position < end and \
662 664 document.characterAt(position).isLetterOrNumber():
663 665 position += 1
664 666 cursor = self.textCursor()
665 667 cursor.setPosition(position)
666 668 return cursor
667 669
668 670 def _prompt_started(self):
669 671 """ Called immediately after a new prompt is displayed.
670 672 """
671 673 # Temporarily disable the maximum block count to permit undo/redo and
672 674 # to ensure that the prompt position does not change due to truncation.
673 675 self.setMaximumBlockCount(0)
674 676 self.setUndoRedoEnabled(True)
675 677
676 678 self.setReadOnly(False)
677 679 self.moveCursor(QtGui.QTextCursor.End)
678 680 self.centerCursor()
679 681
680 682 self._executing = False
681 683 self._prompt_started_hook()
682 684
683 685 def _prompt_finished(self):
684 686 """ Called immediately after a prompt is finished, i.e. when some input
685 687 will be processed and a new prompt displayed.
686 688 """
687 689 self.setUndoRedoEnabled(False)
688 690 self.setReadOnly(True)
689 691 self._prompt_finished_hook()
690 692
691 693 def _readline(self, prompt='', callback=None):
692 694 """ Reads one line of input from the user.
693 695
694 696 Parameters
695 697 ----------
696 698 prompt : str, optional
697 699 The prompt to print before reading the line.
698 700
699 701 callback : callable, optional
700 702 A callback to execute with the read line. If not specified, input is
701 703 read *synchronously* and this method does not return until it has
702 704 been read.
703 705
704 706 Returns
705 707 -------
706 708 If a callback is specified, returns nothing. Otherwise, returns the
707 709 input string with the trailing newline stripped.
708 710 """
709 711 if self._reading:
710 712 raise RuntimeError('Cannot read a line. Widget is already reading.')
711 713
712 714 if not callback and not self.isVisible():
713 715 # If the user cannot see the widget, this function cannot return.
714 716 raise RuntimeError('Cannot synchronously read a line if the widget'
715 717 'is not visible!')
716 718
717 719 self._reading = True
718 720 self._show_prompt(prompt, newline=False)
719 721
720 722 if callback is None:
721 723 self._reading_callback = None
722 724 while self._reading:
723 725 QtCore.QCoreApplication.processEvents()
724 726 return self.input_buffer.rstrip('\n')
725 727
726 728 else:
727 729 self._reading_callback = lambda: \
728 730 callback(self.input_buffer.rstrip('\n'))
729 731
730 732 def _set_position(self, position):
731 733 """ Convenience method to set the position of the cursor.
732 734 """
733 735 cursor = self.textCursor()
734 736 cursor.setPosition(position)
735 737 self.setTextCursor(cursor)
736 738
737 739 def _set_selection(self, start, end):
738 740 """ Convenience method to set the current selected text.
739 741 """
740 742 self.setTextCursor(self._get_selection_cursor(start, end))
741 743
742 744 def _show_prompt(self, prompt=None, newline=True):
743 745 """ Writes a new prompt at the end of the buffer.
744 746
745 747 Parameters
746 748 ----------
747 749 prompt : str, optional
748 750 The prompt to show. If not specified, the previous prompt is used.
749 751
750 752 newline : bool, optional (default True)
751 753 If set, a new line will be written before showing the prompt if
752 754 there is not already a newline at the end of the buffer.
753 755 """
754 756 if newline:
755 757 cursor = self._get_end_cursor()
756 758 if cursor.position() > 0:
757 759 cursor.movePosition(QtGui.QTextCursor.Left,
758 760 QtGui.QTextCursor.KeepAnchor)
759 761 if str(cursor.selection().toPlainText()) != '\n':
760 762 self.appendPlainText('\n')
761 763
762 764 if prompt is not None:
763 765 self._prompt = prompt
764 766 self.appendPlainText(self._prompt)
765 767
766 768 self._prompt_pos = self._get_end_cursor().position()
767 769 self._prompt_started()
768 770
769 771 def _show_continuation_prompt(self):
770 772 """ Writes a new continuation prompt at the end of the buffer.
771 773 """
772 774 self.appendPlainText(self._continuation_prompt)
773 775 self._prompt_started()
774 776
775 777 def _write_text_keeping_prompt(self, text):
776 778 """ Writes 'text' after the current prompt, then restores the old prompt
777 779 with its old input buffer.
778 780 """
779 781 input_buffer = self.input_buffer
780 782 self.appendPlainText('\n')
781 783 self._prompt_finished()
782 784
783 785 self.appendPlainText(text)
784 786 self._show_prompt()
785 787 self.input_buffer = input_buffer
786 788
787 789 def _in_buffer(self, position):
788 790 """ Returns whether the given position is inside the editing region.
789 791 """
790 792 return position >= self._prompt_pos
791 793
792 794 def _keep_cursor_in_buffer(self):
793 795 """ Ensures that the cursor is inside the editing region. Returns
794 796 whether the cursor was moved.
795 797 """
796 798 cursor = self.textCursor()
797 799 if cursor.position() < self._prompt_pos:
798 800 cursor.movePosition(QtGui.QTextCursor.End)
799 801 self.setTextCursor(cursor)
800 802 return True
801 803 else:
802 804 return False
803 805
804 806
805 807 class HistoryConsoleWidget(ConsoleWidget):
806 808 """ A ConsoleWidget that keeps a history of the commands that have been
807 809 executed.
808 810 """
809 811
810 812 #---------------------------------------------------------------------------
811 813 # 'QObject' interface
812 814 #---------------------------------------------------------------------------
813 815
814 816 def __init__(self, parent=None):
815 817 super(HistoryConsoleWidget, self).__init__(parent)
816 818
817 819 self._history = []
818 820 self._history_index = 0
819 821
820 822 #---------------------------------------------------------------------------
821 823 # 'ConsoleWidget' public interface
822 824 #---------------------------------------------------------------------------
823 825
824 826 def execute(self, source=None, hidden=False, interactive=False):
825 827 """ Reimplemented to the store history.
826 828 """
827 829 if not hidden:
828 830 history = self.input_buffer if source is None else source
829 831
830 832 executed = super(HistoryConsoleWidget, self).execute(
831 833 source, hidden, interactive)
832 834
833 835 if executed and not hidden:
834 836 self._history.append(history.rstrip())
835 837 self._history_index = len(self._history)
836 838
837 839 return executed
838 840
839 841 #---------------------------------------------------------------------------
840 842 # 'ConsoleWidget' abstract interface
841 843 #---------------------------------------------------------------------------
842 844
843 845 def _up_pressed(self):
844 846 """ Called when the up key is pressed. Returns whether to continue
845 847 processing the event.
846 848 """
847 849 prompt_cursor = self._get_prompt_cursor()
848 850 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
849 851 self.history_previous()
850 852
851 853 # Go to the first line of prompt for seemless history scrolling.
852 854 cursor = self._get_prompt_cursor()
853 855 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
854 856 self.setTextCursor(cursor)
855 857
856 858 return False
857 859 return True
858 860
859 861 def _down_pressed(self):
860 862 """ Called when the down key is pressed. Returns whether to continue
861 863 processing the event.
862 864 """
863 865 end_cursor = self._get_end_cursor()
864 866 if self.textCursor().blockNumber() == end_cursor.blockNumber():
865 867 self.history_next()
866 868 return False
867 869 return True
868 870
869 871 #---------------------------------------------------------------------------
870 872 # 'HistoryConsoleWidget' interface
871 873 #---------------------------------------------------------------------------
872 874
873 875 def history_previous(self):
874 876 """ If possible, set the input buffer to the previous item in the
875 877 history.
876 878 """
877 879 if self._history_index > 0:
878 880 self._history_index -= 1
879 881 self.input_buffer = self._history[self._history_index]
880 882
881 883 def history_next(self):
882 884 """ Set the input buffer to the next item in the history, or a blank
883 885 line if there is no subsequent item.
884 886 """
885 887 if self._history_index < len(self._history):
886 888 self._history_index += 1
887 889 if self._history_index < len(self._history):
888 890 self.input_buffer = self._history[self._history_index]
889 891 else:
890 892 self.input_buffer = ''
General Comments 0
You need to be logged in to leave comments. Login now