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