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