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