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