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