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