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