##// END OF EJS Templates
Fixed bug in ConsoleWidget where text could be dragged and dropped.
epatters -
Show More
@@ -1,1323 +1,1338 b''
1 1 # Standard library imports
2 2 import re
3 3 import sys
4 4 from textwrap import dedent
5 5
6 6 # System library imports
7 7 from PyQt4 import QtCore, QtGui
8 8
9 9 # Local imports
10 10 from ansi_code_processor import QtAnsiCodeProcessor
11 11 from completion_widget import CompletionWidget
12 12
13 13
14 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
15 """ A QPlainTextEdit suitable for use with ConsoleWidget.
16 """
17 # Prevents text from being moved by drag and drop. Note that is not, for
18 # some reason, sufficient to catch drag events in the ConsoleWidget's
19 # event filter.
20 def dragEnterEvent(self, event): pass
21 def dragLeaveEvent(self, event): pass
22 def dragMoveEvent(self, event): pass
23 def dropEvent(self, event): pass
24
25 class ConsoleTextEdit(QtGui.QTextEdit):
26 """ A QTextEdit suitable for use with ConsoleWidget.
27 """
28 # See above.
29 def dragEnterEvent(self, event): pass
30 def dragLeaveEvent(self, event): pass
31 def dragMoveEvent(self, event): pass
32 def dropEvent(self, event): pass
33
34
14 35 class ConsoleWidget(QtGui.QWidget):
15 36 """ An abstract base class for console-type widgets. This class has
16 37 functionality for:
17 38
18 39 * Maintaining a prompt and editing region
19 40 * Providing the traditional Unix-style console keyboard shortcuts
20 41 * Performing tab completion
21 42 * Paging text
22 43 * Handling ANSI escape codes
23 44
24 45 ConsoleWidget also provides a number of utility methods that will be
25 46 convenient to implementors of a console-style widget.
26 47 """
27 48
28 49 # Whether to process ANSI escape codes.
29 50 ansi_codes = True
30 51
31 52 # The maximum number of lines of text before truncation.
32 53 buffer_size = 500
33 54
34 55 # Whether to use a list widget or plain text output for tab completion.
35 56 gui_completion = True
36 57
37 58 # Whether to override ShortcutEvents for the keybindings defined by this
38 59 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
39 60 # priority (when it has focus) over, e.g., window-level menu shortcuts.
40 61 override_shortcuts = False
41 62
42 63 # Signals that indicate ConsoleWidget state.
43 64 copy_available = QtCore.pyqtSignal(bool)
44 65 redo_available = QtCore.pyqtSignal(bool)
45 66 undo_available = QtCore.pyqtSignal(bool)
46 67
47 68 # Signal emitted when paging is needed and the paging style has been
48 69 # specified as 'custom'.
49 70 custom_page_requested = QtCore.pyqtSignal(object)
50 71
51 72 # Protected class variables.
52 73 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
53 74 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
54 75 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
55 76 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
56 77 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
57 78 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
58 79 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
59 80 _shortcuts = set(_ctrl_down_remap.keys() +
60 81 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
61 82
62 83 #---------------------------------------------------------------------------
63 84 # 'QObject' interface
64 85 #---------------------------------------------------------------------------
65 86
66 87 def __init__(self, kind='plain', paging='inside', parent=None):
67 88 """ Create a ConsoleWidget.
68 89
69 90 Parameters
70 91 ----------
71 92 kind : str, optional [default 'plain']
72 93 The type of underlying text widget to use. Valid values are 'plain',
73 94 which specifies a QPlainTextEdit, and 'rich', which specifies a
74 95 QTextEdit.
75 96
76 97 paging : str, optional [default 'inside']
77 98 The type of paging to use. Valid values are:
78 99 'inside' : The widget pages like a traditional terminal pager.
79 100 'hsplit' : When paging is requested, the widget is split
80 101 horizontally. The top pane contains the console,
81 102 and the bottom pane contains the paged text.
82 103 'vsplit' : Similar to 'hsplit', except that a vertical splitter
83 104 used.
84 105 'custom' : No action is taken by the widget beyond emitting a
85 106 'custom_page_requested(str)' signal.
86 107 'none' : The text is written directly to the console.
87 108
88 109 parent : QWidget, optional [default None]
89 110 The parent for this widget.
90 111 """
91 112 super(ConsoleWidget, self).__init__(parent)
92 113
93 114 # Create the layout and underlying text widget.
94 115 layout = QtGui.QStackedLayout(self)
95 116 layout.setMargin(0)
96 117 self._control = self._create_control(kind)
97 118 self._page_control = None
98 119 self._splitter = None
99 120 if paging in ('hsplit', 'vsplit'):
100 121 self._splitter = QtGui.QSplitter()
101 122 if paging == 'hsplit':
102 123 self._splitter.setOrientation(QtCore.Qt.Horizontal)
103 124 else:
104 125 self._splitter.setOrientation(QtCore.Qt.Vertical)
105 126 self._splitter.addWidget(self._control)
106 127 layout.addWidget(self._splitter)
107 128 else:
108 129 layout.addWidget(self._control)
109 130
110 131 # Create the paging widget, if necessary.
111 132 self._page_style = paging
112 133 if paging in ('inside', 'hsplit', 'vsplit'):
113 134 self._page_control = self._create_page_control()
114 135 if self._splitter:
115 136 self._page_control.hide()
116 137 self._splitter.addWidget(self._page_control)
117 138 else:
118 139 layout.addWidget(self._page_control)
119 140 elif paging not in ('custom', 'none'):
120 141 raise ValueError('Paging style %s unknown.' % repr(paging))
121 142
122 143 # Initialize protected variables. Some variables contain useful state
123 144 # information for subclasses; they should be considered read-only.
124 145 self._ansi_processor = QtAnsiCodeProcessor()
125 146 self._completion_widget = CompletionWidget(self._control)
126 147 self._continuation_prompt = '> '
127 148 self._continuation_prompt_html = None
128 149 self._executing = False
129 150 self._prompt = ''
130 151 self._prompt_html = None
131 152 self._prompt_pos = 0
132 153 self._reading = False
133 154 self._reading_callback = None
134 155 self._tab_width = 8
135 156
136 157 # Set a monospaced font.
137 158 self.reset_font()
138 159
139 160 def eventFilter(self, obj, event):
140 161 """ Reimplemented to ensure a console-like behavior in the underlying
141 162 text widget.
142 163 """
143 164 # Re-map keys for all filtered widgets.
144 165 etype = event.type()
145 166 if etype == QtCore.QEvent.KeyPress and \
146 167 self._control_key_down(event.modifiers()) and \
147 168 event.key() in self._ctrl_down_remap:
148 169 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
149 170 self._ctrl_down_remap[event.key()],
150 171 QtCore.Qt.NoModifier)
151 172 QtGui.qApp.sendEvent(obj, new_event)
152 173 return True
153 174
154 175 # Override shortucts for all filtered widgets. Note that on Mac OS it is
155 176 # always unnecessary to override shortcuts, hence the check below (users
156 177 # should just use the Control key instead of the Command key).
157 178 elif etype == QtCore.QEvent.ShortcutOverride and \
158 179 sys.platform != 'darwin' and \
159 180 self._control_key_down(event.modifiers()) and \
160 181 event.key() in self._shortcuts:
161 182 event.accept()
162 183 return False
163 184
164 elif obj == self._control:
165 # Disable moving text by drag and drop.
166 if etype == QtCore.QEvent.DragMove:
167 return True
168
169 elif etype == QtCore.QEvent.KeyPress:
185 elif etype == QtCore.QEvent.KeyPress:
186 if obj == self._control:
170 187 return self._event_filter_console_keypress(event)
171
172 elif obj == self._page_control:
173 if etype == QtCore.QEvent.KeyPress:
188 elif obj == self._page_control:
174 189 return self._event_filter_page_keypress(event)
175 190
176 191 return super(ConsoleWidget, self).eventFilter(obj, event)
177 192
178 193 #---------------------------------------------------------------------------
179 194 # 'QWidget' interface
180 195 #---------------------------------------------------------------------------
181 196
182 197 def sizeHint(self):
183 198 """ Reimplemented to suggest a size that is 80 characters wide and
184 199 25 lines high.
185 200 """
186 201 style = self.style()
187 202 opt = QtGui.QStyleOptionHeader()
188 203 font_metrics = QtGui.QFontMetrics(self.font)
189 204 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
190 205
191 206 width = font_metrics.width(' ') * 80
192 207 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
193 208 if self._page_style == 'hsplit':
194 209 width = width * 2 + splitwidth
195 210
196 211 height = font_metrics.height() * 25
197 212 if self._page_style == 'vsplit':
198 213 height = height * 2 + splitwidth
199 214
200 215 return QtCore.QSize(width, height)
201 216
202 217 #---------------------------------------------------------------------------
203 218 # 'ConsoleWidget' public interface
204 219 #---------------------------------------------------------------------------
205 220
206 221 def can_paste(self):
207 222 """ Returns whether text can be pasted from the clipboard.
208 223 """
209 224 # Accept only text that can be ASCII encoded.
210 225 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
211 226 text = QtGui.QApplication.clipboard().text()
212 227 if not text.isEmpty():
213 228 try:
214 229 str(text)
215 230 return True
216 231 except UnicodeEncodeError:
217 232 pass
218 233 return False
219 234
220 235 def clear(self, keep_input=True):
221 236 """ Clear the console, then write a new prompt. If 'keep_input' is set,
222 237 restores the old input buffer when the new prompt is written.
223 238 """
224 239 if keep_input:
225 240 input_buffer = self.input_buffer
226 241 self._control.clear()
227 242 self._show_prompt()
228 243 if keep_input:
229 244 self.input_buffer = input_buffer
230 245
231 246 def copy(self):
232 247 """ Copy the current selected text to the clipboard.
233 248 """
234 249 self._control.copy()
235 250
236 251 def execute(self, source=None, hidden=False, interactive=False):
237 252 """ Executes source or the input buffer, possibly prompting for more
238 253 input.
239 254
240 255 Parameters:
241 256 -----------
242 257 source : str, optional
243 258
244 259 The source to execute. If not specified, the input buffer will be
245 260 used. If specified and 'hidden' is False, the input buffer will be
246 261 replaced with the source before execution.
247 262
248 263 hidden : bool, optional (default False)
249 264
250 265 If set, no output will be shown and the prompt will not be modified.
251 266 In other words, it will be completely invisible to the user that
252 267 an execution has occurred.
253 268
254 269 interactive : bool, optional (default False)
255 270
256 271 Whether the console is to treat the source as having been manually
257 272 entered by the user. The effect of this parameter depends on the
258 273 subclass implementation.
259 274
260 275 Raises:
261 276 -------
262 277 RuntimeError
263 278 If incomplete input is given and 'hidden' is True. In this case,
264 279 it is not possible to prompt for more input.
265 280
266 281 Returns:
267 282 --------
268 283 A boolean indicating whether the source was executed.
269 284 """
270 285 if not hidden:
271 286 # Do everything here inside an edit block so continuation prompts
272 287 # are removed seamlessly via undo/redo.
273 288 cursor = self._control.textCursor()
274 289 cursor.beginEditBlock()
275 290
276 291 if source is not None:
277 292 self.input_buffer = source
278 293
279 294 self._append_plain_text('\n')
280 295 self._executing_input_buffer = self.input_buffer
281 296 self._executing = True
282 297 self._prompt_finished()
283 298
284 299 real_source = self.input_buffer if source is None else source
285 300 complete = self._is_complete(real_source, interactive)
286 301 if complete:
287 302 if not hidden:
288 303 # The maximum block count is only in effect during execution.
289 304 # This ensures that _prompt_pos does not become invalid due to
290 305 # text truncation.
291 306 self._control.document().setMaximumBlockCount(self.buffer_size)
292 307
293 308 # Setting a positive maximum block count will automatically
294 309 # disable the undo/redo history, but just to be safe:
295 310 self._control.setUndoRedoEnabled(False)
296 311
297 312 self._execute(real_source, hidden)
298 313 elif hidden:
299 314 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
300 315 else:
301 316 self._show_continuation_prompt()
302 317
303 318 if not hidden:
304 319 cursor.endEditBlock()
305 320
306 321 return complete
307 322
308 323 def _get_input_buffer(self):
309 324 """ The text that the user has entered entered at the current prompt.
310 325 """
311 326 # If we're executing, the input buffer may not even exist anymore due to
312 327 # the limit imposed by 'buffer_size'. Therefore, we store it.
313 328 if self._executing:
314 329 return self._executing_input_buffer
315 330
316 331 cursor = self._get_end_cursor()
317 332 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
318 333 input_buffer = str(cursor.selection().toPlainText())
319 334
320 335 # Strip out continuation prompts.
321 336 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
322 337
323 338 def _set_input_buffer(self, string):
324 339 """ Replaces the text in the input buffer with 'string'.
325 340 """
326 341 # For now, it is an error to modify the input buffer during execution.
327 342 if self._executing:
328 343 raise RuntimeError("Cannot change input buffer during execution.")
329 344
330 345 # Remove old text.
331 346 cursor = self._get_end_cursor()
332 347 cursor.beginEditBlock()
333 348 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
334 349 cursor.removeSelectedText()
335 350
336 351 # Insert new text with continuation prompts.
337 352 lines = string.splitlines(True)
338 353 if lines:
339 354 self._append_plain_text(lines[0])
340 355 for i in xrange(1, len(lines)):
341 356 if self._continuation_prompt_html is None:
342 357 self._append_plain_text(self._continuation_prompt)
343 358 else:
344 359 self._append_html(self._continuation_prompt_html)
345 360 self._append_plain_text(lines[i])
346 361 cursor.endEditBlock()
347 362 self._control.moveCursor(QtGui.QTextCursor.End)
348 363
349 364 input_buffer = property(_get_input_buffer, _set_input_buffer)
350 365
351 366 def _get_font(self):
352 367 """ The base font being used by the ConsoleWidget.
353 368 """
354 369 return self._control.document().defaultFont()
355 370
356 371 def _set_font(self, font):
357 372 """ Sets the base font for the ConsoleWidget to the specified QFont.
358 373 """
359 374 font_metrics = QtGui.QFontMetrics(font)
360 375 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
361 376
362 377 self._completion_widget.setFont(font)
363 378 self._control.document().setDefaultFont(font)
364 379 if self._page_control:
365 380 self._page_control.document().setDefaultFont(font)
366 381
367 382 font = property(_get_font, _set_font)
368 383
369 384 def paste(self):
370 385 """ Paste the contents of the clipboard into the input region.
371 386 """
372 387 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
373 388 try:
374 389 text = str(QtGui.QApplication.clipboard().text())
375 390 except UnicodeEncodeError:
376 391 pass
377 392 else:
378 393 self._insert_plain_text_into_buffer(dedent(text))
379 394
380 395 def print_(self, printer):
381 396 """ Print the contents of the ConsoleWidget to the specified QPrinter.
382 397 """
383 398 self._control.print_(printer)
384 399
385 400 def redo(self):
386 401 """ Redo the last operation. If there is no operation to redo, nothing
387 402 happens.
388 403 """
389 404 self._control.redo()
390 405
391 406 def reset_font(self):
392 407 """ Sets the font to the default fixed-width font for this platform.
393 408 """
394 409 # FIXME: font family and size should be configurable by the user.
395 410
396 411 if sys.platform == 'win32':
397 412 # Fixme: we should test whether Consolas is available and use it
398 413 # first if it is. Consolas ships by default from Vista onwards,
399 414 # it's *vastly* more readable and prettier than Courier, and is
400 415 # often installed even on XP systems. So we should first check for
401 416 # it, and only fallback to Courier if absolutely necessary.
402 417 name = 'Courier'
403 418 elif sys.platform == 'darwin':
404 419 name = 'Monaco'
405 420 else:
406 421 name = 'Monospace'
407 422 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
408 423 font.setStyleHint(QtGui.QFont.TypeWriter)
409 424 self._set_font(font)
410 425
411 426 def select_all(self):
412 427 """ Selects all the text in the buffer.
413 428 """
414 429 self._control.selectAll()
415 430
416 431 def _get_tab_width(self):
417 432 """ The width (in terms of space characters) for tab characters.
418 433 """
419 434 return self._tab_width
420 435
421 436 def _set_tab_width(self, tab_width):
422 437 """ Sets the width (in terms of space characters) for tab characters.
423 438 """
424 439 font_metrics = QtGui.QFontMetrics(self.font)
425 440 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
426 441
427 442 self._tab_width = tab_width
428 443
429 444 tab_width = property(_get_tab_width, _set_tab_width)
430 445
431 446 def undo(self):
432 447 """ Undo the last operation. If there is no operation to undo, nothing
433 448 happens.
434 449 """
435 450 self._control.undo()
436 451
437 452 #---------------------------------------------------------------------------
438 453 # 'ConsoleWidget' abstract interface
439 454 #---------------------------------------------------------------------------
440 455
441 456 def _is_complete(self, source, interactive):
442 457 """ Returns whether 'source' can be executed. When triggered by an
443 458 Enter/Return key press, 'interactive' is True; otherwise, it is
444 459 False.
445 460 """
446 461 raise NotImplementedError
447 462
448 463 def _execute(self, source, hidden):
449 464 """ Execute 'source'. If 'hidden', do not show any output.
450 465 """
451 466 raise NotImplementedError
452 467
453 468 def _prompt_started_hook(self):
454 469 """ Called immediately after a new prompt is displayed.
455 470 """
456 471 pass
457 472
458 473 def _prompt_finished_hook(self):
459 474 """ Called immediately after a prompt is finished, i.e. when some input
460 475 will be processed and a new prompt displayed.
461 476 """
462 477 pass
463 478
464 479 def _up_pressed(self):
465 480 """ Called when the up key is pressed. Returns whether to continue
466 481 processing the event.
467 482 """
468 483 return True
469 484
470 485 def _down_pressed(self):
471 486 """ Called when the down key is pressed. Returns whether to continue
472 487 processing the event.
473 488 """
474 489 return True
475 490
476 491 def _tab_pressed(self):
477 492 """ Called when the tab key is pressed. Returns whether to continue
478 493 processing the event.
479 494 """
480 495 return False
481 496
482 497 #--------------------------------------------------------------------------
483 498 # 'ConsoleWidget' protected interface
484 499 #--------------------------------------------------------------------------
485 500
486 501 def _append_html(self, html):
487 502 """ Appends html at the end of the console buffer.
488 503 """
489 504 cursor = self._get_end_cursor()
490 505 self._insert_html(cursor, html)
491 506
492 507 def _append_html_fetching_plain_text(self, html):
493 508 """ Appends 'html', then returns the plain text version of it.
494 509 """
495 510 cursor = self._get_end_cursor()
496 511 return self._insert_html_fetching_plain_text(cursor, html)
497 512
498 513 def _append_plain_text(self, text):
499 514 """ Appends plain text at the end of the console buffer, processing
500 515 ANSI codes if enabled.
501 516 """
502 517 cursor = self._get_end_cursor()
503 518 self._insert_plain_text(cursor, text)
504 519
505 520 def _append_plain_text_keeping_prompt(self, text):
506 521 """ Writes 'text' after the current prompt, then restores the old prompt
507 522 with its old input buffer.
508 523 """
509 524 input_buffer = self.input_buffer
510 525 self._append_plain_text('\n')
511 526 self._prompt_finished()
512 527
513 528 self._append_plain_text(text)
514 529 self._show_prompt()
515 530 self.input_buffer = input_buffer
516 531
517 532 def _complete_with_items(self, cursor, items):
518 533 """ Performs completion with 'items' at the specified cursor location.
519 534 """
520 535 if len(items) == 1:
521 536 cursor.setPosition(self._control.textCursor().position(),
522 537 QtGui.QTextCursor.KeepAnchor)
523 538 cursor.insertText(items[0])
524 539 elif len(items) > 1:
525 540 if self.gui_completion:
526 541 self._completion_widget.show_items(cursor, items)
527 542 else:
528 543 text = self._format_as_columns(items)
529 544 self._append_plain_text_keeping_prompt(text)
530 545
531 546 def _control_key_down(self, modifiers):
532 547 """ Given a KeyboardModifiers flags object, return whether the Control
533 548 key is down (on Mac OS, treat the Command key as a synonym for
534 549 Control).
535 550 """
536 551 down = bool(modifiers & QtCore.Qt.ControlModifier)
537 552
538 553 # Note: on Mac OS, ControlModifier corresponds to the Command key while
539 554 # MetaModifier corresponds to the Control key.
540 555 if sys.platform == 'darwin':
541 556 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
542 557
543 558 return down
544 559
545 560 def _create_control(self, kind):
546 561 """ Creates and connects the underlying text widget.
547 562 """
548 563 if kind == 'plain':
549 control = QtGui.QPlainTextEdit()
564 control = ConsolePlainTextEdit()
550 565 elif kind == 'rich':
551 control = QtGui.QTextEdit()
566 control = ConsoleTextEdit()
552 567 control.setAcceptRichText(False)
553 568 else:
554 569 raise ValueError("Kind %s unknown." % repr(kind))
555 570 control.installEventFilter(self)
556 571 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
557 572 control.customContextMenuRequested.connect(self._show_context_menu)
558 573 control.copyAvailable.connect(self.copy_available)
559 574 control.redoAvailable.connect(self.redo_available)
560 575 control.undoAvailable.connect(self.undo_available)
561 576 control.setReadOnly(True)
562 577 control.setUndoRedoEnabled(False)
563 578 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
564 579 return control
565 580
566 581 def _create_page_control(self):
567 582 """ Creates and connects the underlying paging widget.
568 583 """
569 control = QtGui.QPlainTextEdit()
584 control = ConsolePlainTextEdit()
570 585 control.installEventFilter(self)
571 586 control.setReadOnly(True)
572 587 control.setUndoRedoEnabled(False)
573 588 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
574 589 return control
575 590
576 591 def _event_filter_console_keypress(self, event):
577 592 """ Filter key events for the underlying text widget to create a
578 593 console-like interface.
579 594 """
580 595 intercepted = False
581 596 cursor = self._control.textCursor()
582 597 position = cursor.position()
583 598 key = event.key()
584 599 ctrl_down = self._control_key_down(event.modifiers())
585 600 alt_down = event.modifiers() & QtCore.Qt.AltModifier
586 601 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
587 602
588 603 if event.matches(QtGui.QKeySequence.Paste):
589 604 # Call our paste instead of the underlying text widget's.
590 605 self.paste()
591 606 intercepted = True
592 607
593 608 elif ctrl_down:
594 609 if key == QtCore.Qt.Key_K:
595 610 if self._in_buffer(position):
596 611 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
597 612 QtGui.QTextCursor.KeepAnchor)
598 613 cursor.removeSelectedText()
599 614 intercepted = True
600 615
601 616 elif key == QtCore.Qt.Key_X:
602 617 intercepted = True
603 618
604 619 elif key == QtCore.Qt.Key_Y:
605 620 self.paste()
606 621 intercepted = True
607 622
608 623 elif alt_down:
609 624 if key == QtCore.Qt.Key_B:
610 625 self._set_cursor(self._get_word_start_cursor(position))
611 626 intercepted = True
612 627
613 628 elif key == QtCore.Qt.Key_F:
614 629 self._set_cursor(self._get_word_end_cursor(position))
615 630 intercepted = True
616 631
617 632 elif key == QtCore.Qt.Key_Backspace:
618 633 cursor = self._get_word_start_cursor(position)
619 634 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
620 635 cursor.removeSelectedText()
621 636 intercepted = True
622 637
623 638 elif key == QtCore.Qt.Key_D:
624 639 cursor = self._get_word_end_cursor(position)
625 640 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
626 641 cursor.removeSelectedText()
627 642 intercepted = True
628 643
629 644 else:
630 645 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
631 646 if self._reading:
632 647 self._append_plain_text('\n')
633 648 self._reading = False
634 649 if self._reading_callback:
635 650 self._reading_callback()
636 651 elif not self._executing:
637 652 self.execute(interactive=True)
638 653 intercepted = True
639 654
640 655 elif key == QtCore.Qt.Key_Up:
641 656 if self._reading or not self._up_pressed():
642 657 intercepted = True
643 658 else:
644 659 prompt_line = self._get_prompt_cursor().blockNumber()
645 660 intercepted = cursor.blockNumber() <= prompt_line
646 661
647 662 elif key == QtCore.Qt.Key_Down:
648 663 if self._reading or not self._down_pressed():
649 664 intercepted = True
650 665 else:
651 666 end_line = self._get_end_cursor().blockNumber()
652 667 intercepted = cursor.blockNumber() == end_line
653 668
654 669 elif key == QtCore.Qt.Key_Tab:
655 670 if not self._reading:
656 671 intercepted = not self._tab_pressed()
657 672
658 673 elif key == QtCore.Qt.Key_Left:
659 674 intercepted = not self._in_buffer(position - 1)
660 675
661 676 elif key == QtCore.Qt.Key_Home:
662 677 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
663 678 start_line = cursor.blockNumber()
664 679 if start_line == self._get_prompt_cursor().blockNumber():
665 680 start_pos = self._prompt_pos
666 681 else:
667 682 start_pos = cursor.position()
668 683 start_pos += len(self._continuation_prompt)
669 684 if shift_down and self._in_buffer(position):
670 685 self._set_selection(position, start_pos)
671 686 else:
672 687 self._set_position(start_pos)
673 688 intercepted = True
674 689
675 690 elif key == QtCore.Qt.Key_Backspace:
676 691
677 692 # Line deletion (remove continuation prompt)
678 693 len_prompt = len(self._continuation_prompt)
679 694 if not self._reading and \
680 695 cursor.columnNumber() == len_prompt and \
681 696 position != self._prompt_pos:
682 697 cursor.beginEditBlock()
683 698 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
684 699 QtGui.QTextCursor.KeepAnchor)
685 700 cursor.removeSelectedText()
686 701 cursor.deletePreviousChar()
687 702 cursor.endEditBlock()
688 703 intercepted = True
689 704
690 705 # Regular backwards deletion
691 706 else:
692 707 anchor = cursor.anchor()
693 708 if anchor == position:
694 709 intercepted = not self._in_buffer(position - 1)
695 710 else:
696 711 intercepted = not self._in_buffer(min(anchor, position))
697 712
698 713 elif key == QtCore.Qt.Key_Delete:
699 714
700 715 # Line deletion (remove continuation prompt)
701 716 if not self._reading and cursor.atBlockEnd() and not \
702 717 cursor.hasSelection():
703 718 cursor.movePosition(QtGui.QTextCursor.NextBlock,
704 719 QtGui.QTextCursor.KeepAnchor)
705 720 cursor.movePosition(QtGui.QTextCursor.Right,
706 721 QtGui.QTextCursor.KeepAnchor,
707 722 len(self._continuation_prompt))
708 723 cursor.removeSelectedText()
709 724 intercepted = True
710 725
711 726 # Regular forwards deletion:
712 727 else:
713 728 anchor = cursor.anchor()
714 729 intercepted = (not self._in_buffer(anchor) or
715 730 not self._in_buffer(position))
716 731
717 732 # Don't move the cursor if control is down to allow copy-paste using
718 733 # the keyboard in any part of the buffer.
719 734 if not ctrl_down:
720 735 self._keep_cursor_in_buffer()
721 736
722 737 return intercepted
723 738
724 739 def _event_filter_page_keypress(self, event):
725 740 """ Filter key events for the paging widget to create console-like
726 741 interface.
727 742 """
728 743 key = event.key()
729 744
730 745 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
731 746 if self._splitter:
732 747 self._page_control.hide()
733 748 else:
734 749 self.layout().setCurrentWidget(self._control)
735 750 return True
736 751
737 752 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
738 753 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
739 754 QtCore.Qt.Key_Down,
740 755 QtCore.Qt.NoModifier)
741 756 QtGui.qApp.sendEvent(self._page_control, new_event)
742 757 return True
743 758
744 759 return False
745 760
746 761 def _format_as_columns(self, items, separator=' '):
747 762 """ Transform a list of strings into a single string with columns.
748 763
749 764 Parameters
750 765 ----------
751 766 items : sequence of strings
752 767 The strings to process.
753 768
754 769 separator : str, optional [default is two spaces]
755 770 The string that separates columns.
756 771
757 772 Returns
758 773 -------
759 774 The formatted string.
760 775 """
761 776 # Note: this code is adapted from columnize 0.3.2.
762 777 # See http://code.google.com/p/pycolumnize/
763 778
764 779 width = self._control.viewport().width()
765 780 char_width = QtGui.QFontMetrics(self.font).width(' ')
766 781 displaywidth = max(5, width / char_width)
767 782
768 783 # Some degenerate cases.
769 784 size = len(items)
770 785 if size == 0:
771 786 return '\n'
772 787 elif size == 1:
773 788 return '%s\n' % str(items[0])
774 789
775 790 # Try every row count from 1 upwards
776 791 array_index = lambda nrows, row, col: nrows*col + row
777 792 for nrows in range(1, size):
778 793 ncols = (size + nrows - 1) // nrows
779 794 colwidths = []
780 795 totwidth = -len(separator)
781 796 for col in range(ncols):
782 797 # Get max column width for this column
783 798 colwidth = 0
784 799 for row in range(nrows):
785 800 i = array_index(nrows, row, col)
786 801 if i >= size: break
787 802 x = items[i]
788 803 colwidth = max(colwidth, len(x))
789 804 colwidths.append(colwidth)
790 805 totwidth += colwidth + len(separator)
791 806 if totwidth > displaywidth:
792 807 break
793 808 if totwidth <= displaywidth:
794 809 break
795 810
796 811 # The smallest number of rows computed and the max widths for each
797 812 # column has been obtained. Now we just have to format each of the rows.
798 813 string = ''
799 814 for row in range(nrows):
800 815 texts = []
801 816 for col in range(ncols):
802 817 i = row + nrows*col
803 818 if i >= size:
804 819 texts.append('')
805 820 else:
806 821 texts.append(items[i])
807 822 while texts and not texts[-1]:
808 823 del texts[-1]
809 824 for col in range(len(texts)):
810 825 texts[col] = texts[col].ljust(colwidths[col])
811 826 string += '%s\n' % str(separator.join(texts))
812 827 return string
813 828
814 829 def _get_block_plain_text(self, block):
815 830 """ Given a QTextBlock, return its unformatted text.
816 831 """
817 832 cursor = QtGui.QTextCursor(block)
818 833 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
819 834 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
820 835 QtGui.QTextCursor.KeepAnchor)
821 836 return str(cursor.selection().toPlainText())
822 837
823 838 def _get_cursor(self):
824 839 """ Convenience method that returns a cursor for the current position.
825 840 """
826 841 return self._control.textCursor()
827 842
828 843 def _get_end_cursor(self):
829 844 """ Convenience method that returns a cursor for the last character.
830 845 """
831 846 cursor = self._control.textCursor()
832 847 cursor.movePosition(QtGui.QTextCursor.End)
833 848 return cursor
834 849
835 850 def _get_input_buffer_cursor_column(self):
836 851 """ Returns the column of the cursor in the input buffer, excluding the
837 852 contribution by the prompt, or -1 if there is no such column.
838 853 """
839 854 prompt = self._get_input_buffer_cursor_prompt()
840 855 if prompt is None:
841 856 return -1
842 857 else:
843 858 cursor = self._control.textCursor()
844 859 return cursor.columnNumber() - len(prompt)
845 860
846 861 def _get_input_buffer_cursor_line(self):
847 862 """ Returns line of the input buffer that contains the cursor, or None
848 863 if there is no such line.
849 864 """
850 865 prompt = self._get_input_buffer_cursor_prompt()
851 866 if prompt is None:
852 867 return None
853 868 else:
854 869 cursor = self._control.textCursor()
855 870 text = self._get_block_plain_text(cursor.block())
856 871 return text[len(prompt):]
857 872
858 873 def _get_input_buffer_cursor_prompt(self):
859 874 """ Returns the (plain text) prompt for line of the input buffer that
860 875 contains the cursor, or None if there is no such line.
861 876 """
862 877 if self._executing:
863 878 return None
864 879 cursor = self._control.textCursor()
865 880 if cursor.position() >= self._prompt_pos:
866 881 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
867 882 return self._prompt
868 883 else:
869 884 return self._continuation_prompt
870 885 else:
871 886 return None
872 887
873 888 def _get_prompt_cursor(self):
874 889 """ Convenience method that returns a cursor for the prompt position.
875 890 """
876 891 cursor = self._control.textCursor()
877 892 cursor.setPosition(self._prompt_pos)
878 893 return cursor
879 894
880 895 def _get_selection_cursor(self, start, end):
881 896 """ Convenience method that returns a cursor with text selected between
882 897 the positions 'start' and 'end'.
883 898 """
884 899 cursor = self._control.textCursor()
885 900 cursor.setPosition(start)
886 901 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
887 902 return cursor
888 903
889 904 def _get_word_start_cursor(self, position):
890 905 """ Find the start of the word to the left the given position. If a
891 906 sequence of non-word characters precedes the first word, skip over
892 907 them. (This emulates the behavior of bash, emacs, etc.)
893 908 """
894 909 document = self._control.document()
895 910 position -= 1
896 911 while position >= self._prompt_pos and \
897 912 not document.characterAt(position).isLetterOrNumber():
898 913 position -= 1
899 914 while position >= self._prompt_pos and \
900 915 document.characterAt(position).isLetterOrNumber():
901 916 position -= 1
902 917 cursor = self._control.textCursor()
903 918 cursor.setPosition(position + 1)
904 919 return cursor
905 920
906 921 def _get_word_end_cursor(self, position):
907 922 """ Find the end of the word to the right the given position. If a
908 923 sequence of non-word characters precedes the first word, skip over
909 924 them. (This emulates the behavior of bash, emacs, etc.)
910 925 """
911 926 document = self._control.document()
912 927 end = self._get_end_cursor().position()
913 928 while position < end and \
914 929 not document.characterAt(position).isLetterOrNumber():
915 930 position += 1
916 931 while position < end and \
917 932 document.characterAt(position).isLetterOrNumber():
918 933 position += 1
919 934 cursor = self._control.textCursor()
920 935 cursor.setPosition(position)
921 936 return cursor
922 937
923 938 def _insert_html(self, cursor, html):
924 939 """ Inserts HTML using the specified cursor in such a way that future
925 940 formatting is unaffected.
926 941 """
927 942 cursor.beginEditBlock()
928 943 cursor.insertHtml(html)
929 944
930 945 # After inserting HTML, the text document "remembers" it's in "html
931 946 # mode", which means that subsequent calls adding plain text will result
932 947 # in unwanted formatting, lost tab characters, etc. The following code
933 948 # hacks around this behavior, which I consider to be a bug in Qt, by
934 949 # (crudely) resetting the document's style state.
935 950 cursor.movePosition(QtGui.QTextCursor.Left,
936 951 QtGui.QTextCursor.KeepAnchor)
937 952 if cursor.selection().toPlainText() == ' ':
938 953 cursor.removeSelectedText()
939 954 else:
940 955 cursor.movePosition(QtGui.QTextCursor.Right)
941 956 cursor.insertText(' ', QtGui.QTextCharFormat())
942 957 cursor.endEditBlock()
943 958
944 959 def _insert_html_fetching_plain_text(self, cursor, html):
945 960 """ Inserts HTML using the specified cursor, then returns its plain text
946 961 version.
947 962 """
948 963 cursor.beginEditBlock()
949 964 cursor.removeSelectedText()
950 965
951 966 start = cursor.position()
952 967 self._insert_html(cursor, html)
953 968 end = cursor.position()
954 969 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
955 970 text = str(cursor.selection().toPlainText())
956 971
957 972 cursor.setPosition(end)
958 973 cursor.endEditBlock()
959 974 return text
960 975
961 976 def _insert_plain_text(self, cursor, text):
962 977 """ Inserts plain text using the specified cursor, processing ANSI codes
963 978 if enabled.
964 979 """
965 980 cursor.beginEditBlock()
966 981 if self.ansi_codes:
967 982 for substring in self._ansi_processor.split_string(text):
968 983 for action in self._ansi_processor.actions:
969 984 if action.kind == 'erase' and action.area == 'screen':
970 985 cursor.select(QtGui.QTextCursor.Document)
971 986 cursor.removeSelectedText()
972 987 format = self._ansi_processor.get_format()
973 988 cursor.insertText(substring, format)
974 989 else:
975 990 cursor.insertText(text)
976 991 cursor.endEditBlock()
977 992
978 993 def _insert_plain_text_into_buffer(self, text):
979 994 """ Inserts text into the input buffer at the current cursor position,
980 995 ensuring that continuation prompts are inserted as necessary.
981 996 """
982 997 lines = str(text).splitlines(True)
983 998 if lines:
984 999 self._keep_cursor_in_buffer()
985 1000 cursor = self._control.textCursor()
986 1001 cursor.beginEditBlock()
987 1002 cursor.insertText(lines[0])
988 1003 for line in lines[1:]:
989 1004 if self._continuation_prompt_html is None:
990 1005 cursor.insertText(self._continuation_prompt)
991 1006 else:
992 1007 self._continuation_prompt = \
993 1008 self._insert_html_fetching_plain_text(
994 1009 cursor, self._continuation_prompt_html)
995 1010 cursor.insertText(line)
996 1011 cursor.endEditBlock()
997 1012 self._control.setTextCursor(cursor)
998 1013
999 1014 def _in_buffer(self, position=None):
1000 1015 """ Returns whether the current cursor (or, if specified, a position) is
1001 1016 inside the editing region.
1002 1017 """
1003 1018 cursor = self._control.textCursor()
1004 1019 if position is None:
1005 1020 position = cursor.position()
1006 1021 else:
1007 1022 cursor.setPosition(position)
1008 1023 line = cursor.blockNumber()
1009 1024 prompt_line = self._get_prompt_cursor().blockNumber()
1010 1025 if line == prompt_line:
1011 1026 return position >= self._prompt_pos
1012 1027 elif line > prompt_line:
1013 1028 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1014 1029 prompt_pos = cursor.position() + len(self._continuation_prompt)
1015 1030 return position >= prompt_pos
1016 1031 return False
1017 1032
1018 1033 def _keep_cursor_in_buffer(self):
1019 1034 """ Ensures that the cursor is inside the editing region. Returns
1020 1035 whether the cursor was moved.
1021 1036 """
1022 1037 moved = not self._in_buffer()
1023 1038 if moved:
1024 1039 cursor = self._control.textCursor()
1025 1040 cursor.movePosition(QtGui.QTextCursor.End)
1026 1041 self._control.setTextCursor(cursor)
1027 1042 return moved
1028 1043
1029 1044 def _page(self, text):
1030 1045 """ Displays text using the pager if it exceeds the height of the
1031 1046 visible area.
1032 1047 """
1033 1048 if self._page_style == 'none':
1034 1049 self._append_plain_text(text)
1035 1050 else:
1036 1051 line_height = QtGui.QFontMetrics(self.font).height()
1037 1052 minlines = self._control.viewport().height() / line_height
1038 1053 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1039 1054 if self._page_style == 'custom':
1040 1055 self.custom_page_requested.emit(text)
1041 1056 else:
1042 1057 self._page_control.clear()
1043 1058 cursor = self._page_control.textCursor()
1044 1059 self._insert_plain_text(cursor, text)
1045 1060 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1046 1061
1047 1062 self._page_control.viewport().resize(self._control.size())
1048 1063 if self._splitter:
1049 1064 self._page_control.show()
1050 1065 self._page_control.setFocus()
1051 1066 else:
1052 1067 self.layout().setCurrentWidget(self._page_control)
1053 1068 else:
1054 1069 self._append_plain_text(text)
1055 1070
1056 1071 def _prompt_started(self):
1057 1072 """ Called immediately after a new prompt is displayed.
1058 1073 """
1059 1074 # Temporarily disable the maximum block count to permit undo/redo and
1060 1075 # to ensure that the prompt position does not change due to truncation.
1061 1076 # Because setting this property clears the undo/redo history, we only
1062 1077 # set it if we have to.
1063 1078 if self._control.document().maximumBlockCount() > 0:
1064 1079 self._control.document().setMaximumBlockCount(0)
1065 1080 self._control.setUndoRedoEnabled(True)
1066 1081
1067 1082 self._control.setReadOnly(False)
1068 1083 self._control.moveCursor(QtGui.QTextCursor.End)
1069 1084
1070 1085 self._executing = False
1071 1086 self._prompt_started_hook()
1072 1087
1073 1088 def _prompt_finished(self):
1074 1089 """ Called immediately after a prompt is finished, i.e. when some input
1075 1090 will be processed and a new prompt displayed.
1076 1091 """
1077 1092 self._control.setReadOnly(True)
1078 1093 self._prompt_finished_hook()
1079 1094
1080 1095 def _readline(self, prompt='', callback=None):
1081 1096 """ Reads one line of input from the user.
1082 1097
1083 1098 Parameters
1084 1099 ----------
1085 1100 prompt : str, optional
1086 1101 The prompt to print before reading the line.
1087 1102
1088 1103 callback : callable, optional
1089 1104 A callback to execute with the read line. If not specified, input is
1090 1105 read *synchronously* and this method does not return until it has
1091 1106 been read.
1092 1107
1093 1108 Returns
1094 1109 -------
1095 1110 If a callback is specified, returns nothing. Otherwise, returns the
1096 1111 input string with the trailing newline stripped.
1097 1112 """
1098 1113 if self._reading:
1099 1114 raise RuntimeError('Cannot read a line. Widget is already reading.')
1100 1115
1101 1116 if not callback and not self.isVisible():
1102 1117 # If the user cannot see the widget, this function cannot return.
1103 1118 raise RuntimeError('Cannot synchronously read a line if the widget'
1104 1119 'is not visible!')
1105 1120
1106 1121 self._reading = True
1107 1122 self._show_prompt(prompt, newline=False)
1108 1123
1109 1124 if callback is None:
1110 1125 self._reading_callback = None
1111 1126 while self._reading:
1112 1127 QtCore.QCoreApplication.processEvents()
1113 1128 return self.input_buffer.rstrip('\n')
1114 1129
1115 1130 else:
1116 1131 self._reading_callback = lambda: \
1117 1132 callback(self.input_buffer.rstrip('\n'))
1118 1133
1119 1134 def _set_continuation_prompt(self, prompt, html=False):
1120 1135 """ Sets the continuation prompt.
1121 1136
1122 1137 Parameters
1123 1138 ----------
1124 1139 prompt : str
1125 1140 The prompt to show when more input is needed.
1126 1141
1127 1142 html : bool, optional (default False)
1128 1143 If set, the prompt will be inserted as formatted HTML. Otherwise,
1129 1144 the prompt will be treated as plain text, though ANSI color codes
1130 1145 will be handled.
1131 1146 """
1132 1147 if html:
1133 1148 self._continuation_prompt_html = prompt
1134 1149 else:
1135 1150 self._continuation_prompt = prompt
1136 1151 self._continuation_prompt_html = None
1137 1152
1138 1153 def _set_cursor(self, cursor):
1139 1154 """ Convenience method to set the current cursor.
1140 1155 """
1141 1156 self._control.setTextCursor(cursor)
1142 1157
1143 1158 def _set_position(self, position):
1144 1159 """ Convenience method to set the position of the cursor.
1145 1160 """
1146 1161 cursor = self._control.textCursor()
1147 1162 cursor.setPosition(position)
1148 1163 self._control.setTextCursor(cursor)
1149 1164
1150 1165 def _set_selection(self, start, end):
1151 1166 """ Convenience method to set the current selected text.
1152 1167 """
1153 1168 self._control.setTextCursor(self._get_selection_cursor(start, end))
1154 1169
1155 1170 def _show_context_menu(self, pos):
1156 1171 """ Shows a context menu at the given QPoint (in widget coordinates).
1157 1172 """
1158 1173 menu = QtGui.QMenu()
1159 1174
1160 1175 copy_action = menu.addAction('Copy', self.copy)
1161 1176 copy_action.setEnabled(self._get_cursor().hasSelection())
1162 1177 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1163 1178
1164 1179 paste_action = menu.addAction('Paste', self.paste)
1165 1180 paste_action.setEnabled(self.can_paste())
1166 1181 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1167 1182
1168 1183 menu.addSeparator()
1169 1184 menu.addAction('Select All', self.select_all)
1170 1185
1171 1186 menu.exec_(self._control.mapToGlobal(pos))
1172 1187
1173 1188 def _show_prompt(self, prompt=None, html=False, newline=True):
1174 1189 """ Writes a new prompt at the end of the buffer.
1175 1190
1176 1191 Parameters
1177 1192 ----------
1178 1193 prompt : str, optional
1179 1194 The prompt to show. If not specified, the previous prompt is used.
1180 1195
1181 1196 html : bool, optional (default False)
1182 1197 Only relevant when a prompt is specified. If set, the prompt will
1183 1198 be inserted as formatted HTML. Otherwise, the prompt will be treated
1184 1199 as plain text, though ANSI color codes will be handled.
1185 1200
1186 1201 newline : bool, optional (default True)
1187 1202 If set, a new line will be written before showing the prompt if
1188 1203 there is not already a newline at the end of the buffer.
1189 1204 """
1190 1205 # Insert a preliminary newline, if necessary.
1191 1206 if newline:
1192 1207 cursor = self._get_end_cursor()
1193 1208 if cursor.position() > 0:
1194 1209 cursor.movePosition(QtGui.QTextCursor.Left,
1195 1210 QtGui.QTextCursor.KeepAnchor)
1196 1211 if str(cursor.selection().toPlainText()) != '\n':
1197 1212 self._append_plain_text('\n')
1198 1213
1199 1214 # Write the prompt.
1200 1215 if prompt is None:
1201 1216 if self._prompt_html is None:
1202 1217 self._append_plain_text(self._prompt)
1203 1218 else:
1204 1219 self._append_html(self._prompt_html)
1205 1220 else:
1206 1221 if html:
1207 1222 self._prompt = self._append_html_fetching_plain_text(prompt)
1208 1223 self._prompt_html = prompt
1209 1224 else:
1210 1225 self._append_plain_text(prompt)
1211 1226 self._prompt = prompt
1212 1227 self._prompt_html = None
1213 1228
1214 1229 self._prompt_pos = self._get_end_cursor().position()
1215 1230 self._prompt_started()
1216 1231
1217 1232 def _show_continuation_prompt(self):
1218 1233 """ Writes a new continuation prompt at the end of the buffer.
1219 1234 """
1220 1235 if self._continuation_prompt_html is None:
1221 1236 self._append_plain_text(self._continuation_prompt)
1222 1237 else:
1223 1238 self._continuation_prompt = self._append_html_fetching_plain_text(
1224 1239 self._continuation_prompt_html)
1225 1240
1226 1241 self._prompt_started()
1227 1242
1228 1243
1229 1244 class HistoryConsoleWidget(ConsoleWidget):
1230 1245 """ A ConsoleWidget that keeps a history of the commands that have been
1231 1246 executed.
1232 1247 """
1233 1248
1234 1249 #---------------------------------------------------------------------------
1235 1250 # 'object' interface
1236 1251 #---------------------------------------------------------------------------
1237 1252
1238 1253 def __init__(self, *args, **kw):
1239 1254 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1240 1255 self._history = []
1241 1256 self._history_index = 0
1242 1257
1243 1258 #---------------------------------------------------------------------------
1244 1259 # 'ConsoleWidget' public interface
1245 1260 #---------------------------------------------------------------------------
1246 1261
1247 1262 def execute(self, source=None, hidden=False, interactive=False):
1248 1263 """ Reimplemented to the store history.
1249 1264 """
1250 1265 if not hidden:
1251 1266 history = self.input_buffer if source is None else source
1252 1267
1253 1268 executed = super(HistoryConsoleWidget, self).execute(
1254 1269 source, hidden, interactive)
1255 1270
1256 1271 if executed and not hidden:
1257 1272 self._history.append(history.rstrip())
1258 1273 self._history_index = len(self._history)
1259 1274
1260 1275 return executed
1261 1276
1262 1277 #---------------------------------------------------------------------------
1263 1278 # 'ConsoleWidget' abstract interface
1264 1279 #---------------------------------------------------------------------------
1265 1280
1266 1281 def _up_pressed(self):
1267 1282 """ Called when the up key is pressed. Returns whether to continue
1268 1283 processing the event.
1269 1284 """
1270 1285 prompt_cursor = self._get_prompt_cursor()
1271 1286 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1272 1287 self.history_previous()
1273 1288
1274 1289 # Go to the first line of prompt for seemless history scrolling.
1275 1290 cursor = self._get_prompt_cursor()
1276 1291 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1277 1292 self._set_cursor(cursor)
1278 1293
1279 1294 return False
1280 1295 return True
1281 1296
1282 1297 def _down_pressed(self):
1283 1298 """ Called when the down key is pressed. Returns whether to continue
1284 1299 processing the event.
1285 1300 """
1286 1301 end_cursor = self._get_end_cursor()
1287 1302 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1288 1303 self.history_next()
1289 1304 return False
1290 1305 return True
1291 1306
1292 1307 #---------------------------------------------------------------------------
1293 1308 # 'HistoryConsoleWidget' public interface
1294 1309 #---------------------------------------------------------------------------
1295 1310
1296 1311 def history_previous(self):
1297 1312 """ If possible, set the input buffer to the previous item in the
1298 1313 history.
1299 1314 """
1300 1315 if self._history_index > 0:
1301 1316 self._history_index -= 1
1302 1317 self.input_buffer = self._history[self._history_index]
1303 1318
1304 1319 def history_next(self):
1305 1320 """ Set the input buffer to the next item in the history, or a blank
1306 1321 line if there is no subsequent item.
1307 1322 """
1308 1323 if self._history_index < len(self._history):
1309 1324 self._history_index += 1
1310 1325 if self._history_index < len(self._history):
1311 1326 self.input_buffer = self._history[self._history_index]
1312 1327 else:
1313 1328 self.input_buffer = ''
1314 1329
1315 1330 #---------------------------------------------------------------------------
1316 1331 # 'HistoryConsoleWidget' protected interface
1317 1332 #---------------------------------------------------------------------------
1318 1333
1319 1334 def _set_history(self, history):
1320 1335 """ Replace the current history with a sequence of history items.
1321 1336 """
1322 1337 self._history = list(history)
1323 1338 self._history_index = len(self._history)
General Comments 0
You need to be logged in to leave comments. Login now