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