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