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