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