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