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