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