##// END OF EJS Templates
* Added option (disabled by default) to ConsoleWidget for overriding global/window-level shortcuts....
epatters -
Show More
@@ -1,718 +1,739 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 # Regex to match ANSI escape sequences
104 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
103 # Whether to process ANSI escape codes.
104 ansi_codes = True
105
106 # The maximum number of lines of text before truncation.
107 buffer_size = 500
108
109 # Whether to use a CompletionWidget or plain text output for tab completion.
110 gui_completion = True
105 111
106 # When ctrl is pressed, map certain keys to other keys (without the ctrl):
112 # Whether to override ShortcutEvents for the keybindings defined by this
113 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
114 # priority (when it has focus) over, e.g., window-level menu shortcuts.
115 override_shortcuts = False
116
117 # Protected class variables.
118 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
107 119 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
108 120 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
109 121 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
110 122 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
111 123 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
112 124 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
113 125 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
114 126
115 127 #---------------------------------------------------------------------------
116 128 # 'QWidget' interface
117 129 #---------------------------------------------------------------------------
118 130
119 131 def __init__(self, parent=None):
120 132 QtGui.QPlainTextEdit.__init__(self, parent)
121 133
122 # Initialize public and protected variables.
123 self.ansi_codes = True
124 self.buffer_size = 500
125 self.gui_completion = True
134 # Initialize protected variables.
126 135 self._ansi_processor = QtAnsiCodeProcessor()
127 136 self._completion_widget = CompletionWidget(self)
128 137 self._continuation_prompt = '> '
129 138 self._executing = False
130 139 self._prompt = ''
131 140 self._prompt_pos = 0
132 141 self._reading = False
133 142
134 143 # Set a monospaced font.
135 144 self.reset_font()
136 145
137 146 # Define a custom context menu.
138 147 self._context_menu = QtGui.QMenu(self)
139 148
140 149 copy_action = QtGui.QAction('Copy', self)
141 150 copy_action.triggered.connect(self.copy)
142 151 self.copyAvailable.connect(copy_action.setEnabled)
143 152 self._context_menu.addAction(copy_action)
144 153
145 154 self._paste_action = QtGui.QAction('Paste', self)
146 155 self._paste_action.triggered.connect(self.paste)
147 156 self._context_menu.addAction(self._paste_action)
148 157 self._context_menu.addSeparator()
149 158
150 159 select_all_action = QtGui.QAction('Select All', self)
151 160 select_all_action.triggered.connect(self.selectAll)
152 161 self._context_menu.addAction(select_all_action)
153 162
154 163 def contextMenuEvent(self, event):
155 164 """ Reimplemented to create a menu without destructive actions like
156 165 'Cut' and 'Delete'.
157 166 """
158 167 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
159 168 self._paste_action.setEnabled(not clipboard_empty)
160 169
161 170 self._context_menu.exec_(event.globalPos())
162 171
172 def event(self, event):
173 """ Reimplemented to override shortcuts, if necessary.
174 """
175 if self.override_shortcuts and \
176 event.type() == QtCore.QEvent.ShortcutOverride and \
177 event.modifiers() & QtCore.Qt.ControlModifier and \
178 event.key() in self._ctrl_down_remap:
179 event.accept()
180 return True
181 else:
182 return QtGui.QPlainTextEdit.event(self, event)
183
163 184 def keyPressEvent(self, event):
164 185 """ Reimplemented to create a console-like interface.
165 186 """
166 187 intercepted = False
167 188 cursor = self.textCursor()
168 189 position = cursor.position()
169 190 key = event.key()
170 191 ctrl_down = event.modifiers() & QtCore.Qt.ControlModifier
171 192 alt_down = event.modifiers() & QtCore.Qt.AltModifier
172 193 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
173 194
174 195 # Even though we have reimplemented 'paste', the C++ level slot is still
175 196 # called by Qt. So we intercept the key press here.
176 197 if event.matches(QtGui.QKeySequence.Paste):
177 198 self.paste()
178 199 intercepted = True
179 200
180 201 elif ctrl_down:
181 202 if key in self._ctrl_down_remap:
182 203 ctrl_down = False
183 204 key = self._ctrl_down_remap[key]
184 205 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
185 206 QtCore.Qt.NoModifier)
186 207
187 208 elif key == QtCore.Qt.Key_K:
188 209 if self._in_buffer(position):
189 210 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
190 211 QtGui.QTextCursor.KeepAnchor)
191 212 cursor.removeSelectedText()
192 213 intercepted = True
193 214
194 215 elif key == QtCore.Qt.Key_Y:
195 216 self.paste()
196 217 intercepted = True
197 218
198 219 elif alt_down:
199 220 if key == QtCore.Qt.Key_B:
200 221 self.setTextCursor(self._get_word_start_cursor(position))
201 222 intercepted = True
202 223
203 224 elif key == QtCore.Qt.Key_F:
204 225 self.setTextCursor(self._get_word_end_cursor(position))
205 226 intercepted = True
206 227
207 228 elif key == QtCore.Qt.Key_Backspace:
208 229 cursor = self._get_word_start_cursor(position)
209 230 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
210 231 cursor.removeSelectedText()
211 232 intercepted = True
212 233
213 234 elif key == QtCore.Qt.Key_D:
214 235 cursor = self._get_word_end_cursor(position)
215 236 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
216 237 cursor.removeSelectedText()
217 238 intercepted = True
218 239
219 240 if self._completion_widget.isVisible():
220 241 self._completion_widget.keyPressEvent(event)
221 242 intercepted = event.isAccepted()
222 243
223 244 else:
224 245 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
225 246 if self._reading:
226 247 self._reading = False
227 248 elif not self._executing:
228 249 self.execute(interactive=True)
229 250 intercepted = True
230 251
231 252 elif key == QtCore.Qt.Key_Up:
232 253 if self._reading or not self._up_pressed():
233 254 intercepted = True
234 255 else:
235 256 prompt_line = self._get_prompt_cursor().blockNumber()
236 257 intercepted = cursor.blockNumber() <= prompt_line
237 258
238 259 elif key == QtCore.Qt.Key_Down:
239 260 if self._reading or not self._down_pressed():
240 261 intercepted = True
241 262 else:
242 263 end_line = self._get_end_cursor().blockNumber()
243 264 intercepted = cursor.blockNumber() == end_line
244 265
245 266 elif key == QtCore.Qt.Key_Tab:
246 267 if self._reading:
247 268 intercepted = False
248 269 else:
249 270 intercepted = not self._tab_pressed()
250 271
251 272 elif key == QtCore.Qt.Key_Left:
252 273 intercepted = not self._in_buffer(position - 1)
253 274
254 275 elif key == QtCore.Qt.Key_Home:
255 276 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
256 277 start_pos = cursor.position()
257 278 start_line = cursor.blockNumber()
258 279 if start_line == self._get_prompt_cursor().blockNumber():
259 280 start_pos += len(self._prompt)
260 281 else:
261 282 start_pos += len(self._continuation_prompt)
262 283 if shift_down and self._in_buffer(position):
263 284 self._set_selection(position, start_pos)
264 285 else:
265 286 self._set_position(start_pos)
266 287 intercepted = True
267 288
268 289 elif key == QtCore.Qt.Key_Backspace and not alt_down:
269 290
270 291 # Line deletion (remove continuation prompt)
271 292 len_prompt = len(self._continuation_prompt)
272 293 if cursor.columnNumber() == len_prompt and \
273 294 position != self._prompt_pos:
274 295 cursor.setPosition(position - len_prompt,
275 296 QtGui.QTextCursor.KeepAnchor)
276 297 cursor.removeSelectedText()
277 298
278 299 # Regular backwards deletion
279 300 else:
280 301 anchor = cursor.anchor()
281 302 if anchor == position:
282 303 intercepted = not self._in_buffer(position - 1)
283 304 else:
284 305 intercepted = not self._in_buffer(min(anchor, position))
285 306
286 307 elif key == QtCore.Qt.Key_Delete:
287 308 anchor = cursor.anchor()
288 309 intercepted = not self._in_buffer(min(anchor, position))
289 310
290 311 # Don't move cursor if control is down to allow copy-paste using
291 312 # the keyboard in any part of the buffer.
292 313 if not ctrl_down:
293 314 self._keep_cursor_in_buffer()
294 315
295 316 if not intercepted:
296 317 QtGui.QPlainTextEdit.keyPressEvent(self, event)
297 318
298 319 #--------------------------------------------------------------------------
299 320 # 'QPlainTextEdit' interface
300 321 #--------------------------------------------------------------------------
301 322
302 323 def appendPlainText(self, text):
303 324 """ Reimplemented to not append text as a new paragraph, which doesn't
304 325 make sense for a console widget. Also, if enabled, handle ANSI
305 326 codes.
306 327 """
307 328 cursor = self.textCursor()
308 329 cursor.movePosition(QtGui.QTextCursor.End)
309 330
310 331 if self.ansi_codes:
311 332 format = QtGui.QTextCharFormat()
312 333 previous_end = 0
313 334 for match in self._ansi_pattern.finditer(text):
314 335 cursor.insertText(text[previous_end:match.start()], format)
315 336 previous_end = match.end()
316 337 for code in match.group(1).split(';'):
317 338 self._ansi_processor.set_code(int(code))
318 339 format = self._ansi_processor.get_format()
319 340 cursor.insertText(text[previous_end:], format)
320 341 else:
321 342 cursor.insertText(text)
322 343
323 344 def clear(self, keep_input=False):
324 345 """ Reimplemented to write a new prompt. If 'keep_input' is set,
325 346 restores the old input buffer when the new prompt is written.
326 347 """
327 348 super(ConsoleWidget, self).clear()
328 349
329 350 if keep_input:
330 351 input_buffer = self.input_buffer
331 352 self._show_prompt()
332 353 if keep_input:
333 354 self.input_buffer = input_buffer
334 355
335 356 def paste(self):
336 357 """ Reimplemented to ensure that text is pasted in the editing region.
337 358 """
338 359 self._keep_cursor_in_buffer()
339 360 QtGui.QPlainTextEdit.paste(self)
340 361
341 362 def print_(self, printer):
342 363 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
343 364 slot has the wrong signature.
344 365 """
345 366 QtGui.QPlainTextEdit.print_(self, printer)
346 367
347 368 #---------------------------------------------------------------------------
348 369 # 'ConsoleWidget' public interface
349 370 #---------------------------------------------------------------------------
350 371
351 372 def execute(self, interactive=False):
352 373 """ Execute the text in the input buffer. Returns whether the input
353 374 buffer was completely processed and a new prompt created.
354 375 """
355 376 self.appendPlainText('\n')
356 377 self._executing_input_buffer = self.input_buffer
357 378 self._executing = True
358 379 self._prompt_finished()
359 380 return self._execute(interactive=interactive)
360 381
361 382 def _get_input_buffer(self):
362 383 """ The text that the user has entered entered at the current prompt.
363 384 """
364 385 # If we're executing, the input buffer may not even exist anymore due to
365 386 # the limit imposed by 'buffer_size'. Therefore, we store it.
366 387 if self._executing:
367 388 return self._executing_input_buffer
368 389
369 390 cursor = self._get_end_cursor()
370 391 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
371 392
372 393 # Use QTextDocumentFragment intermediate object because it strips
373 394 # out the Unicode line break characters that Qt insists on inserting.
374 395 input_buffer = str(cursor.selection().toPlainText())
375 396
376 397 # Strip out continuation prompts.
377 398 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
378 399
379 400 def _set_input_buffer(self, string):
380 401 """ Replaces the text in the input buffer with 'string'.
381 402 """
382 403 # Add continuation prompts where necessary.
383 404 lines = string.splitlines()
384 405 for i in xrange(1, len(lines)):
385 406 lines[i] = self._continuation_prompt + lines[i]
386 407 string = '\n'.join(lines)
387 408
388 409 # Replace buffer with new text.
389 410 cursor = self._get_end_cursor()
390 411 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
391 412 cursor.insertText(string)
392 413 self.moveCursor(QtGui.QTextCursor.End)
393 414
394 415 input_buffer = property(_get_input_buffer, _set_input_buffer)
395 416
396 417 def _get_input_buffer_cursor_line(self):
397 418 """ The text in the line of the input buffer in which the user's cursor
398 419 rests. Returns a string if there is such a line; otherwise, None.
399 420 """
400 421 if self._executing:
401 422 return None
402 423 cursor = self.textCursor()
403 424 if cursor.position() >= self._prompt_pos:
404 425 text = str(cursor.block().text())
405 426 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
406 427 return text[len(self._prompt):]
407 428 else:
408 429 return text[len(self._continuation_prompt):]
409 430 else:
410 431 return None
411 432
412 433 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
413 434
414 435 def _get_font(self):
415 436 """ The base font being used by the ConsoleWidget.
416 437 """
417 438 return self.document().defaultFont()
418 439
419 440 def _set_font(self, font):
420 441 """ Sets the base font for the ConsoleWidget to the specified QFont.
421 442 """
422 443 self._completion_widget.setFont(font)
423 444 self.document().setDefaultFont(font)
424 445
425 446 font = property(_get_font, _set_font)
426 447
427 448 def reset_font(self):
428 449 """ Sets the font to the default fixed-width font for this platform.
429 450 """
430 451 if sys.platform == 'win32':
431 452 name = 'Courier'
432 453 elif sys.platform == 'darwin':
433 454 name = 'Monaco'
434 455 else:
435 456 name = 'Monospace'
436 457 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
437 458 font.setStyleHint(QtGui.QFont.TypeWriter)
438 459 self._set_font(font)
439 460
440 461 #---------------------------------------------------------------------------
441 462 # 'ConsoleWidget' abstract interface
442 463 #---------------------------------------------------------------------------
443 464
444 465 def _execute(self, interactive):
445 466 """ Called to execute the input buffer. When triggered by an the enter
446 467 key press, 'interactive' is True; otherwise, it is False. Returns
447 468 whether the input buffer was completely processed and a new prompt
448 469 created.
449 470 """
450 471 raise NotImplementedError
451 472
452 473 def _prompt_started_hook(self):
453 474 """ Called immediately after a new prompt is displayed.
454 475 """
455 476 pass
456 477
457 478 def _prompt_finished_hook(self):
458 479 """ Called immediately after a prompt is finished, i.e. when some input
459 480 will be processed and a new prompt displayed.
460 481 """
461 482 pass
462 483
463 484 def _up_pressed(self):
464 485 """ Called when the up key is pressed. Returns whether to continue
465 486 processing the event.
466 487 """
467 488 return True
468 489
469 490 def _down_pressed(self):
470 491 """ Called when the down key is pressed. Returns whether to continue
471 492 processing the event.
472 493 """
473 494 return True
474 495
475 496 def _tab_pressed(self):
476 497 """ Called when the tab key is pressed. Returns whether to continue
477 498 processing the event.
478 499 """
479 500 return False
480 501
481 502 #--------------------------------------------------------------------------
482 503 # 'ConsoleWidget' protected interface
483 504 #--------------------------------------------------------------------------
484 505
485 506 def _complete_with_items(self, cursor, items):
486 507 """ Performs completion with 'items' at the specified cursor location.
487 508 """
488 509 if len(items) == 1:
489 510 cursor.setPosition(self.textCursor().position(),
490 511 QtGui.QTextCursor.KeepAnchor)
491 512 cursor.insertText(items[0])
492 513 elif len(items) > 1:
493 514 if self.gui_completion:
494 515 self._completion_widget.show_items(cursor, items)
495 516 else:
496 517 text = '\n'.join(items) + '\n'
497 518 self._write_text_keeping_prompt(text)
498 519
499 520 def _get_end_cursor(self):
500 521 """ Convenience method that returns a cursor for the last character.
501 522 """
502 523 cursor = self.textCursor()
503 524 cursor.movePosition(QtGui.QTextCursor.End)
504 525 return cursor
505 526
506 527 def _get_prompt_cursor(self):
507 528 """ Convenience method that returns a cursor for the prompt position.
508 529 """
509 530 cursor = self.textCursor()
510 531 cursor.setPosition(self._prompt_pos)
511 532 return cursor
512 533
513 534 def _get_selection_cursor(self, start, end):
514 535 """ Convenience method that returns a cursor with text selected between
515 536 the positions 'start' and 'end'.
516 537 """
517 538 cursor = self.textCursor()
518 539 cursor.setPosition(start)
519 540 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
520 541 return cursor
521 542
522 543 def _get_word_start_cursor(self, position):
523 544 """ Find the start of the word to the left the given position. If a
524 545 sequence of non-word characters precedes the first word, skip over
525 546 them. (This emulates the behavior of bash, emacs, etc.)
526 547 """
527 548 document = self.document()
528 549 position -= 1
529 550 while self._in_buffer(position) and \
530 551 not document.characterAt(position).isLetterOrNumber():
531 552 position -= 1
532 553 while self._in_buffer(position) and \
533 554 document.characterAt(position).isLetterOrNumber():
534 555 position -= 1
535 556 cursor = self.textCursor()
536 557 cursor.setPosition(position + 1)
537 558 return cursor
538 559
539 560 def _get_word_end_cursor(self, position):
540 561 """ Find the end of the word to the right the given position. If a
541 562 sequence of non-word characters precedes the first word, skip over
542 563 them. (This emulates the behavior of bash, emacs, etc.)
543 564 """
544 565 document = self.document()
545 566 end = self._get_end_cursor().position()
546 567 while position < end and \
547 568 not document.characterAt(position).isLetterOrNumber():
548 569 position += 1
549 570 while position < end and \
550 571 document.characterAt(position).isLetterOrNumber():
551 572 position += 1
552 573 cursor = self.textCursor()
553 574 cursor.setPosition(position)
554 575 return cursor
555 576
556 577 def _prompt_started(self):
557 578 """ Called immediately after a new prompt is displayed.
558 579 """
559 580 # Temporarily disable the maximum block count to permit undo/redo.
560 581 self.setMaximumBlockCount(0)
561 582 self.setUndoRedoEnabled(True)
562 583
563 584 self.setReadOnly(False)
564 585 self.moveCursor(QtGui.QTextCursor.End)
565 586 self.centerCursor()
566 587
567 588 self._executing = False
568 589 self._prompt_started_hook()
569 590
570 591 def _prompt_finished(self):
571 592 """ Called immediately after a prompt is finished, i.e. when some input
572 593 will be processed and a new prompt displayed.
573 594 """
574 595 # This has the (desired) side effect of disabling the undo/redo history.
575 596 self.setMaximumBlockCount(self.buffer_size)
576 597
577 598 self.setReadOnly(True)
578 599 self._prompt_finished_hook()
579 600
580 601 def _set_position(self, position):
581 602 """ Convenience method to set the position of the cursor.
582 603 """
583 604 cursor = self.textCursor()
584 605 cursor.setPosition(position)
585 606 self.setTextCursor(cursor)
586 607
587 608 def _set_selection(self, start, end):
588 609 """ Convenience method to set the current selected text.
589 610 """
590 611 self.setTextCursor(self._get_selection_cursor(start, end))
591 612
592 613 def _show_prompt(self, prompt=None):
593 614 """ Writes a new prompt at the end of the buffer. If 'prompt' is not
594 615 specified, uses the previous prompt.
595 616 """
596 617 if prompt is not None:
597 618 self._prompt = prompt
598 619 self.appendPlainText('\n' + self._prompt)
599 620 self._prompt_pos = self._get_end_cursor().position()
600 621 self._prompt_started()
601 622
602 623 def _show_continuation_prompt(self):
603 624 """ Writes a new continuation prompt at the end of the buffer.
604 625 """
605 626 self.appendPlainText(self._continuation_prompt)
606 627 self._prompt_started()
607 628
608 629 def _write_text_keeping_prompt(self, text):
609 630 """ Writes 'text' after the current prompt, then restores the old prompt
610 631 with its old input buffer.
611 632 """
612 633 input_buffer = self.input_buffer
613 634 self.appendPlainText('\n')
614 635 self._prompt_finished()
615 636
616 637 self.appendPlainText(text)
617 638 self._show_prompt()
618 639 self.input_buffer = input_buffer
619 640
620 641 def _in_buffer(self, position):
621 642 """ Returns whether the given position is inside the editing region.
622 643 """
623 644 return position >= self._prompt_pos
624 645
625 646 def _keep_cursor_in_buffer(self):
626 647 """ Ensures that the cursor is inside the editing region. Returns
627 648 whether the cursor was moved.
628 649 """
629 650 cursor = self.textCursor()
630 651 if cursor.position() < self._prompt_pos:
631 652 cursor.movePosition(QtGui.QTextCursor.End)
632 653 self.setTextCursor(cursor)
633 654 return True
634 655 else:
635 656 return False
636 657
637 658
638 659 class HistoryConsoleWidget(ConsoleWidget):
639 660 """ A ConsoleWidget that keeps a history of the commands that have been
640 661 executed.
641 662 """
642 663
643 664 #---------------------------------------------------------------------------
644 665 # 'QWidget' interface
645 666 #---------------------------------------------------------------------------
646 667
647 668 def __init__(self, parent=None):
648 669 super(HistoryConsoleWidget, self).__init__(parent)
649 670
650 671 self._history = []
651 672 self._history_index = 0
652 673
653 674 #---------------------------------------------------------------------------
654 675 # 'ConsoleWidget' public interface
655 676 #---------------------------------------------------------------------------
656 677
657 678 def execute(self, interactive=False):
658 679 """ Reimplemented to the store history.
659 680 """
660 681 stripped = self.input_buffer.rstrip()
661 682 executed = super(HistoryConsoleWidget, self).execute(interactive)
662 683 if executed:
663 684 self._history.append(stripped)
664 685 self._history_index = len(self._history)
665 686 return executed
666 687
667 688 #---------------------------------------------------------------------------
668 689 # 'ConsoleWidget' abstract interface
669 690 #---------------------------------------------------------------------------
670 691
671 692 def _up_pressed(self):
672 693 """ Called when the up key is pressed. Returns whether to continue
673 694 processing the event.
674 695 """
675 696 prompt_cursor = self._get_prompt_cursor()
676 697 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
677 698 self.history_previous()
678 699
679 700 # Go to the first line of prompt for seemless history scrolling.
680 701 cursor = self._get_prompt_cursor()
681 702 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
682 703 self.setTextCursor(cursor)
683 704
684 705 return False
685 706 return True
686 707
687 708 def _down_pressed(self):
688 709 """ Called when the down key is pressed. Returns whether to continue
689 710 processing the event.
690 711 """
691 712 end_cursor = self._get_end_cursor()
692 713 if self.textCursor().blockNumber() == end_cursor.blockNumber():
693 714 self.history_next()
694 715 return False
695 716 return True
696 717
697 718 #---------------------------------------------------------------------------
698 719 # 'HistoryConsoleWidget' interface
699 720 #---------------------------------------------------------------------------
700 721
701 722 def history_previous(self):
702 723 """ If possible, set the input buffer to the previous item in the
703 724 history.
704 725 """
705 726 if self._history_index > 0:
706 727 self._history_index -= 1
707 728 self.input_buffer = self._history[self._history_index]
708 729
709 730 def history_next(self):
710 731 """ Set the input buffer to the next item in the history, or a blank
711 732 line if there is no subsequent item.
712 733 """
713 734 if self._history_index < len(self._history):
714 735 self._history_index += 1
715 736 if self._history_index < len(self._history):
716 737 self.input_buffer = self._history[self._history_index]
717 738 else:
718 739 self.input_buffer = ''
General Comments 0
You need to be logged in to leave comments. Login now