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