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