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