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