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