##// END OF EJS Templates
* Fixed bug where ConsoleWidget accepted non-ASCII characters on paste (and also rich text in 'rich' mode)...
epatters -
Show More
@@ -1,1049 +1,1065 b''
1 # Standard library imports
1 # Standard library imports
2 import sys
2 import sys
3
3
4 # System library imports
4 # System library imports
5 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
6
6
7 # Local imports
7 # Local imports
8 from ansi_code_processor import QtAnsiCodeProcessor
8 from ansi_code_processor import QtAnsiCodeProcessor
9 from completion_widget import CompletionWidget
9 from completion_widget import CompletionWidget
10
10
11
11
12 class ConsoleWidget(QtGui.QWidget):
12 class ConsoleWidget(QtGui.QWidget):
13 """ Base class for console-type widgets. This class is mainly concerned with
13 """ Base class for console-type widgets. This class is mainly concerned with
14 dealing with the prompt, keeping the cursor inside the editing line, and
14 dealing with the prompt, keeping the cursor inside the editing line, and
15 handling ANSI escape sequences.
15 handling ANSI escape sequences.
16 """
16 """
17
17
18 # Whether to process ANSI escape codes.
18 # Whether to process ANSI escape codes.
19 ansi_codes = True
19 ansi_codes = True
20
20
21 # The maximum number of lines of text before truncation.
21 # The maximum number of lines of text before truncation.
22 buffer_size = 500
22 buffer_size = 500
23
23
24 # Whether to use a CompletionWidget or plain text output for tab completion.
24 # Whether to use a CompletionWidget or plain text output for tab completion.
25 gui_completion = True
25 gui_completion = True
26
26
27 # Whether to override ShortcutEvents for the keybindings defined by this
27 # Whether to override ShortcutEvents for the keybindings defined by this
28 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
28 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
29 # priority (when it has focus) over, e.g., window-level menu shortcuts.
29 # priority (when it has focus) over, e.g., window-level menu shortcuts.
30 override_shortcuts = False
30 override_shortcuts = False
31
31
32 # Signals that indicate ConsoleWidget state.
32 # Signals that indicate ConsoleWidget state.
33 copy_available = QtCore.pyqtSignal(bool)
33 copy_available = QtCore.pyqtSignal(bool)
34 redo_available = QtCore.pyqtSignal(bool)
34 redo_available = QtCore.pyqtSignal(bool)
35 undo_available = QtCore.pyqtSignal(bool)
35 undo_available = QtCore.pyqtSignal(bool)
36
36
37 # Protected class variables.
37 # Protected class variables.
38 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
38 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
39 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
39 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
40 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
40 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
41 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
41 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
42 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
42 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
43 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
43 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
44 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
44 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
45 _shortcuts = set(_ctrl_down_remap.keys() +
45 _shortcuts = set(_ctrl_down_remap.keys() +
46 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
46 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
47
47
48 #---------------------------------------------------------------------------
48 #---------------------------------------------------------------------------
49 # 'QObject' interface
49 # 'QObject' interface
50 #---------------------------------------------------------------------------
50 #---------------------------------------------------------------------------
51
51
52 def __init__(self, kind='plain', parent=None):
52 def __init__(self, kind='plain', parent=None):
53 """ Create a ConsoleWidget.
53 """ Create a ConsoleWidget.
54
54
55 Parameters
55 Parameters
56 ----------
56 ----------
57 kind : str, optional [default 'plain']
57 kind : str, optional [default 'plain']
58 The type of text widget to use. Valid values are 'plain', which
58 The type of text widget to use. Valid values are 'plain', which
59 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
59 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
60
60
61 parent : QWidget, optional [default None]
61 parent : QWidget, optional [default None]
62 The parent for this widget.
62 The parent for this widget.
63 """
63 """
64 super(ConsoleWidget, self).__init__(parent)
64 super(ConsoleWidget, self).__init__(parent)
65
65
66 # Create the underlying text widget.
66 # Create the underlying text widget.
67 self._control = self._create_control(kind)
67 self._control = self._create_control(kind)
68
68
69 # Initialize protected variables. Some variables contain useful state
69 # Initialize protected variables. Some variables contain useful state
70 # information for subclasses; they should be considered read-only.
70 # information for subclasses; they should be considered read-only.
71 self._ansi_processor = QtAnsiCodeProcessor()
71 self._ansi_processor = QtAnsiCodeProcessor()
72 self._completion_widget = CompletionWidget(self._control)
72 self._completion_widget = CompletionWidget(self._control)
73 self._continuation_prompt = '> '
73 self._continuation_prompt = '> '
74 self._continuation_prompt_html = None
74 self._continuation_prompt_html = None
75 self._executing = False
75 self._executing = False
76 self._prompt = ''
76 self._prompt = ''
77 self._prompt_html = None
77 self._prompt_html = None
78 self._prompt_pos = 0
78 self._prompt_pos = 0
79 self._reading = False
79 self._reading = False
80 self._reading_callback = None
80 self._reading_callback = None
81 self._tab_width = 8
81 self._tab_width = 8
82
82
83 # Set a monospaced font.
83 # Set a monospaced font.
84 self.reset_font()
84 self.reset_font()
85
85
86 def eventFilter(self, obj, event):
86 def eventFilter(self, obj, event):
87 """ Reimplemented to ensure a console-like behavior in the underlying
87 """ Reimplemented to ensure a console-like behavior in the underlying
88 text widget.
88 text widget.
89 """
89 """
90 if obj == self._control:
90 if obj == self._control:
91 etype = event.type()
91 etype = event.type()
92
92
93 # Disable moving text by drag and drop.
93 # Disable moving text by drag and drop.
94 if etype == QtCore.QEvent.DragMove:
94 if etype == QtCore.QEvent.DragMove:
95 return True
95 return True
96
96
97 elif etype == QtCore.QEvent.KeyPress:
97 elif etype == QtCore.QEvent.KeyPress:
98 return self._event_filter_keypress(event)
98 return self._event_filter_keypress(event)
99
99
100 # On Mac OS, it is always unnecessary to override shortcuts, hence
100 # On Mac OS, it is always unnecessary to override shortcuts, hence
101 # the check below. Users should just use the Control key instead of
101 # the check below. Users should just use the Control key instead of
102 # the Command key.
102 # the Command key.
103 elif etype == QtCore.QEvent.ShortcutOverride:
103 elif etype == QtCore.QEvent.ShortcutOverride:
104 if sys.platform != 'darwin' and \
104 if sys.platform != 'darwin' and \
105 self._control_key_down(event.modifiers()) and \
105 self._control_key_down(event.modifiers()) and \
106 event.key() in self._shortcuts:
106 event.key() in self._shortcuts:
107 event.accept()
107 event.accept()
108 return False
108 return False
109
109
110 return super(ConsoleWidget, self).eventFilter(obj, event)
110 return super(ConsoleWidget, self).eventFilter(obj, event)
111
111
112 #---------------------------------------------------------------------------
112 #---------------------------------------------------------------------------
113 # 'ConsoleWidget' public interface
113 # 'ConsoleWidget' public interface
114 #---------------------------------------------------------------------------
114 #---------------------------------------------------------------------------
115
115
116 def can_paste(self):
117 """ Returns whether text can be pasted from the clipboard.
118 """
119 # Accept only text that can be ASCII encoded.
120 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
121 text = QtGui.QApplication.clipboard().text()
122 if not text.isEmpty():
123 try:
124 str(text)
125 return True
126 except UnicodeEncodeError:
127 pass
128 return False
129
116 def clear(self, keep_input=False):
130 def clear(self, keep_input=False):
117 """ Clear the console, then write a new prompt. If 'keep_input' is set,
131 """ Clear the console, then write a new prompt. If 'keep_input' is set,
118 restores the old input buffer when the new prompt is written.
132 restores the old input buffer when the new prompt is written.
119 """
133 """
120 self._control.clear()
134 self._control.clear()
121 if keep_input:
135 if keep_input:
122 input_buffer = self.input_buffer
136 input_buffer = self.input_buffer
123 self._show_prompt()
137 self._show_prompt()
124 if keep_input:
138 if keep_input:
125 self.input_buffer = input_buffer
139 self.input_buffer = input_buffer
126
140
127 def copy(self):
141 def copy(self):
128 """ Copy the current selected text to the clipboard.
142 """ Copy the current selected text to the clipboard.
129 """
143 """
130 self._control.copy()
144 self._control.copy()
131
145
132 def execute(self, source=None, hidden=False, interactive=False):
146 def execute(self, source=None, hidden=False, interactive=False):
133 """ Executes source or the input buffer, possibly prompting for more
147 """ Executes source or the input buffer, possibly prompting for more
134 input.
148 input.
135
149
136 Parameters:
150 Parameters:
137 -----------
151 -----------
138 source : str, optional
152 source : str, optional
139
153
140 The source to execute. If not specified, the input buffer will be
154 The source to execute. If not specified, the input buffer will be
141 used. If specified and 'hidden' is False, the input buffer will be
155 used. If specified and 'hidden' is False, the input buffer will be
142 replaced with the source before execution.
156 replaced with the source before execution.
143
157
144 hidden : bool, optional (default False)
158 hidden : bool, optional (default False)
145
159
146 If set, no output will be shown and the prompt will not be modified.
160 If set, no output will be shown and the prompt will not be modified.
147 In other words, it will be completely invisible to the user that
161 In other words, it will be completely invisible to the user that
148 an execution has occurred.
162 an execution has occurred.
149
163
150 interactive : bool, optional (default False)
164 interactive : bool, optional (default False)
151
165
152 Whether the console is to treat the source as having been manually
166 Whether the console is to treat the source as having been manually
153 entered by the user. The effect of this parameter depends on the
167 entered by the user. The effect of this parameter depends on the
154 subclass implementation.
168 subclass implementation.
155
169
156 Raises:
170 Raises:
157 -------
171 -------
158 RuntimeError
172 RuntimeError
159 If incomplete input is given and 'hidden' is True. In this case,
173 If incomplete input is given and 'hidden' is True. In this case,
160 it not possible to prompt for more input.
174 it not possible to prompt for more input.
161
175
162 Returns:
176 Returns:
163 --------
177 --------
164 A boolean indicating whether the source was executed.
178 A boolean indicating whether the source was executed.
165 """
179 """
166 if not hidden:
180 if not hidden:
167 if source is not None:
181 if source is not None:
168 self.input_buffer = source
182 self.input_buffer = source
169
183
170 self._append_plain_text('\n')
184 self._append_plain_text('\n')
171 self._executing_input_buffer = self.input_buffer
185 self._executing_input_buffer = self.input_buffer
172 self._executing = True
186 self._executing = True
173 self._prompt_finished()
187 self._prompt_finished()
174
188
175 real_source = self.input_buffer if source is None else source
189 real_source = self.input_buffer if source is None else source
176 complete = self._is_complete(real_source, interactive)
190 complete = self._is_complete(real_source, interactive)
177 if complete:
191 if complete:
178 if not hidden:
192 if not hidden:
179 # The maximum block count is only in effect during execution.
193 # The maximum block count is only in effect during execution.
180 # This ensures that _prompt_pos does not become invalid due to
194 # This ensures that _prompt_pos does not become invalid due to
181 # text truncation.
195 # text truncation.
182 self._control.document().setMaximumBlockCount(self.buffer_size)
196 self._control.document().setMaximumBlockCount(self.buffer_size)
183 self._execute(real_source, hidden)
197 self._execute(real_source, hidden)
184 elif hidden:
198 elif hidden:
185 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
199 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
186 else:
200 else:
187 self._show_continuation_prompt()
201 self._show_continuation_prompt()
188
202
189 return complete
203 return complete
190
204
191 def _get_input_buffer(self):
205 def _get_input_buffer(self):
192 """ The text that the user has entered entered at the current prompt.
206 """ The text that the user has entered entered at the current prompt.
193 """
207 """
194 # If we're executing, the input buffer may not even exist anymore due to
208 # If we're executing, the input buffer may not even exist anymore due to
195 # the limit imposed by 'buffer_size'. Therefore, we store it.
209 # the limit imposed by 'buffer_size'. Therefore, we store it.
196 if self._executing:
210 if self._executing:
197 return self._executing_input_buffer
211 return self._executing_input_buffer
198
212
199 cursor = self._get_end_cursor()
213 cursor = self._get_end_cursor()
200 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
214 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
201 input_buffer = str(cursor.selection().toPlainText())
215 input_buffer = str(cursor.selection().toPlainText())
202
216
203 # Strip out continuation prompts.
217 # Strip out continuation prompts.
204 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
218 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
205
219
206 def _set_input_buffer(self, string):
220 def _set_input_buffer(self, string):
207 """ Replaces the text in the input buffer with 'string'.
221 """ Replaces the text in the input buffer with 'string'.
208 """
222 """
209 # Remove old text.
223 # Remove old text.
210 cursor = self._get_end_cursor()
224 cursor = self._get_end_cursor()
211 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
225 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
212 cursor.removeSelectedText()
226 cursor.removeSelectedText()
213
227
214 # Insert new text with continuation prompts.
228 # Insert new text with continuation prompts.
215 lines = string.splitlines(True)
229 lines = string.splitlines(True)
216 if lines:
230 if lines:
217 self._append_plain_text(lines[0])
231 self._append_plain_text(lines[0])
218 for i in xrange(1, len(lines)):
232 for i in xrange(1, len(lines)):
219 if self._continuation_prompt_html is None:
233 if self._continuation_prompt_html is None:
220 self._append_plain_text(self._continuation_prompt)
234 self._append_plain_text(self._continuation_prompt)
221 else:
235 else:
222 self._append_html(self._continuation_prompt_html)
236 self._append_html(self._continuation_prompt_html)
223 self._append_plain_text(lines[i])
237 self._append_plain_text(lines[i])
224 self._control.moveCursor(QtGui.QTextCursor.End)
238 self._control.moveCursor(QtGui.QTextCursor.End)
225
239
226 input_buffer = property(_get_input_buffer, _set_input_buffer)
240 input_buffer = property(_get_input_buffer, _set_input_buffer)
227
241
228 def _get_input_buffer_cursor_line(self):
242 def _get_input_buffer_cursor_line(self):
229 """ The text in the line of the input buffer in which the user's cursor
243 """ The text in the line of the input buffer in which the user's cursor
230 rests. Returns a string if there is such a line; otherwise, None.
244 rests. Returns a string if there is such a line; otherwise, None.
231 """
245 """
232 if self._executing:
246 if self._executing:
233 return None
247 return None
234 cursor = self._control.textCursor()
248 cursor = self._control.textCursor()
235 if cursor.position() >= self._prompt_pos:
249 if cursor.position() >= self._prompt_pos:
236 text = self._get_block_plain_text(cursor.block())
250 text = self._get_block_plain_text(cursor.block())
237 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
251 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
238 return text[len(self._prompt):]
252 return text[len(self._prompt):]
239 else:
253 else:
240 return text[len(self._continuation_prompt):]
254 return text[len(self._continuation_prompt):]
241 else:
255 else:
242 return None
256 return None
243
257
244 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
258 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
245
259
246 def _get_font(self):
260 def _get_font(self):
247 """ The base font being used by the ConsoleWidget.
261 """ The base font being used by the ConsoleWidget.
248 """
262 """
249 return self._control.document().defaultFont()
263 return self._control.document().defaultFont()
250
264
251 def _set_font(self, font):
265 def _set_font(self, font):
252 """ Sets the base font for the ConsoleWidget to the specified QFont.
266 """ Sets the base font for the ConsoleWidget to the specified QFont.
253 """
267 """
254 font_metrics = QtGui.QFontMetrics(font)
268 font_metrics = QtGui.QFontMetrics(font)
255 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
269 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
256
270
257 self._completion_widget.setFont(font)
271 self._completion_widget.setFont(font)
258 self._control.document().setDefaultFont(font)
272 self._control.document().setDefaultFont(font)
259
273
260 font = property(_get_font, _set_font)
274 font = property(_get_font, _set_font)
261
275
262 def paste(self):
276 def paste(self):
263 """ Paste the contents of the clipboard into the input region.
277 """ Paste the contents of the clipboard into the input region.
264 """
278 """
265 self._keep_cursor_in_buffer()
279 if self.can_paste():
266 self._control.paste()
280 self._keep_cursor_in_buffer()
281 self._control.paste()
267
282
268 def print_(self, printer):
283 def print_(self, printer):
269 """ Print the contents of the ConsoleWidget to the specified QPrinter.
284 """ Print the contents of the ConsoleWidget to the specified QPrinter.
270 """
285 """
271 self._control.print_(printer)
286 self._control.print_(printer)
272
287
273 def redo(self):
288 def redo(self):
274 """ Redo the last operation. If there is no operation to redo, nothing
289 """ Redo the last operation. If there is no operation to redo, nothing
275 happens.
290 happens.
276 """
291 """
277 self._control.redo()
292 self._control.redo()
278
293
279 def reset_font(self):
294 def reset_font(self):
280 """ Sets the font to the default fixed-width font for this platform.
295 """ Sets the font to the default fixed-width font for this platform.
281 """
296 """
282 if sys.platform == 'win32':
297 if sys.platform == 'win32':
283 name = 'Courier'
298 name = 'Courier'
284 elif sys.platform == 'darwin':
299 elif sys.platform == 'darwin':
285 name = 'Monaco'
300 name = 'Monaco'
286 else:
301 else:
287 name = 'Monospace'
302 name = 'Monospace'
288 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
303 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
289 font.setStyleHint(QtGui.QFont.TypeWriter)
304 font.setStyleHint(QtGui.QFont.TypeWriter)
290 self._set_font(font)
305 self._set_font(font)
291
306
292 def select_all(self):
307 def select_all(self):
293 """ Selects all the text in the buffer.
308 """ Selects all the text in the buffer.
294 """
309 """
295 self._control.selectAll()
310 self._control.selectAll()
296
311
297 def _get_tab_width(self):
312 def _get_tab_width(self):
298 """ The width (in terms of space characters) for tab characters.
313 """ The width (in terms of space characters) for tab characters.
299 """
314 """
300 return self._tab_width
315 return self._tab_width
301
316
302 def _set_tab_width(self, tab_width):
317 def _set_tab_width(self, tab_width):
303 """ Sets the width (in terms of space characters) for tab characters.
318 """ Sets the width (in terms of space characters) for tab characters.
304 """
319 """
305 font_metrics = QtGui.QFontMetrics(self.font)
320 font_metrics = QtGui.QFontMetrics(self.font)
306 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
321 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
307
322
308 self._tab_width = tab_width
323 self._tab_width = tab_width
309
324
310 tab_width = property(_get_tab_width, _set_tab_width)
325 tab_width = property(_get_tab_width, _set_tab_width)
311
326
312 def undo(self):
327 def undo(self):
313 """ Undo the last operation. If there is no operation to undo, nothing
328 """ Undo the last operation. If there is no operation to undo, nothing
314 happens.
329 happens.
315 """
330 """
316 self._control.undo()
331 self._control.undo()
317
332
318 #---------------------------------------------------------------------------
333 #---------------------------------------------------------------------------
319 # 'ConsoleWidget' abstract interface
334 # 'ConsoleWidget' abstract interface
320 #---------------------------------------------------------------------------
335 #---------------------------------------------------------------------------
321
336
322 def _is_complete(self, source, interactive):
337 def _is_complete(self, source, interactive):
323 """ Returns whether 'source' can be executed. When triggered by an
338 """ Returns whether 'source' can be executed. When triggered by an
324 Enter/Return key press, 'interactive' is True; otherwise, it is
339 Enter/Return key press, 'interactive' is True; otherwise, it is
325 False.
340 False.
326 """
341 """
327 raise NotImplementedError
342 raise NotImplementedError
328
343
329 def _execute(self, source, hidden):
344 def _execute(self, source, hidden):
330 """ Execute 'source'. If 'hidden', do not show any output.
345 """ Execute 'source'. If 'hidden', do not show any output.
331 """
346 """
332 raise NotImplementedError
347 raise NotImplementedError
333
348
334 def _execute_interrupt(self):
349 def _execute_interrupt(self):
335 """ Attempts to stop execution. Returns whether this method has an
350 """ Attempts to stop execution. Returns whether this method has an
336 implementation.
351 implementation.
337 """
352 """
338 return False
353 return False
339
354
340 def _prompt_started_hook(self):
355 def _prompt_started_hook(self):
341 """ Called immediately after a new prompt is displayed.
356 """ Called immediately after a new prompt is displayed.
342 """
357 """
343 pass
358 pass
344
359
345 def _prompt_finished_hook(self):
360 def _prompt_finished_hook(self):
346 """ Called immediately after a prompt is finished, i.e. when some input
361 """ Called immediately after a prompt is finished, i.e. when some input
347 will be processed and a new prompt displayed.
362 will be processed and a new prompt displayed.
348 """
363 """
349 pass
364 pass
350
365
351 def _up_pressed(self):
366 def _up_pressed(self):
352 """ Called when the up key is pressed. Returns whether to continue
367 """ Called when the up key is pressed. Returns whether to continue
353 processing the event.
368 processing the event.
354 """
369 """
355 return True
370 return True
356
371
357 def _down_pressed(self):
372 def _down_pressed(self):
358 """ Called when the down key is pressed. Returns whether to continue
373 """ Called when the down key is pressed. Returns whether to continue
359 processing the event.
374 processing the event.
360 """
375 """
361 return True
376 return True
362
377
363 def _tab_pressed(self):
378 def _tab_pressed(self):
364 """ Called when the tab key is pressed. Returns whether to continue
379 """ Called when the tab key is pressed. Returns whether to continue
365 processing the event.
380 processing the event.
366 """
381 """
367 return False
382 return False
368
383
369 #--------------------------------------------------------------------------
384 #--------------------------------------------------------------------------
370 # 'ConsoleWidget' protected interface
385 # 'ConsoleWidget' protected interface
371 #--------------------------------------------------------------------------
386 #--------------------------------------------------------------------------
372
387
373 def _append_html(self, html):
388 def _append_html(self, html):
374 """ Appends html at the end of the console buffer.
389 """ Appends html at the end of the console buffer.
375 """
390 """
376 cursor = self._get_end_cursor()
391 cursor = self._get_end_cursor()
377 self._insert_html(cursor, html)
392 self._insert_html(cursor, html)
378
393
379 def _append_html_fetching_plain_text(self, html):
394 def _append_html_fetching_plain_text(self, html):
380 """ Appends 'html', then returns the plain text version of it.
395 """ Appends 'html', then returns the plain text version of it.
381 """
396 """
382 anchor = self._get_end_cursor().position()
397 anchor = self._get_end_cursor().position()
383 self._append_html(html)
398 self._append_html(html)
384 cursor = self._get_end_cursor()
399 cursor = self._get_end_cursor()
385 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
400 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
386 return str(cursor.selection().toPlainText())
401 return str(cursor.selection().toPlainText())
387
402
388 def _append_plain_text(self, text):
403 def _append_plain_text(self, text):
389 """ Appends plain text at the end of the console buffer, processing
404 """ Appends plain text at the end of the console buffer, processing
390 ANSI codes if enabled.
405 ANSI codes if enabled.
391 """
406 """
392 cursor = self._get_end_cursor()
407 cursor = self._get_end_cursor()
393 if self.ansi_codes:
408 if self.ansi_codes:
394 for substring in self._ansi_processor.split_string(text):
409 for substring in self._ansi_processor.split_string(text):
395 format = self._ansi_processor.get_format()
410 format = self._ansi_processor.get_format()
396 cursor.insertText(substring, format)
411 cursor.insertText(substring, format)
397 else:
412 else:
398 cursor.insertText(text)
413 cursor.insertText(text)
399
414
400 def _append_plain_text_keeping_prompt(self, text):
415 def _append_plain_text_keeping_prompt(self, text):
401 """ Writes 'text' after the current prompt, then restores the old prompt
416 """ Writes 'text' after the current prompt, then restores the old prompt
402 with its old input buffer.
417 with its old input buffer.
403 """
418 """
404 input_buffer = self.input_buffer
419 input_buffer = self.input_buffer
405 self._append_plain_text('\n')
420 self._append_plain_text('\n')
406 self._prompt_finished()
421 self._prompt_finished()
407
422
408 self._append_plain_text(text)
423 self._append_plain_text(text)
409 self._show_prompt()
424 self._show_prompt()
410 self.input_buffer = input_buffer
425 self.input_buffer = input_buffer
411
426
412 def _complete_with_items(self, cursor, items):
427 def _complete_with_items(self, cursor, items):
413 """ Performs completion with 'items' at the specified cursor location.
428 """ Performs completion with 'items' at the specified cursor location.
414 """
429 """
415 if len(items) == 1:
430 if len(items) == 1:
416 cursor.setPosition(self._control.textCursor().position(),
431 cursor.setPosition(self._control.textCursor().position(),
417 QtGui.QTextCursor.KeepAnchor)
432 QtGui.QTextCursor.KeepAnchor)
418 cursor.insertText(items[0])
433 cursor.insertText(items[0])
419 elif len(items) > 1:
434 elif len(items) > 1:
420 if self.gui_completion:
435 if self.gui_completion:
421 self._completion_widget.show_items(cursor, items)
436 self._completion_widget.show_items(cursor, items)
422 else:
437 else:
423 text = self._format_as_columns(items)
438 text = self._format_as_columns(items)
424 self._append_plain_text_keeping_prompt(text)
439 self._append_plain_text_keeping_prompt(text)
425
440
426 def _control_key_down(self, modifiers):
441 def _control_key_down(self, modifiers):
427 """ Given a KeyboardModifiers flags object, return whether the Control
442 """ Given a KeyboardModifiers flags object, return whether the Control
428 key is down (on Mac OS, treat the Command key as a synonym for
443 key is down (on Mac OS, treat the Command key as a synonym for
429 Control).
444 Control).
430 """
445 """
431 down = bool(modifiers & QtCore.Qt.ControlModifier)
446 down = bool(modifiers & QtCore.Qt.ControlModifier)
432
447
433 # Note: on Mac OS, ControlModifier corresponds to the Command key while
448 # Note: on Mac OS, ControlModifier corresponds to the Command key while
434 # MetaModifier corresponds to the Control key.
449 # MetaModifier corresponds to the Control key.
435 if sys.platform == 'darwin':
450 if sys.platform == 'darwin':
436 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
451 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
437
452
438 return down
453 return down
439
454
440 def _create_control(self, kind):
455 def _create_control(self, kind):
441 """ Creates and sets the underlying text widget.
456 """ Creates and sets the underlying text widget.
442 """
457 """
443 layout = QtGui.QVBoxLayout(self)
458 layout = QtGui.QVBoxLayout(self)
444 layout.setMargin(0)
459 layout.setMargin(0)
445 if kind == 'plain':
460 if kind == 'plain':
446 control = QtGui.QPlainTextEdit()
461 control = QtGui.QPlainTextEdit()
447 elif kind == 'rich':
462 elif kind == 'rich':
448 control = QtGui.QTextEdit()
463 control = QtGui.QTextEdit()
464 control.setAcceptRichText(False)
449 else:
465 else:
450 raise ValueError("Kind %s unknown." % repr(kind))
466 raise ValueError("Kind %s unknown." % repr(kind))
451 layout.addWidget(control)
467 layout.addWidget(control)
452
468
453 control.installEventFilter(self)
469 control.installEventFilter(self)
454 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
470 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
455 control.customContextMenuRequested.connect(self._show_context_menu)
471 control.customContextMenuRequested.connect(self._show_context_menu)
456 control.copyAvailable.connect(self.copy_available)
472 control.copyAvailable.connect(self.copy_available)
457 control.redoAvailable.connect(self.redo_available)
473 control.redoAvailable.connect(self.redo_available)
458 control.undoAvailable.connect(self.undo_available)
474 control.undoAvailable.connect(self.undo_available)
459
475
460 return control
476 return control
461
477
462 def _event_filter_keypress(self, event):
478 def _event_filter_keypress(self, event):
463 """ Filter key events for the underlying text widget to create a
479 """ Filter key events for the underlying text widget to create a
464 console-like interface.
480 console-like interface.
465 """
481 """
466 key = event.key()
482 key = event.key()
467 ctrl_down = self._control_key_down(event.modifiers())
483 ctrl_down = self._control_key_down(event.modifiers())
468
484
469 # If the key is remapped, return immediately.
485 # If the key is remapped, return immediately.
470 if ctrl_down and key in self._ctrl_down_remap:
486 if ctrl_down and key in self._ctrl_down_remap:
471 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
487 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
472 self._ctrl_down_remap[key],
488 self._ctrl_down_remap[key],
473 QtCore.Qt.NoModifier)
489 QtCore.Qt.NoModifier)
474 QtGui.qApp.sendEvent(self._control, new_event)
490 QtGui.qApp.sendEvent(self._control, new_event)
475 return True
491 return True
476
492
477 # Otherwise, proceed normally and do not return early.
493 # Otherwise, proceed normally and do not return early.
478 intercepted = False
494 intercepted = False
479 cursor = self._control.textCursor()
495 cursor = self._control.textCursor()
480 position = cursor.position()
496 position = cursor.position()
481 alt_down = event.modifiers() & QtCore.Qt.AltModifier
497 alt_down = event.modifiers() & QtCore.Qt.AltModifier
482 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
498 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
483
499
484 if event.matches(QtGui.QKeySequence.Paste):
500 if event.matches(QtGui.QKeySequence.Paste):
485 # Call our paste instead of the underlying text widget's.
501 # Call our paste instead of the underlying text widget's.
486 self.paste()
502 self.paste()
487 intercepted = True
503 intercepted = True
488
504
489 elif ctrl_down:
505 elif ctrl_down:
490 if key == QtCore.Qt.Key_C and self._executing:
506 if key == QtCore.Qt.Key_C and self._executing:
491 intercepted = self._execute_interrupt()
507 intercepted = self._execute_interrupt()
492
508
493 elif key == QtCore.Qt.Key_K:
509 elif key == QtCore.Qt.Key_K:
494 if self._in_buffer(position):
510 if self._in_buffer(position):
495 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
511 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
496 QtGui.QTextCursor.KeepAnchor)
512 QtGui.QTextCursor.KeepAnchor)
497 cursor.removeSelectedText()
513 cursor.removeSelectedText()
498 intercepted = True
514 intercepted = True
499
515
500 elif key == QtCore.Qt.Key_X:
516 elif key == QtCore.Qt.Key_X:
501 intercepted = True
517 intercepted = True
502
518
503 elif key == QtCore.Qt.Key_Y:
519 elif key == QtCore.Qt.Key_Y:
504 self.paste()
520 self.paste()
505 intercepted = True
521 intercepted = True
506
522
507 elif alt_down:
523 elif alt_down:
508 if key == QtCore.Qt.Key_B:
524 if key == QtCore.Qt.Key_B:
509 self._set_cursor(self._get_word_start_cursor(position))
525 self._set_cursor(self._get_word_start_cursor(position))
510 intercepted = True
526 intercepted = True
511
527
512 elif key == QtCore.Qt.Key_F:
528 elif key == QtCore.Qt.Key_F:
513 self._set_cursor(self._get_word_end_cursor(position))
529 self._set_cursor(self._get_word_end_cursor(position))
514 intercepted = True
530 intercepted = True
515
531
516 elif key == QtCore.Qt.Key_Backspace:
532 elif key == QtCore.Qt.Key_Backspace:
517 cursor = self._get_word_start_cursor(position)
533 cursor = self._get_word_start_cursor(position)
518 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
534 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
519 cursor.removeSelectedText()
535 cursor.removeSelectedText()
520 intercepted = True
536 intercepted = True
521
537
522 elif key == QtCore.Qt.Key_D:
538 elif key == QtCore.Qt.Key_D:
523 cursor = self._get_word_end_cursor(position)
539 cursor = self._get_word_end_cursor(position)
524 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
540 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
525 cursor.removeSelectedText()
541 cursor.removeSelectedText()
526 intercepted = True
542 intercepted = True
527
543
528 else:
544 else:
529 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
545 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
530 if self._reading:
546 if self._reading:
531 self._append_plain_text('\n')
547 self._append_plain_text('\n')
532 self._reading = False
548 self._reading = False
533 if self._reading_callback:
549 if self._reading_callback:
534 self._reading_callback()
550 self._reading_callback()
535 elif not self._executing:
551 elif not self._executing:
536 self.execute(interactive=True)
552 self.execute(interactive=True)
537 intercepted = True
553 intercepted = True
538
554
539 elif key == QtCore.Qt.Key_Up:
555 elif key == QtCore.Qt.Key_Up:
540 if self._reading or not self._up_pressed():
556 if self._reading or not self._up_pressed():
541 intercepted = True
557 intercepted = True
542 else:
558 else:
543 prompt_line = self._get_prompt_cursor().blockNumber()
559 prompt_line = self._get_prompt_cursor().blockNumber()
544 intercepted = cursor.blockNumber() <= prompt_line
560 intercepted = cursor.blockNumber() <= prompt_line
545
561
546 elif key == QtCore.Qt.Key_Down:
562 elif key == QtCore.Qt.Key_Down:
547 if self._reading or not self._down_pressed():
563 if self._reading or not self._down_pressed():
548 intercepted = True
564 intercepted = True
549 else:
565 else:
550 end_line = self._get_end_cursor().blockNumber()
566 end_line = self._get_end_cursor().blockNumber()
551 intercepted = cursor.blockNumber() == end_line
567 intercepted = cursor.blockNumber() == end_line
552
568
553 elif key == QtCore.Qt.Key_Tab:
569 elif key == QtCore.Qt.Key_Tab:
554 if self._reading:
570 if self._reading:
555 intercepted = False
571 intercepted = False
556 else:
572 else:
557 intercepted = not self._tab_pressed()
573 intercepted = not self._tab_pressed()
558
574
559 elif key == QtCore.Qt.Key_Left:
575 elif key == QtCore.Qt.Key_Left:
560 intercepted = not self._in_buffer(position - 1)
576 intercepted = not self._in_buffer(position - 1)
561
577
562 elif key == QtCore.Qt.Key_Home:
578 elif key == QtCore.Qt.Key_Home:
563 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
579 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
564 start_line = cursor.blockNumber()
580 start_line = cursor.blockNumber()
565 if start_line == self._get_prompt_cursor().blockNumber():
581 if start_line == self._get_prompt_cursor().blockNumber():
566 start_pos = self._prompt_pos
582 start_pos = self._prompt_pos
567 else:
583 else:
568 start_pos = cursor.position()
584 start_pos = cursor.position()
569 start_pos += len(self._continuation_prompt)
585 start_pos += len(self._continuation_prompt)
570 if shift_down and self._in_buffer(position):
586 if shift_down and self._in_buffer(position):
571 self._set_selection(position, start_pos)
587 self._set_selection(position, start_pos)
572 else:
588 else:
573 self._set_position(start_pos)
589 self._set_position(start_pos)
574 intercepted = True
590 intercepted = True
575
591
576 elif key == QtCore.Qt.Key_Backspace and not alt_down:
592 elif key == QtCore.Qt.Key_Backspace and not alt_down:
577
593
578 # Line deletion (remove continuation prompt)
594 # Line deletion (remove continuation prompt)
579 len_prompt = len(self._continuation_prompt)
595 len_prompt = len(self._continuation_prompt)
580 if not self._reading and \
596 if not self._reading and \
581 cursor.columnNumber() == len_prompt and \
597 cursor.columnNumber() == len_prompt and \
582 position != self._prompt_pos:
598 position != self._prompt_pos:
583 cursor.setPosition(position - len_prompt,
599 cursor.setPosition(position - len_prompt,
584 QtGui.QTextCursor.KeepAnchor)
600 QtGui.QTextCursor.KeepAnchor)
585 cursor.removeSelectedText()
601 cursor.removeSelectedText()
586
602
587 # Regular backwards deletion
603 # Regular backwards deletion
588 else:
604 else:
589 anchor = cursor.anchor()
605 anchor = cursor.anchor()
590 if anchor == position:
606 if anchor == position:
591 intercepted = not self._in_buffer(position - 1)
607 intercepted = not self._in_buffer(position - 1)
592 else:
608 else:
593 intercepted = not self._in_buffer(min(anchor, position))
609 intercepted = not self._in_buffer(min(anchor, position))
594
610
595 elif key == QtCore.Qt.Key_Delete:
611 elif key == QtCore.Qt.Key_Delete:
596 anchor = cursor.anchor()
612 anchor = cursor.anchor()
597 intercepted = not self._in_buffer(min(anchor, position))
613 intercepted = not self._in_buffer(min(anchor, position))
598
614
599 # Don't move the cursor if control is down to allow copy-paste using
615 # Don't move the cursor if control is down to allow copy-paste using
600 # the keyboard in any part of the buffer.
616 # the keyboard in any part of the buffer.
601 if not ctrl_down:
617 if not ctrl_down:
602 self._keep_cursor_in_buffer()
618 self._keep_cursor_in_buffer()
603
619
604 return intercepted
620 return intercepted
605
621
606 def _format_as_columns(self, items, separator=' '):
622 def _format_as_columns(self, items, separator=' '):
607 """ Transform a list of strings into a single string with columns.
623 """ Transform a list of strings into a single string with columns.
608
624
609 Parameters
625 Parameters
610 ----------
626 ----------
611 items : sequence of strings
627 items : sequence of strings
612 The strings to process.
628 The strings to process.
613
629
614 separator : str, optional [default is two spaces]
630 separator : str, optional [default is two spaces]
615 The string that separates columns.
631 The string that separates columns.
616
632
617 Returns
633 Returns
618 -------
634 -------
619 The formatted string.
635 The formatted string.
620 """
636 """
621 # Note: this code is adapted from columnize 0.3.2.
637 # Note: this code is adapted from columnize 0.3.2.
622 # See http://code.google.com/p/pycolumnize/
638 # See http://code.google.com/p/pycolumnize/
623
639
624 font_metrics = QtGui.QFontMetrics(self.font)
640 font_metrics = QtGui.QFontMetrics(self.font)
625 displaywidth = max(5, (self.width() / font_metrics.width(' ')) - 1)
641 displaywidth = max(5, (self.width() / font_metrics.width(' ')) - 1)
626
642
627 # Some degenerate cases.
643 # Some degenerate cases.
628 size = len(items)
644 size = len(items)
629 if size == 0:
645 if size == 0:
630 return '\n'
646 return '\n'
631 elif size == 1:
647 elif size == 1:
632 return '%s\n' % str(items[0])
648 return '%s\n' % str(items[0])
633
649
634 # Try every row count from 1 upwards
650 # Try every row count from 1 upwards
635 array_index = lambda nrows, row, col: nrows*col + row
651 array_index = lambda nrows, row, col: nrows*col + row
636 for nrows in range(1, size):
652 for nrows in range(1, size):
637 ncols = (size + nrows - 1) // nrows
653 ncols = (size + nrows - 1) // nrows
638 colwidths = []
654 colwidths = []
639 totwidth = -len(separator)
655 totwidth = -len(separator)
640 for col in range(ncols):
656 for col in range(ncols):
641 # Get max column width for this column
657 # Get max column width for this column
642 colwidth = 0
658 colwidth = 0
643 for row in range(nrows):
659 for row in range(nrows):
644 i = array_index(nrows, row, col)
660 i = array_index(nrows, row, col)
645 if i >= size: break
661 if i >= size: break
646 x = items[i]
662 x = items[i]
647 colwidth = max(colwidth, len(x))
663 colwidth = max(colwidth, len(x))
648 colwidths.append(colwidth)
664 colwidths.append(colwidth)
649 totwidth += colwidth + len(separator)
665 totwidth += colwidth + len(separator)
650 if totwidth > displaywidth:
666 if totwidth > displaywidth:
651 break
667 break
652 if totwidth <= displaywidth:
668 if totwidth <= displaywidth:
653 break
669 break
654
670
655 # The smallest number of rows computed and the max widths for each
671 # The smallest number of rows computed and the max widths for each
656 # column has been obtained. Now we just have to format each of the rows.
672 # column has been obtained. Now we just have to format each of the rows.
657 string = ''
673 string = ''
658 for row in range(nrows):
674 for row in range(nrows):
659 texts = []
675 texts = []
660 for col in range(ncols):
676 for col in range(ncols):
661 i = row + nrows*col
677 i = row + nrows*col
662 if i >= size:
678 if i >= size:
663 texts.append('')
679 texts.append('')
664 else:
680 else:
665 texts.append(items[i])
681 texts.append(items[i])
666 while texts and not texts[-1]:
682 while texts and not texts[-1]:
667 del texts[-1]
683 del texts[-1]
668 for col in range(len(texts)):
684 for col in range(len(texts)):
669 texts[col] = texts[col].ljust(colwidths[col])
685 texts[col] = texts[col].ljust(colwidths[col])
670 string += '%s\n' % str(separator.join(texts))
686 string += '%s\n' % str(separator.join(texts))
671 return string
687 return string
672
688
673 def _get_block_plain_text(self, block):
689 def _get_block_plain_text(self, block):
674 """ Given a QTextBlock, return its unformatted text.
690 """ Given a QTextBlock, return its unformatted text.
675 """
691 """
676 cursor = QtGui.QTextCursor(block)
692 cursor = QtGui.QTextCursor(block)
677 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
693 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
678 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
694 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
679 QtGui.QTextCursor.KeepAnchor)
695 QtGui.QTextCursor.KeepAnchor)
680 return str(cursor.selection().toPlainText())
696 return str(cursor.selection().toPlainText())
681
697
682 def _get_cursor(self):
698 def _get_cursor(self):
683 """ Convenience method that returns a cursor for the current position.
699 """ Convenience method that returns a cursor for the current position.
684 """
700 """
685 return self._control.textCursor()
701 return self._control.textCursor()
686
702
687 def _get_end_cursor(self):
703 def _get_end_cursor(self):
688 """ Convenience method that returns a cursor for the last character.
704 """ Convenience method that returns a cursor for the last character.
689 """
705 """
690 cursor = self._control.textCursor()
706 cursor = self._control.textCursor()
691 cursor.movePosition(QtGui.QTextCursor.End)
707 cursor.movePosition(QtGui.QTextCursor.End)
692 return cursor
708 return cursor
693
709
694 def _get_prompt_cursor(self):
710 def _get_prompt_cursor(self):
695 """ Convenience method that returns a cursor for the prompt position.
711 """ Convenience method that returns a cursor for the prompt position.
696 """
712 """
697 cursor = self._control.textCursor()
713 cursor = self._control.textCursor()
698 cursor.setPosition(self._prompt_pos)
714 cursor.setPosition(self._prompt_pos)
699 return cursor
715 return cursor
700
716
701 def _get_selection_cursor(self, start, end):
717 def _get_selection_cursor(self, start, end):
702 """ Convenience method that returns a cursor with text selected between
718 """ Convenience method that returns a cursor with text selected between
703 the positions 'start' and 'end'.
719 the positions 'start' and 'end'.
704 """
720 """
705 cursor = self._control.textCursor()
721 cursor = self._control.textCursor()
706 cursor.setPosition(start)
722 cursor.setPosition(start)
707 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
723 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
708 return cursor
724 return cursor
709
725
710 def _get_word_start_cursor(self, position):
726 def _get_word_start_cursor(self, position):
711 """ Find the start of the word to the left the given position. If a
727 """ Find the start of the word to the left the given position. If a
712 sequence of non-word characters precedes the first word, skip over
728 sequence of non-word characters precedes the first word, skip over
713 them. (This emulates the behavior of bash, emacs, etc.)
729 them. (This emulates the behavior of bash, emacs, etc.)
714 """
730 """
715 document = self._control.document()
731 document = self._control.document()
716 position -= 1
732 position -= 1
717 while self._in_buffer(position) and \
733 while self._in_buffer(position) and \
718 not document.characterAt(position).isLetterOrNumber():
734 not document.characterAt(position).isLetterOrNumber():
719 position -= 1
735 position -= 1
720 while self._in_buffer(position) and \
736 while self._in_buffer(position) and \
721 document.characterAt(position).isLetterOrNumber():
737 document.characterAt(position).isLetterOrNumber():
722 position -= 1
738 position -= 1
723 cursor = self._control.textCursor()
739 cursor = self._control.textCursor()
724 cursor.setPosition(position + 1)
740 cursor.setPosition(position + 1)
725 return cursor
741 return cursor
726
742
727 def _get_word_end_cursor(self, position):
743 def _get_word_end_cursor(self, position):
728 """ Find the end of the word to the right the given position. If a
744 """ Find the end of the word to the right the given position. If a
729 sequence of non-word characters precedes the first word, skip over
745 sequence of non-word characters precedes the first word, skip over
730 them. (This emulates the behavior of bash, emacs, etc.)
746 them. (This emulates the behavior of bash, emacs, etc.)
731 """
747 """
732 document = self._control.document()
748 document = self._control.document()
733 end = self._get_end_cursor().position()
749 end = self._get_end_cursor().position()
734 while position < end and \
750 while position < end and \
735 not document.characterAt(position).isLetterOrNumber():
751 not document.characterAt(position).isLetterOrNumber():
736 position += 1
752 position += 1
737 while position < end and \
753 while position < end and \
738 document.characterAt(position).isLetterOrNumber():
754 document.characterAt(position).isLetterOrNumber():
739 position += 1
755 position += 1
740 cursor = self._control.textCursor()
756 cursor = self._control.textCursor()
741 cursor.setPosition(position)
757 cursor.setPosition(position)
742 return cursor
758 return cursor
743
759
744 def _insert_html(self, cursor, html):
760 def _insert_html(self, cursor, html):
745 """ Insert HTML using the specified cursor in such a way that future
761 """ Insert HTML using the specified cursor in such a way that future
746 formatting is unaffected.
762 formatting is unaffected.
747 """
763 """
748 cursor.insertHtml(html)
764 cursor.insertHtml(html)
749
765
750 # After inserting HTML, the text document "remembers" the current
766 # After inserting HTML, the text document "remembers" the current
751 # formatting, which means that subsequent calls adding plain text
767 # formatting, which means that subsequent calls adding plain text
752 # will result in similar formatting, a behavior that we do not want. To
768 # will result in similar formatting, a behavior that we do not want. To
753 # prevent this, we make sure that the last character has no formatting.
769 # prevent this, we make sure that the last character has no formatting.
754 cursor.movePosition(QtGui.QTextCursor.Left,
770 cursor.movePosition(QtGui.QTextCursor.Left,
755 QtGui.QTextCursor.KeepAnchor)
771 QtGui.QTextCursor.KeepAnchor)
756 if cursor.selection().toPlainText().trimmed().isEmpty():
772 if cursor.selection().toPlainText().trimmed().isEmpty():
757 # If the last character is whitespace, it doesn't matter how it's
773 # If the last character is whitespace, it doesn't matter how it's
758 # formatted, so just clear the formatting.
774 # formatted, so just clear the formatting.
759 cursor.setCharFormat(QtGui.QTextCharFormat())
775 cursor.setCharFormat(QtGui.QTextCharFormat())
760 else:
776 else:
761 # Otherwise, add an unformatted space.
777 # Otherwise, add an unformatted space.
762 cursor.movePosition(QtGui.QTextCursor.Right)
778 cursor.movePosition(QtGui.QTextCursor.Right)
763 cursor.insertText(' ', QtGui.QTextCharFormat())
779 cursor.insertText(' ', QtGui.QTextCharFormat())
764
780
765 def _in_buffer(self, position):
781 def _in_buffer(self, position):
766 """ Returns whether the given position is inside the editing region.
782 """ Returns whether the given position is inside the editing region.
767 """
783 """
768 return position >= self._prompt_pos
784 return position >= self._prompt_pos
769
785
770 def _keep_cursor_in_buffer(self):
786 def _keep_cursor_in_buffer(self):
771 """ Ensures that the cursor is inside the editing region. Returns
787 """ Ensures that the cursor is inside the editing region. Returns
772 whether the cursor was moved.
788 whether the cursor was moved.
773 """
789 """
774 cursor = self._control.textCursor()
790 cursor = self._control.textCursor()
775 if cursor.position() < self._prompt_pos:
791 if cursor.position() < self._prompt_pos:
776 cursor.movePosition(QtGui.QTextCursor.End)
792 cursor.movePosition(QtGui.QTextCursor.End)
777 self._control.setTextCursor(cursor)
793 self._control.setTextCursor(cursor)
778 return True
794 return True
779 else:
795 else:
780 return False
796 return False
781
797
782 def _prompt_started(self):
798 def _prompt_started(self):
783 """ Called immediately after a new prompt is displayed.
799 """ Called immediately after a new prompt is displayed.
784 """
800 """
785 # Temporarily disable the maximum block count to permit undo/redo and
801 # Temporarily disable the maximum block count to permit undo/redo and
786 # to ensure that the prompt position does not change due to truncation.
802 # to ensure that the prompt position does not change due to truncation.
787 self._control.document().setMaximumBlockCount(0)
803 self._control.document().setMaximumBlockCount(0)
788 self._control.setUndoRedoEnabled(True)
804 self._control.setUndoRedoEnabled(True)
789
805
790 self._control.setReadOnly(False)
806 self._control.setReadOnly(False)
791 self._control.moveCursor(QtGui.QTextCursor.End)
807 self._control.moveCursor(QtGui.QTextCursor.End)
792
808
793 self._executing = False
809 self._executing = False
794 self._prompt_started_hook()
810 self._prompt_started_hook()
795
811
796 def _prompt_finished(self):
812 def _prompt_finished(self):
797 """ Called immediately after a prompt is finished, i.e. when some input
813 """ Called immediately after a prompt is finished, i.e. when some input
798 will be processed and a new prompt displayed.
814 will be processed and a new prompt displayed.
799 """
815 """
800 self._control.setUndoRedoEnabled(False)
816 self._control.setUndoRedoEnabled(False)
801 self._control.setReadOnly(True)
817 self._control.setReadOnly(True)
802 self._prompt_finished_hook()
818 self._prompt_finished_hook()
803
819
804 def _readline(self, prompt='', callback=None):
820 def _readline(self, prompt='', callback=None):
805 """ Reads one line of input from the user.
821 """ Reads one line of input from the user.
806
822
807 Parameters
823 Parameters
808 ----------
824 ----------
809 prompt : str, optional
825 prompt : str, optional
810 The prompt to print before reading the line.
826 The prompt to print before reading the line.
811
827
812 callback : callable, optional
828 callback : callable, optional
813 A callback to execute with the read line. If not specified, input is
829 A callback to execute with the read line. If not specified, input is
814 read *synchronously* and this method does not return until it has
830 read *synchronously* and this method does not return until it has
815 been read.
831 been read.
816
832
817 Returns
833 Returns
818 -------
834 -------
819 If a callback is specified, returns nothing. Otherwise, returns the
835 If a callback is specified, returns nothing. Otherwise, returns the
820 input string with the trailing newline stripped.
836 input string with the trailing newline stripped.
821 """
837 """
822 if self._reading:
838 if self._reading:
823 raise RuntimeError('Cannot read a line. Widget is already reading.')
839 raise RuntimeError('Cannot read a line. Widget is already reading.')
824
840
825 if not callback and not self.isVisible():
841 if not callback and not self.isVisible():
826 # If the user cannot see the widget, this function cannot return.
842 # If the user cannot see the widget, this function cannot return.
827 raise RuntimeError('Cannot synchronously read a line if the widget'
843 raise RuntimeError('Cannot synchronously read a line if the widget'
828 'is not visible!')
844 'is not visible!')
829
845
830 self._reading = True
846 self._reading = True
831 self._show_prompt(prompt, newline=False)
847 self._show_prompt(prompt, newline=False)
832
848
833 if callback is None:
849 if callback is None:
834 self._reading_callback = None
850 self._reading_callback = None
835 while self._reading:
851 while self._reading:
836 QtCore.QCoreApplication.processEvents()
852 QtCore.QCoreApplication.processEvents()
837 return self.input_buffer.rstrip('\n')
853 return self.input_buffer.rstrip('\n')
838
854
839 else:
855 else:
840 self._reading_callback = lambda: \
856 self._reading_callback = lambda: \
841 callback(self.input_buffer.rstrip('\n'))
857 callback(self.input_buffer.rstrip('\n'))
842
858
843 def _reset(self):
859 def _reset(self):
844 """ Clears the console and resets internal state variables.
860 """ Clears the console and resets internal state variables.
845 """
861 """
846 self._control.clear()
862 self._control.clear()
847 self._executing = self._reading = False
863 self._executing = self._reading = False
848
864
849 def _set_continuation_prompt(self, prompt, html=False):
865 def _set_continuation_prompt(self, prompt, html=False):
850 """ Sets the continuation prompt.
866 """ Sets the continuation prompt.
851
867
852 Parameters
868 Parameters
853 ----------
869 ----------
854 prompt : str
870 prompt : str
855 The prompt to show when more input is needed.
871 The prompt to show when more input is needed.
856
872
857 html : bool, optional (default False)
873 html : bool, optional (default False)
858 If set, the prompt will be inserted as formatted HTML. Otherwise,
874 If set, the prompt will be inserted as formatted HTML. Otherwise,
859 the prompt will be treated as plain text, though ANSI color codes
875 the prompt will be treated as plain text, though ANSI color codes
860 will be handled.
876 will be handled.
861 """
877 """
862 if html:
878 if html:
863 self._continuation_prompt_html = prompt
879 self._continuation_prompt_html = prompt
864 else:
880 else:
865 self._continuation_prompt = prompt
881 self._continuation_prompt = prompt
866 self._continuation_prompt_html = None
882 self._continuation_prompt_html = None
867
883
868 def _set_cursor(self, cursor):
884 def _set_cursor(self, cursor):
869 """ Convenience method to set the current cursor.
885 """ Convenience method to set the current cursor.
870 """
886 """
871 self._control.setTextCursor(cursor)
887 self._control.setTextCursor(cursor)
872
888
873 def _set_position(self, position):
889 def _set_position(self, position):
874 """ Convenience method to set the position of the cursor.
890 """ Convenience method to set the position of the cursor.
875 """
891 """
876 cursor = self._control.textCursor()
892 cursor = self._control.textCursor()
877 cursor.setPosition(position)
893 cursor.setPosition(position)
878 self._control.setTextCursor(cursor)
894 self._control.setTextCursor(cursor)
879
895
880 def _set_selection(self, start, end):
896 def _set_selection(self, start, end):
881 """ Convenience method to set the current selected text.
897 """ Convenience method to set the current selected text.
882 """
898 """
883 self._control.setTextCursor(self._get_selection_cursor(start, end))
899 self._control.setTextCursor(self._get_selection_cursor(start, end))
884
900
885 def _show_context_menu(self, pos):
901 def _show_context_menu(self, pos):
886 """ Shows a context menu at the given QPoint (in widget coordinates).
902 """ Shows a context menu at the given QPoint (in widget coordinates).
887 """
903 """
888 menu = QtGui.QMenu()
904 menu = QtGui.QMenu()
889
905
890 copy_action = QtGui.QAction('Copy', menu)
906 copy_action = QtGui.QAction('Copy', menu)
891 copy_action.triggered.connect(self.copy)
907 copy_action.triggered.connect(self.copy)
892 copy_action.setEnabled(self._get_cursor().hasSelection())
908 copy_action.setEnabled(self._get_cursor().hasSelection())
893 copy_action.setShortcut(QtGui.QKeySequence.Copy)
909 copy_action.setShortcut(QtGui.QKeySequence.Copy)
894 menu.addAction(copy_action)
910 menu.addAction(copy_action)
895
911
896 paste_action = QtGui.QAction('Paste', menu)
912 paste_action = QtGui.QAction('Paste', menu)
897 paste_action.triggered.connect(self.paste)
913 paste_action.triggered.connect(self.paste)
898 paste_action.setEnabled(self._control.canPaste())
914 paste_action.setEnabled(self.can_paste())
899 paste_action.setShortcut(QtGui.QKeySequence.Paste)
915 paste_action.setShortcut(QtGui.QKeySequence.Paste)
900 menu.addAction(paste_action)
916 menu.addAction(paste_action)
901 menu.addSeparator()
917 menu.addSeparator()
902
918
903 select_all_action = QtGui.QAction('Select All', menu)
919 select_all_action = QtGui.QAction('Select All', menu)
904 select_all_action.triggered.connect(self.select_all)
920 select_all_action.triggered.connect(self.select_all)
905 menu.addAction(select_all_action)
921 menu.addAction(select_all_action)
906
922
907 menu.exec_(self._control.mapToGlobal(pos))
923 menu.exec_(self._control.mapToGlobal(pos))
908
924
909 def _show_prompt(self, prompt=None, html=False, newline=True):
925 def _show_prompt(self, prompt=None, html=False, newline=True):
910 """ Writes a new prompt at the end of the buffer.
926 """ Writes a new prompt at the end of the buffer.
911
927
912 Parameters
928 Parameters
913 ----------
929 ----------
914 prompt : str, optional
930 prompt : str, optional
915 The prompt to show. If not specified, the previous prompt is used.
931 The prompt to show. If not specified, the previous prompt is used.
916
932
917 html : bool, optional (default False)
933 html : bool, optional (default False)
918 Only relevant when a prompt is specified. If set, the prompt will
934 Only relevant when a prompt is specified. If set, the prompt will
919 be inserted as formatted HTML. Otherwise, the prompt will be treated
935 be inserted as formatted HTML. Otherwise, the prompt will be treated
920 as plain text, though ANSI color codes will be handled.
936 as plain text, though ANSI color codes will be handled.
921
937
922 newline : bool, optional (default True)
938 newline : bool, optional (default True)
923 If set, a new line will be written before showing the prompt if
939 If set, a new line will be written before showing the prompt if
924 there is not already a newline at the end of the buffer.
940 there is not already a newline at the end of the buffer.
925 """
941 """
926 # Insert a preliminary newline, if necessary.
942 # Insert a preliminary newline, if necessary.
927 if newline:
943 if newline:
928 cursor = self._get_end_cursor()
944 cursor = self._get_end_cursor()
929 if cursor.position() > 0:
945 if cursor.position() > 0:
930 cursor.movePosition(QtGui.QTextCursor.Left,
946 cursor.movePosition(QtGui.QTextCursor.Left,
931 QtGui.QTextCursor.KeepAnchor)
947 QtGui.QTextCursor.KeepAnchor)
932 if str(cursor.selection().toPlainText()) != '\n':
948 if str(cursor.selection().toPlainText()) != '\n':
933 self._append_plain_text('\n')
949 self._append_plain_text('\n')
934
950
935 # Write the prompt.
951 # Write the prompt.
936 if prompt is None:
952 if prompt is None:
937 if self._prompt_html is None:
953 if self._prompt_html is None:
938 self._append_plain_text(self._prompt)
954 self._append_plain_text(self._prompt)
939 else:
955 else:
940 self._append_html(self._prompt_html)
956 self._append_html(self._prompt_html)
941 else:
957 else:
942 if html:
958 if html:
943 self._prompt = self._append_html_fetching_plain_text(prompt)
959 self._prompt = self._append_html_fetching_plain_text(prompt)
944 self._prompt_html = prompt
960 self._prompt_html = prompt
945 else:
961 else:
946 self._append_plain_text(prompt)
962 self._append_plain_text(prompt)
947 self._prompt = prompt
963 self._prompt = prompt
948 self._prompt_html = None
964 self._prompt_html = None
949
965
950 self._prompt_pos = self._get_end_cursor().position()
966 self._prompt_pos = self._get_end_cursor().position()
951 self._prompt_started()
967 self._prompt_started()
952
968
953 def _show_continuation_prompt(self):
969 def _show_continuation_prompt(self):
954 """ Writes a new continuation prompt at the end of the buffer.
970 """ Writes a new continuation prompt at the end of the buffer.
955 """
971 """
956 if self._continuation_prompt_html is None:
972 if self._continuation_prompt_html is None:
957 self._append_plain_text(self._continuation_prompt)
973 self._append_plain_text(self._continuation_prompt)
958 else:
974 else:
959 self._continuation_prompt = self._append_html_fetching_plain_text(
975 self._continuation_prompt = self._append_html_fetching_plain_text(
960 self._continuation_prompt_html)
976 self._continuation_prompt_html)
961
977
962 self._prompt_started()
978 self._prompt_started()
963
979
964
980
965 class HistoryConsoleWidget(ConsoleWidget):
981 class HistoryConsoleWidget(ConsoleWidget):
966 """ A ConsoleWidget that keeps a history of the commands that have been
982 """ A ConsoleWidget that keeps a history of the commands that have been
967 executed.
983 executed.
968 """
984 """
969
985
970 #---------------------------------------------------------------------------
986 #---------------------------------------------------------------------------
971 # 'object' interface
987 # 'object' interface
972 #---------------------------------------------------------------------------
988 #---------------------------------------------------------------------------
973
989
974 def __init__(self, *args, **kw):
990 def __init__(self, *args, **kw):
975 super(HistoryConsoleWidget, self).__init__(*args, **kw)
991 super(HistoryConsoleWidget, self).__init__(*args, **kw)
976 self._history = []
992 self._history = []
977 self._history_index = 0
993 self._history_index = 0
978
994
979 #---------------------------------------------------------------------------
995 #---------------------------------------------------------------------------
980 # 'ConsoleWidget' public interface
996 # 'ConsoleWidget' public interface
981 #---------------------------------------------------------------------------
997 #---------------------------------------------------------------------------
982
998
983 def execute(self, source=None, hidden=False, interactive=False):
999 def execute(self, source=None, hidden=False, interactive=False):
984 """ Reimplemented to the store history.
1000 """ Reimplemented to the store history.
985 """
1001 """
986 if not hidden:
1002 if not hidden:
987 history = self.input_buffer if source is None else source
1003 history = self.input_buffer if source is None else source
988
1004
989 executed = super(HistoryConsoleWidget, self).execute(
1005 executed = super(HistoryConsoleWidget, self).execute(
990 source, hidden, interactive)
1006 source, hidden, interactive)
991
1007
992 if executed and not hidden:
1008 if executed and not hidden:
993 self._history.append(history.rstrip())
1009 self._history.append(history.rstrip())
994 self._history_index = len(self._history)
1010 self._history_index = len(self._history)
995
1011
996 return executed
1012 return executed
997
1013
998 #---------------------------------------------------------------------------
1014 #---------------------------------------------------------------------------
999 # 'ConsoleWidget' abstract interface
1015 # 'ConsoleWidget' abstract interface
1000 #---------------------------------------------------------------------------
1016 #---------------------------------------------------------------------------
1001
1017
1002 def _up_pressed(self):
1018 def _up_pressed(self):
1003 """ Called when the up key is pressed. Returns whether to continue
1019 """ Called when the up key is pressed. Returns whether to continue
1004 processing the event.
1020 processing the event.
1005 """
1021 """
1006 prompt_cursor = self._get_prompt_cursor()
1022 prompt_cursor = self._get_prompt_cursor()
1007 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1023 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1008 self.history_previous()
1024 self.history_previous()
1009
1025
1010 # Go to the first line of prompt for seemless history scrolling.
1026 # Go to the first line of prompt for seemless history scrolling.
1011 cursor = self._get_prompt_cursor()
1027 cursor = self._get_prompt_cursor()
1012 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1028 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1013 self._set_cursor(cursor)
1029 self._set_cursor(cursor)
1014
1030
1015 return False
1031 return False
1016 return True
1032 return True
1017
1033
1018 def _down_pressed(self):
1034 def _down_pressed(self):
1019 """ Called when the down key is pressed. Returns whether to continue
1035 """ Called when the down key is pressed. Returns whether to continue
1020 processing the event.
1036 processing the event.
1021 """
1037 """
1022 end_cursor = self._get_end_cursor()
1038 end_cursor = self._get_end_cursor()
1023 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1039 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1024 self.history_next()
1040 self.history_next()
1025 return False
1041 return False
1026 return True
1042 return True
1027
1043
1028 #---------------------------------------------------------------------------
1044 #---------------------------------------------------------------------------
1029 # 'HistoryConsoleWidget' interface
1045 # 'HistoryConsoleWidget' interface
1030 #---------------------------------------------------------------------------
1046 #---------------------------------------------------------------------------
1031
1047
1032 def history_previous(self):
1048 def history_previous(self):
1033 """ If possible, set the input buffer to the previous item in the
1049 """ If possible, set the input buffer to the previous item in the
1034 history.
1050 history.
1035 """
1051 """
1036 if self._history_index > 0:
1052 if self._history_index > 0:
1037 self._history_index -= 1
1053 self._history_index -= 1
1038 self.input_buffer = self._history[self._history_index]
1054 self.input_buffer = self._history[self._history_index]
1039
1055
1040 def history_next(self):
1056 def history_next(self):
1041 """ Set the input buffer to the next item in the history, or a blank
1057 """ Set the input buffer to the next item in the history, or a blank
1042 line if there is no subsequent item.
1058 line if there is no subsequent item.
1043 """
1059 """
1044 if self._history_index < len(self._history):
1060 if self._history_index < len(self._history):
1045 self._history_index += 1
1061 self._history_index += 1
1046 if self._history_index < len(self._history):
1062 if self._history_index < len(self._history):
1047 self.input_buffer = self._history[self._history_index]
1063 self.input_buffer = self._history[self._history_index]
1048 else:
1064 else:
1049 self.input_buffer = ''
1065 self.input_buffer = ''
@@ -1,43 +1,43 b''
1 # System library imports
1 # System library imports
2 from PyQt4 import QtCore, QtGui
2 from PyQt4 import QtCore, QtGui
3
3
4 # Local imports
4 # Local imports
5 from IPython.frontend.qt.util import image_from_svg
5 from IPython.frontend.qt.util import image_from_svg
6 from ipython_widget import IPythonWidget
6 from ipython_widget import IPythonWidget
7
7
8
8
9 class RichIPythonWidget(IPythonWidget):
9 class RichIPythonWidget(IPythonWidget):
10 """ An IPythonWidget that supports rich text, including lists, images, and
10 """ An IPythonWidget that supports rich text, including lists, images, and
11 tables. Note that raw performance will be reduced compared to the plain
11 tables. Note that raw performance will be reduced compared to the plain
12 text version.
12 text version.
13 """
13 """
14
14
15 #---------------------------------------------------------------------------
15 #---------------------------------------------------------------------------
16 # 'QObject' interface
16 # 'QObject' interface
17 #---------------------------------------------------------------------------
17 #---------------------------------------------------------------------------
18
18
19 def __init__(self, parent=None):
19 def __init__(self, parent=None):
20 """ Create a RichIPythonWidget.
20 """ Create a RichIPythonWidget.
21 """
21 """
22 super(RichIPythonWidget, self).__init__(kind='rich', parent=parent)
22 super(RichIPythonWidget, self).__init__(kind='rich', parent=parent)
23
23
24 #---------------------------------------------------------------------------
24 #---------------------------------------------------------------------------
25 # 'FrontendWidget' interface
25 # 'FrontendWidget' protected interface
26 #---------------------------------------------------------------------------
26 #---------------------------------------------------------------------------
27
27
28 def _handle_execute_payload(self, payload):
28 def _handle_execute_payload(self, payload):
29 """ Reimplemented to handle pylab plot payloads.
29 """ Reimplemented to handle pylab plot payloads.
30 """
30 """
31 super(RichIPythonWidget, self)._handle_execute_payload(payload)
32
33 plot_payload = payload.get('plot', None)
31 plot_payload = payload.get('plot', None)
34 if plot_payload and plot_payload['format'] == 'svg':
32 if plot_payload and plot_payload['format'] == 'svg':
35 try:
33 try:
36 image = image_from_svg(plot_payload['data'])
34 image = image_from_svg(plot_payload['data'])
37 except ValueError:
35 except ValueError:
38 self._append_plain_text('Received invalid plot data.')
36 self._append_plain_text('Received invalid plot data.')
39 else:
37 else:
40 cursor = self._get_end_cursor()
38 cursor = self._get_end_cursor()
41 cursor.insertBlock()
39 cursor.insertBlock()
42 cursor.insertImage(image)
40 cursor.insertImage(image)
43 cursor.insertBlock()
41 cursor.insertBlock()
42 else:
43 super(RichIPythonWidget, self)._handle_execute_payload(payload)
@@ -1,64 +1,64 b''
1 """ Defines miscellaneous Qt-related helper classes and functions.
1 """ Defines miscellaneous Qt-related helper classes and functions.
2 """
2 """
3
3
4 # System library imports.
4 # System library imports.
5 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
6
6
7 # IPython imports.
7 # IPython imports.
8 from IPython.utils.traitlets import HasTraits
8 from IPython.utils.traitlets import HasTraits
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Metaclasses
11 # Metaclasses
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 MetaHasTraits = type(HasTraits)
14 MetaHasTraits = type(HasTraits)
15 MetaQObject = type(QtCore.QObject)
15 MetaQObject = type(QtCore.QObject)
16
16
17 # You can switch the order of the parents here and it doesn't seem to matter.
17 # You can switch the order of the parents here and it doesn't seem to matter.
18 class MetaQObjectHasTraits(MetaQObject, MetaHasTraits):
18 class MetaQObjectHasTraits(MetaQObject, MetaHasTraits):
19 """ A metaclass that inherits from the metaclasses of both HasTraits and
19 """ A metaclass that inherits from the metaclasses of both HasTraits and
20 QObject.
20 QObject.
21
21
22 Using this metaclass allows a class to inherit from both HasTraits and
22 Using this metaclass allows a class to inherit from both HasTraits and
23 QObject. See QtKernelManager for an example.
23 QObject. See QtKernelManager for an example.
24 """
24 """
25 pass
25 pass
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Functions
28 # Functions
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31 def image_from_svg(string, size=None):
31 def image_from_svg(string, size=None):
32 """ Convert a string containing SVG data into a QImage.
32 """ Convert a SVG document to a QImage.
33
33
34 Parameters:
34 Parameters:
35 -----------
35 -----------
36 string : str
36 string : str
37 A Python string containing the SVG data.
37 A Python string containing a SVG document.
38
38
39 size : QSize or None [default None]
39 size : QSize, optional
40 The size of the image that is produced. If not specified, the SVG data's
40 The size of the image that is produced. If not specified, the SVG
41 default size is used.
41 document's default size is used.
42
42
43 Raises:
43 Raises:
44 -------
44 -------
45 ValueError
45 ValueError
46 If an invalid SVG string is provided.
46 If an invalid SVG string is provided.
47
47
48 Returns:
48 Returns:
49 --------
49 --------
50 A QImage with format QImage.Format_ARGB32_Premultiplied.
50 A QImage of format QImage.Format_ARGB32.
51 """
51 """
52 from PyQt4 import QtSvg
52 from PyQt4 import QtSvg
53
53
54 bytes = QtCore.QByteArray(string)
54 bytes = QtCore.QByteArray.fromRawData(string) # shallow copy
55 renderer = QtSvg.QSvgRenderer(bytes)
55 renderer = QtSvg.QSvgRenderer(bytes)
56 if not renderer.isValid():
56 if not renderer.isValid():
57 raise ValueError('Invalid SVG data.')
57 raise ValueError('Invalid SVG data.')
58
58
59 if size is None:
59 if size is None:
60 size = renderer.defaultSize()
60 size = renderer.defaultSize()
61 image = QtGui.QImage(size, QtGui.QImage.Format_ARGB32_Premultiplied)
61 image = QtGui.QImage(size, QtGui.QImage.Format_ARGB32)
62 painter = QtGui.QPainter(image)
62 painter = QtGui.QPainter(image)
63 renderer.render(painter)
63 renderer.render(painter)
64 return image
64 return image
General Comments 0
You need to be logged in to leave comments. Login now