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