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