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