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