##// END OF EJS Templates
Fixed minor bug in last checkin.
epatters -
Show More
@@ -1,1295 +1,1293 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._reading:
643 intercepted = False
644 elif not self._tab_pressed():
645 intercepted = True
646 else:
642 if self._tab_pressed():
647 643 intercepted = not self._in_buffer()
644 else:
645 intercepted = True
648 646
649 647 elif key == QtCore.Qt.Key_Left:
650 648 intercepted = not self._in_buffer(position - 1)
651 649
652 650 elif key == QtCore.Qt.Key_Home:
653 651 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
654 652 start_line = cursor.blockNumber()
655 653 if start_line == self._get_prompt_cursor().blockNumber():
656 654 start_pos = self._prompt_pos
657 655 else:
658 656 start_pos = cursor.position()
659 657 start_pos += len(self._continuation_prompt)
660 658 if shift_down and self._in_buffer(position):
661 659 self._set_selection(position, start_pos)
662 660 else:
663 661 self._set_position(start_pos)
664 662 intercepted = True
665 663
666 664 elif key == QtCore.Qt.Key_Backspace:
667 665
668 666 # Line deletion (remove continuation prompt)
669 667 len_prompt = len(self._continuation_prompt)
670 668 if not self._reading and \
671 669 cursor.columnNumber() == len_prompt and \
672 670 position != self._prompt_pos:
673 671 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
674 672 QtGui.QTextCursor.KeepAnchor)
675 673 cursor.removeSelectedText()
676 674 cursor.deletePreviousChar()
677 675 intercepted = True
678 676
679 677 # Regular backwards deletion
680 678 else:
681 679 anchor = cursor.anchor()
682 680 if anchor == position:
683 681 intercepted = not self._in_buffer(position - 1)
684 682 else:
685 683 intercepted = not self._in_buffer(min(anchor, position))
686 684
687 685 elif key == QtCore.Qt.Key_Delete:
688 686 anchor = cursor.anchor()
689 687 intercepted = not self._in_buffer(min(anchor, position))
690 688
691 689 # Don't move the cursor if control is down to allow copy-paste using
692 690 # the keyboard in any part of the buffer.
693 691 if not ctrl_down:
694 692 self._keep_cursor_in_buffer()
695 693
696 694 return intercepted
697 695
698 696 def _event_filter_page_keypress(self, event):
699 697 """ Filter key events for the paging widget to create console-like
700 698 interface.
701 699 """
702 700 key = event.key()
703 701
704 702 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
705 703 if self._splitter:
706 704 self._page_control.hide()
707 705 else:
708 706 self.layout().setCurrentWidget(self._control)
709 707 return True
710 708
711 709 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
712 710 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
713 711 QtCore.Qt.Key_Down,
714 712 QtCore.Qt.NoModifier)
715 713 QtGui.qApp.sendEvent(self._page_control, new_event)
716 714 return True
717 715
718 716 return False
719 717
720 718 def _format_as_columns(self, items, separator=' '):
721 719 """ Transform a list of strings into a single string with columns.
722 720
723 721 Parameters
724 722 ----------
725 723 items : sequence of strings
726 724 The strings to process.
727 725
728 726 separator : str, optional [default is two spaces]
729 727 The string that separates columns.
730 728
731 729 Returns
732 730 -------
733 731 The formatted string.
734 732 """
735 733 # Note: this code is adapted from columnize 0.3.2.
736 734 # See http://code.google.com/p/pycolumnize/
737 735
738 736 width = self._control.viewport().width()
739 737 char_width = QtGui.QFontMetrics(self.font).width(' ')
740 738 displaywidth = max(5, width / char_width)
741 739
742 740 # Some degenerate cases.
743 741 size = len(items)
744 742 if size == 0:
745 743 return '\n'
746 744 elif size == 1:
747 745 return '%s\n' % str(items[0])
748 746
749 747 # Try every row count from 1 upwards
750 748 array_index = lambda nrows, row, col: nrows*col + row
751 749 for nrows in range(1, size):
752 750 ncols = (size + nrows - 1) // nrows
753 751 colwidths = []
754 752 totwidth = -len(separator)
755 753 for col in range(ncols):
756 754 # Get max column width for this column
757 755 colwidth = 0
758 756 for row in range(nrows):
759 757 i = array_index(nrows, row, col)
760 758 if i >= size: break
761 759 x = items[i]
762 760 colwidth = max(colwidth, len(x))
763 761 colwidths.append(colwidth)
764 762 totwidth += colwidth + len(separator)
765 763 if totwidth > displaywidth:
766 764 break
767 765 if totwidth <= displaywidth:
768 766 break
769 767
770 768 # The smallest number of rows computed and the max widths for each
771 769 # column has been obtained. Now we just have to format each of the rows.
772 770 string = ''
773 771 for row in range(nrows):
774 772 texts = []
775 773 for col in range(ncols):
776 774 i = row + nrows*col
777 775 if i >= size:
778 776 texts.append('')
779 777 else:
780 778 texts.append(items[i])
781 779 while texts and not texts[-1]:
782 780 del texts[-1]
783 781 for col in range(len(texts)):
784 782 texts[col] = texts[col].ljust(colwidths[col])
785 783 string += '%s\n' % str(separator.join(texts))
786 784 return string
787 785
788 786 def _get_block_plain_text(self, block):
789 787 """ Given a QTextBlock, return its unformatted text.
790 788 """
791 789 cursor = QtGui.QTextCursor(block)
792 790 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
793 791 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
794 792 QtGui.QTextCursor.KeepAnchor)
795 793 return str(cursor.selection().toPlainText())
796 794
797 795 def _get_cursor(self):
798 796 """ Convenience method that returns a cursor for the current position.
799 797 """
800 798 return self._control.textCursor()
801 799
802 800 def _get_end_cursor(self):
803 801 """ Convenience method that returns a cursor for the last character.
804 802 """
805 803 cursor = self._control.textCursor()
806 804 cursor.movePosition(QtGui.QTextCursor.End)
807 805 return cursor
808 806
809 807 def _get_input_buffer_cursor_column(self):
810 808 """ Returns the column of the cursor in the input buffer, excluding the
811 809 contribution by the prompt, or -1 if there is no such column.
812 810 """
813 811 prompt = self._get_input_buffer_cursor_prompt()
814 812 if prompt is None:
815 813 return -1
816 814 else:
817 815 cursor = self._control.textCursor()
818 816 return cursor.columnNumber() - len(prompt)
819 817
820 818 def _get_input_buffer_cursor_line(self):
821 819 """ Returns line of the input buffer that contains the cursor, or None
822 820 if there is no such line.
823 821 """
824 822 prompt = self._get_input_buffer_cursor_prompt()
825 823 if prompt is None:
826 824 return None
827 825 else:
828 826 cursor = self._control.textCursor()
829 827 text = self._get_block_plain_text(cursor.block())
830 828 return text[len(prompt):]
831 829
832 830 def _get_input_buffer_cursor_prompt(self):
833 831 """ Returns the (plain text) prompt for line of the input buffer that
834 832 contains the cursor, or None if there is no such line.
835 833 """
836 834 if self._executing:
837 835 return None
838 836 cursor = self._control.textCursor()
839 837 if cursor.position() >= self._prompt_pos:
840 838 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
841 839 return self._prompt
842 840 else:
843 841 return self._continuation_prompt
844 842 else:
845 843 return None
846 844
847 845 def _get_prompt_cursor(self):
848 846 """ Convenience method that returns a cursor for the prompt position.
849 847 """
850 848 cursor = self._control.textCursor()
851 849 cursor.setPosition(self._prompt_pos)
852 850 return cursor
853 851
854 852 def _get_selection_cursor(self, start, end):
855 853 """ Convenience method that returns a cursor with text selected between
856 854 the positions 'start' and 'end'.
857 855 """
858 856 cursor = self._control.textCursor()
859 857 cursor.setPosition(start)
860 858 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
861 859 return cursor
862 860
863 861 def _get_word_start_cursor(self, position):
864 862 """ Find the start of the word to the left the given position. If a
865 863 sequence of non-word characters precedes the first word, skip over
866 864 them. (This emulates the behavior of bash, emacs, etc.)
867 865 """
868 866 document = self._control.document()
869 867 position -= 1
870 868 while position >= self._prompt_pos and \
871 869 not document.characterAt(position).isLetterOrNumber():
872 870 position -= 1
873 871 while position >= self._prompt_pos and \
874 872 document.characterAt(position).isLetterOrNumber():
875 873 position -= 1
876 874 cursor = self._control.textCursor()
877 875 cursor.setPosition(position + 1)
878 876 return cursor
879 877
880 878 def _get_word_end_cursor(self, position):
881 879 """ Find the end of the word to the right the given position. If a
882 880 sequence of non-word characters precedes the first word, skip over
883 881 them. (This emulates the behavior of bash, emacs, etc.)
884 882 """
885 883 document = self._control.document()
886 884 end = self._get_end_cursor().position()
887 885 while position < end and \
888 886 not document.characterAt(position).isLetterOrNumber():
889 887 position += 1
890 888 while position < end and \
891 889 document.characterAt(position).isLetterOrNumber():
892 890 position += 1
893 891 cursor = self._control.textCursor()
894 892 cursor.setPosition(position)
895 893 return cursor
896 894
897 895 def _insert_html(self, cursor, html):
898 896 """ Inserts HTML using the specified cursor in such a way that future
899 897 formatting is unaffected.
900 898 """
901 899 cursor.beginEditBlock()
902 900 cursor.insertHtml(html)
903 901
904 902 # After inserting HTML, the text document "remembers" it's in "html
905 903 # mode", which means that subsequent calls adding plain text will result
906 904 # in unwanted formatting, lost tab characters, etc. The following code
907 905 # hacks around this behavior, which I consider to be a bug in Qt, by
908 906 # (crudely) resetting the document's style state.
909 907 cursor.movePosition(QtGui.QTextCursor.Left,
910 908 QtGui.QTextCursor.KeepAnchor)
911 909 if cursor.selection().toPlainText() == ' ':
912 910 cursor.removeSelectedText()
913 911 else:
914 912 cursor.movePosition(QtGui.QTextCursor.Right)
915 913 cursor.insertText(' ', QtGui.QTextCharFormat())
916 914 cursor.endEditBlock()
917 915
918 916 def _insert_html_fetching_plain_text(self, cursor, html):
919 917 """ Inserts HTML using the specified cursor, then returns its plain text
920 918 version.
921 919 """
922 920 cursor.beginEditBlock()
923 921 cursor.removeSelectedText()
924 922
925 923 start = cursor.position()
926 924 self._insert_html(cursor, html)
927 925 end = cursor.position()
928 926 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
929 927 text = str(cursor.selection().toPlainText())
930 928
931 929 cursor.setPosition(end)
932 930 cursor.endEditBlock()
933 931 return text
934 932
935 933 def _insert_plain_text(self, cursor, text):
936 934 """ Inserts plain text using the specified cursor, processing ANSI codes
937 935 if enabled.
938 936 """
939 937 cursor.beginEditBlock()
940 938 if self.ansi_codes:
941 939 for substring in self._ansi_processor.split_string(text):
942 940 for action in self._ansi_processor.actions:
943 941 if action.kind == 'erase' and action.area == 'screen':
944 942 cursor.select(QtGui.QTextCursor.Document)
945 943 cursor.removeSelectedText()
946 944 format = self._ansi_processor.get_format()
947 945 cursor.insertText(substring, format)
948 946 else:
949 947 cursor.insertText(text)
950 948 cursor.endEditBlock()
951 949
952 950 def _insert_plain_text_into_buffer(self, text):
953 951 """ Inserts text into the input buffer at the current cursor position,
954 952 ensuring that continuation prompts are inserted as necessary.
955 953 """
956 954 lines = str(text).splitlines(True)
957 955 if lines:
958 956 self._keep_cursor_in_buffer()
959 957 cursor = self._control.textCursor()
960 958 cursor.beginEditBlock()
961 959 cursor.insertText(lines[0])
962 960 for line in lines[1:]:
963 961 if self._continuation_prompt_html is None:
964 962 cursor.insertText(self._continuation_prompt)
965 963 else:
966 964 self._continuation_prompt = \
967 965 self._insert_html_fetching_plain_text(
968 966 cursor, self._continuation_prompt_html)
969 967 cursor.insertText(line)
970 968 cursor.endEditBlock()
971 969 self._control.setTextCursor(cursor)
972 970
973 971 def _in_buffer(self, position=None):
974 972 """ Returns whether the current cursor (or, if specified, a position) is
975 973 inside the editing region.
976 974 """
977 975 cursor = self._control.textCursor()
978 976 if position is None:
979 977 position = cursor.position()
980 978 else:
981 979 cursor.setPosition(position)
982 980 line = cursor.blockNumber()
983 981 prompt_line = self._get_prompt_cursor().blockNumber()
984 982 if line == prompt_line:
985 983 return position >= self._prompt_pos
986 984 elif line > prompt_line:
987 985 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
988 986 prompt_pos = cursor.position() + len(self._continuation_prompt)
989 987 return position >= prompt_pos
990 988 return False
991 989
992 990 def _keep_cursor_in_buffer(self):
993 991 """ Ensures that the cursor is inside the editing region. Returns
994 992 whether the cursor was moved.
995 993 """
996 994 moved = not self._in_buffer()
997 995 if moved:
998 996 cursor = self._control.textCursor()
999 997 cursor.movePosition(QtGui.QTextCursor.End)
1000 998 self._control.setTextCursor(cursor)
1001 999 return moved
1002 1000
1003 1001 def _page(self, text):
1004 1002 """ Displays text using the pager if it exceeds the height of the
1005 1003 visible area.
1006 1004 """
1007 1005 if self._page_style == 'none':
1008 1006 self._append_plain_text(text)
1009 1007 else:
1010 1008 line_height = QtGui.QFontMetrics(self.font).height()
1011 1009 minlines = self._control.viewport().height() / line_height
1012 1010 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1013 1011 if self._page_style == 'custom':
1014 1012 self.custom_page_requested.emit(text)
1015 1013 else:
1016 1014 self._page_control.clear()
1017 1015 cursor = self._page_control.textCursor()
1018 1016 self._insert_plain_text(cursor, text)
1019 1017 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1020 1018
1021 1019 self._page_control.viewport().resize(self._control.size())
1022 1020 if self._splitter:
1023 1021 self._page_control.show()
1024 1022 self._page_control.setFocus()
1025 1023 else:
1026 1024 self.layout().setCurrentWidget(self._page_control)
1027 1025 else:
1028 1026 self._append_plain_text(text)
1029 1027
1030 1028 def _prompt_started(self):
1031 1029 """ Called immediately after a new prompt is displayed.
1032 1030 """
1033 1031 # Temporarily disable the maximum block count to permit undo/redo and
1034 1032 # to ensure that the prompt position does not change due to truncation.
1035 1033 self._control.document().setMaximumBlockCount(0)
1036 1034 self._control.setUndoRedoEnabled(True)
1037 1035
1038 1036 self._control.setReadOnly(False)
1039 1037 self._control.moveCursor(QtGui.QTextCursor.End)
1040 1038
1041 1039 self._executing = False
1042 1040 self._prompt_started_hook()
1043 1041
1044 1042 def _prompt_finished(self):
1045 1043 """ Called immediately after a prompt is finished, i.e. when some input
1046 1044 will be processed and a new prompt displayed.
1047 1045 """
1048 1046 self._control.setUndoRedoEnabled(False)
1049 1047 self._control.setReadOnly(True)
1050 1048 self._prompt_finished_hook()
1051 1049
1052 1050 def _readline(self, prompt='', callback=None):
1053 1051 """ Reads one line of input from the user.
1054 1052
1055 1053 Parameters
1056 1054 ----------
1057 1055 prompt : str, optional
1058 1056 The prompt to print before reading the line.
1059 1057
1060 1058 callback : callable, optional
1061 1059 A callback to execute with the read line. If not specified, input is
1062 1060 read *synchronously* and this method does not return until it has
1063 1061 been read.
1064 1062
1065 1063 Returns
1066 1064 -------
1067 1065 If a callback is specified, returns nothing. Otherwise, returns the
1068 1066 input string with the trailing newline stripped.
1069 1067 """
1070 1068 if self._reading:
1071 1069 raise RuntimeError('Cannot read a line. Widget is already reading.')
1072 1070
1073 1071 if not callback and not self.isVisible():
1074 1072 # If the user cannot see the widget, this function cannot return.
1075 1073 raise RuntimeError('Cannot synchronously read a line if the widget'
1076 1074 'is not visible!')
1077 1075
1078 1076 self._reading = True
1079 1077 self._show_prompt(prompt, newline=False)
1080 1078
1081 1079 if callback is None:
1082 1080 self._reading_callback = None
1083 1081 while self._reading:
1084 1082 QtCore.QCoreApplication.processEvents()
1085 1083 return self.input_buffer.rstrip('\n')
1086 1084
1087 1085 else:
1088 1086 self._reading_callback = lambda: \
1089 1087 callback(self.input_buffer.rstrip('\n'))
1090 1088
1091 1089 def _set_continuation_prompt(self, prompt, html=False):
1092 1090 """ Sets the continuation prompt.
1093 1091
1094 1092 Parameters
1095 1093 ----------
1096 1094 prompt : str
1097 1095 The prompt to show when more input is needed.
1098 1096
1099 1097 html : bool, optional (default False)
1100 1098 If set, the prompt will be inserted as formatted HTML. Otherwise,
1101 1099 the prompt will be treated as plain text, though ANSI color codes
1102 1100 will be handled.
1103 1101 """
1104 1102 if html:
1105 1103 self._continuation_prompt_html = prompt
1106 1104 else:
1107 1105 self._continuation_prompt = prompt
1108 1106 self._continuation_prompt_html = None
1109 1107
1110 1108 def _set_cursor(self, cursor):
1111 1109 """ Convenience method to set the current cursor.
1112 1110 """
1113 1111 self._control.setTextCursor(cursor)
1114 1112
1115 1113 def _set_position(self, position):
1116 1114 """ Convenience method to set the position of the cursor.
1117 1115 """
1118 1116 cursor = self._control.textCursor()
1119 1117 cursor.setPosition(position)
1120 1118 self._control.setTextCursor(cursor)
1121 1119
1122 1120 def _set_selection(self, start, end):
1123 1121 """ Convenience method to set the current selected text.
1124 1122 """
1125 1123 self._control.setTextCursor(self._get_selection_cursor(start, end))
1126 1124
1127 1125 def _show_context_menu(self, pos):
1128 1126 """ Shows a context menu at the given QPoint (in widget coordinates).
1129 1127 """
1130 1128 menu = QtGui.QMenu()
1131 1129
1132 1130 copy_action = menu.addAction('Copy', self.copy)
1133 1131 copy_action.setEnabled(self._get_cursor().hasSelection())
1134 1132 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1135 1133
1136 1134 paste_action = menu.addAction('Paste', self.paste)
1137 1135 paste_action.setEnabled(self.can_paste())
1138 1136 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1139 1137
1140 1138 menu.addSeparator()
1141 1139 menu.addAction('Select All', self.select_all)
1142 1140
1143 1141 menu.exec_(self._control.mapToGlobal(pos))
1144 1142
1145 1143 def _show_prompt(self, prompt=None, html=False, newline=True):
1146 1144 """ Writes a new prompt at the end of the buffer.
1147 1145
1148 1146 Parameters
1149 1147 ----------
1150 1148 prompt : str, optional
1151 1149 The prompt to show. If not specified, the previous prompt is used.
1152 1150
1153 1151 html : bool, optional (default False)
1154 1152 Only relevant when a prompt is specified. If set, the prompt will
1155 1153 be inserted as formatted HTML. Otherwise, the prompt will be treated
1156 1154 as plain text, though ANSI color codes will be handled.
1157 1155
1158 1156 newline : bool, optional (default True)
1159 1157 If set, a new line will be written before showing the prompt if
1160 1158 there is not already a newline at the end of the buffer.
1161 1159 """
1162 1160 # Insert a preliminary newline, if necessary.
1163 1161 if newline:
1164 1162 cursor = self._get_end_cursor()
1165 1163 if cursor.position() > 0:
1166 1164 cursor.movePosition(QtGui.QTextCursor.Left,
1167 1165 QtGui.QTextCursor.KeepAnchor)
1168 1166 if str(cursor.selection().toPlainText()) != '\n':
1169 1167 self._append_plain_text('\n')
1170 1168
1171 1169 # Write the prompt.
1172 1170 if prompt is None:
1173 1171 if self._prompt_html is None:
1174 1172 self._append_plain_text(self._prompt)
1175 1173 else:
1176 1174 self._append_html(self._prompt_html)
1177 1175 else:
1178 1176 if html:
1179 1177 self._prompt = self._append_html_fetching_plain_text(prompt)
1180 1178 self._prompt_html = prompt
1181 1179 else:
1182 1180 self._append_plain_text(prompt)
1183 1181 self._prompt = prompt
1184 1182 self._prompt_html = None
1185 1183
1186 1184 self._prompt_pos = self._get_end_cursor().position()
1187 1185 self._prompt_started()
1188 1186
1189 1187 def _show_continuation_prompt(self):
1190 1188 """ Writes a new continuation prompt at the end of the buffer.
1191 1189 """
1192 1190 if self._continuation_prompt_html is None:
1193 1191 self._append_plain_text(self._continuation_prompt)
1194 1192 else:
1195 1193 self._continuation_prompt = self._append_html_fetching_plain_text(
1196 1194 self._continuation_prompt_html)
1197 1195
1198 1196 self._prompt_started()
1199 1197
1200 1198
1201 1199 class HistoryConsoleWidget(ConsoleWidget):
1202 1200 """ A ConsoleWidget that keeps a history of the commands that have been
1203 1201 executed.
1204 1202 """
1205 1203
1206 1204 #---------------------------------------------------------------------------
1207 1205 # 'object' interface
1208 1206 #---------------------------------------------------------------------------
1209 1207
1210 1208 def __init__(self, *args, **kw):
1211 1209 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1212 1210 self._history = []
1213 1211 self._history_index = 0
1214 1212
1215 1213 #---------------------------------------------------------------------------
1216 1214 # 'ConsoleWidget' public interface
1217 1215 #---------------------------------------------------------------------------
1218 1216
1219 1217 def execute(self, source=None, hidden=False, interactive=False):
1220 1218 """ Reimplemented to the store history.
1221 1219 """
1222 1220 if not hidden:
1223 1221 history = self.input_buffer if source is None else source
1224 1222
1225 1223 executed = super(HistoryConsoleWidget, self).execute(
1226 1224 source, hidden, interactive)
1227 1225
1228 1226 if executed and not hidden:
1229 1227 self._history.append(history.rstrip())
1230 1228 self._history_index = len(self._history)
1231 1229
1232 1230 return executed
1233 1231
1234 1232 #---------------------------------------------------------------------------
1235 1233 # 'ConsoleWidget' abstract interface
1236 1234 #---------------------------------------------------------------------------
1237 1235
1238 1236 def _up_pressed(self):
1239 1237 """ Called when the up key is pressed. Returns whether to continue
1240 1238 processing the event.
1241 1239 """
1242 1240 prompt_cursor = self._get_prompt_cursor()
1243 1241 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1244 1242 self.history_previous()
1245 1243
1246 1244 # Go to the first line of prompt for seemless history scrolling.
1247 1245 cursor = self._get_prompt_cursor()
1248 1246 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1249 1247 self._set_cursor(cursor)
1250 1248
1251 1249 return False
1252 1250 return True
1253 1251
1254 1252 def _down_pressed(self):
1255 1253 """ Called when the down key is pressed. Returns whether to continue
1256 1254 processing the event.
1257 1255 """
1258 1256 end_cursor = self._get_end_cursor()
1259 1257 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1260 1258 self.history_next()
1261 1259 return False
1262 1260 return True
1263 1261
1264 1262 #---------------------------------------------------------------------------
1265 1263 # 'HistoryConsoleWidget' public interface
1266 1264 #---------------------------------------------------------------------------
1267 1265
1268 1266 def history_previous(self):
1269 1267 """ If possible, set the input buffer to the previous item in the
1270 1268 history.
1271 1269 """
1272 1270 if self._history_index > 0:
1273 1271 self._history_index -= 1
1274 1272 self.input_buffer = self._history[self._history_index]
1275 1273
1276 1274 def history_next(self):
1277 1275 """ Set the input buffer to the next item in the history, or a blank
1278 1276 line if there is no subsequent item.
1279 1277 """
1280 1278 if self._history_index < len(self._history):
1281 1279 self._history_index += 1
1282 1280 if self._history_index < len(self._history):
1283 1281 self.input_buffer = self._history[self._history_index]
1284 1282 else:
1285 1283 self.input_buffer = ''
1286 1284
1287 1285 #---------------------------------------------------------------------------
1288 1286 # 'HistoryConsoleWidget' protected interface
1289 1287 #---------------------------------------------------------------------------
1290 1288
1291 1289 def _set_history(self, history):
1292 1290 """ Replace the current history with a sequence of history items.
1293 1291 """
1294 1292 self._history = list(history)
1295 1293 self._history_index = len(self._history)
General Comments 0
You need to be logged in to leave comments. Login now