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