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