##// END OF EJS Templates
First cut at allowing the kernel to be restarted from the frontend.
epatters -
Show More
@@ -1,1291 +1,1282 b''
1 # Standard library imports
1 # Standard library imports
2 import re
2 import re
3 import sys
3 import sys
4 from textwrap import dedent
4 from textwrap import dedent
5
5
6 # System library imports
6 # System library imports
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8
8
9 # Local imports
9 # Local imports
10 from ansi_code_processor import QtAnsiCodeProcessor
10 from ansi_code_processor import QtAnsiCodeProcessor
11 from completion_widget import CompletionWidget
11 from completion_widget import CompletionWidget
12
12
13
13
14 class ConsoleWidget(QtGui.QWidget):
14 class ConsoleWidget(QtGui.QWidget):
15 """ An abstract base class for console-type widgets. This class has
15 """ An abstract base class for console-type widgets. This class has
16 functionality for:
16 functionality for:
17
17
18 * Maintaining a prompt and editing region
18 * Maintaining a prompt and editing region
19 * Providing the traditional Unix-style console keyboard shortcuts
19 * Providing the traditional Unix-style console keyboard shortcuts
20 * Performing tab completion
20 * Performing tab completion
21 * Paging text
21 * Paging text
22 * Handling ANSI escape codes
22 * Handling ANSI escape codes
23
23
24 ConsoleWidget also provides a number of utility methods that will be
24 ConsoleWidget also provides a number of utility methods that will be
25 convenient to implementors of a console-style widget.
25 convenient to implementors of a console-style widget.
26 """
26 """
27
27
28 # Whether to process ANSI escape codes.
28 # Whether to process ANSI escape codes.
29 ansi_codes = True
29 ansi_codes = True
30
30
31 # The maximum number of lines of text before truncation.
31 # The maximum number of lines of text before truncation.
32 buffer_size = 500
32 buffer_size = 500
33
33
34 # Whether to use a list widget or plain text output for tab completion.
34 # Whether to use a list widget or plain text output for tab completion.
35 gui_completion = True
35 gui_completion = True
36
36
37 # Whether to override ShortcutEvents for the keybindings defined by this
37 # Whether to override ShortcutEvents for the keybindings defined by this
38 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
38 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
39 # priority (when it has focus) over, e.g., window-level menu shortcuts.
39 # priority (when it has focus) over, e.g., window-level menu shortcuts.
40 override_shortcuts = False
40 override_shortcuts = False
41
41
42 # Signals that indicate ConsoleWidget state.
42 # Signals that indicate ConsoleWidget state.
43 copy_available = QtCore.pyqtSignal(bool)
43 copy_available = QtCore.pyqtSignal(bool)
44 redo_available = QtCore.pyqtSignal(bool)
44 redo_available = QtCore.pyqtSignal(bool)
45 undo_available = QtCore.pyqtSignal(bool)
45 undo_available = QtCore.pyqtSignal(bool)
46
46
47 # Signal emitted when paging is needed and the paging style has been
47 # Signal emitted when paging is needed and the paging style has been
48 # specified as 'custom'.
48 # specified as 'custom'.
49 custom_page_requested = QtCore.pyqtSignal(object)
49 custom_page_requested = QtCore.pyqtSignal(object)
50
50
51 # Protected class variables.
51 # Protected class variables.
52 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
52 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
53 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
53 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
54 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
54 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
55 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
55 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
56 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
56 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
57 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
57 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
58 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
58 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
59 _shortcuts = set(_ctrl_down_remap.keys() +
59 _shortcuts = set(_ctrl_down_remap.keys() +
60 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
60 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
61
61
62 #---------------------------------------------------------------------------
62 #---------------------------------------------------------------------------
63 # 'QObject' interface
63 # 'QObject' interface
64 #---------------------------------------------------------------------------
64 #---------------------------------------------------------------------------
65
65
66 def __init__(self, kind='plain', paging='inside', parent=None):
66 def __init__(self, kind='plain', paging='inside', parent=None):
67 """ Create a ConsoleWidget.
67 """ Create a ConsoleWidget.
68
68
69 Parameters
69 Parameters
70 ----------
70 ----------
71 kind : str, optional [default 'plain']
71 kind : str, optional [default 'plain']
72 The type of underlying text widget to use. Valid values are 'plain',
72 The type of underlying text widget to use. Valid values are 'plain',
73 which specifies a QPlainTextEdit, and 'rich', which specifies a
73 which specifies a QPlainTextEdit, and 'rich', which specifies a
74 QTextEdit.
74 QTextEdit.
75
75
76 paging : str, optional [default 'inside']
76 paging : str, optional [default 'inside']
77 The type of paging to use. Valid values are:
77 The type of paging to use. Valid values are:
78 'inside' : The widget pages like a traditional terminal pager.
78 'inside' : The widget pages like a traditional terminal pager.
79 'hsplit' : When paging is requested, the widget is split
79 'hsplit' : When paging is requested, the widget is split
80 horizontally. The top pane contains the console,
80 horizontally. The top pane contains the console,
81 and the bottom pane contains the paged text.
81 and the bottom pane contains the paged text.
82 'vsplit' : Similar to 'hsplit', except that a vertical splitter
82 'vsplit' : Similar to 'hsplit', except that a vertical splitter
83 used.
83 used.
84 'custom' : No action is taken by the widget beyond emitting a
84 'custom' : No action is taken by the widget beyond emitting a
85 'custom_page_requested(str)' signal.
85 'custom_page_requested(str)' signal.
86 'none' : The text is written directly to the console.
86 'none' : The text is written directly to the console.
87
87
88 parent : QWidget, optional [default None]
88 parent : QWidget, optional [default None]
89 The parent for this widget.
89 The parent for this widget.
90 """
90 """
91 super(ConsoleWidget, self).__init__(parent)
91 super(ConsoleWidget, self).__init__(parent)
92
92
93 # Create the layout and underlying text widget.
93 # Create the layout and underlying text widget.
94 layout = QtGui.QStackedLayout(self)
94 layout = QtGui.QStackedLayout(self)
95 layout.setMargin(0)
95 layout.setMargin(0)
96 self._control = self._create_control(kind)
96 self._control = self._create_control(kind)
97 self._page_control = None
97 self._page_control = None
98 self._splitter = None
98 self._splitter = None
99 if paging in ('hsplit', 'vsplit'):
99 if paging in ('hsplit', 'vsplit'):
100 self._splitter = QtGui.QSplitter()
100 self._splitter = QtGui.QSplitter()
101 if paging == 'hsplit':
101 if paging == 'hsplit':
102 self._splitter.setOrientation(QtCore.Qt.Horizontal)
102 self._splitter.setOrientation(QtCore.Qt.Horizontal)
103 else:
103 else:
104 self._splitter.setOrientation(QtCore.Qt.Vertical)
104 self._splitter.setOrientation(QtCore.Qt.Vertical)
105 self._splitter.addWidget(self._control)
105 self._splitter.addWidget(self._control)
106 layout.addWidget(self._splitter)
106 layout.addWidget(self._splitter)
107 else:
107 else:
108 layout.addWidget(self._control)
108 layout.addWidget(self._control)
109
109
110 # Create the paging widget, if necessary.
110 # Create the paging widget, if necessary.
111 self._page_style = paging
111 self._page_style = paging
112 if paging in ('inside', 'hsplit', 'vsplit'):
112 if paging in ('inside', 'hsplit', 'vsplit'):
113 self._page_control = self._create_page_control()
113 self._page_control = self._create_page_control()
114 if self._splitter:
114 if self._splitter:
115 self._page_control.hide()
115 self._page_control.hide()
116 self._splitter.addWidget(self._page_control)
116 self._splitter.addWidget(self._page_control)
117 else:
117 else:
118 layout.addWidget(self._page_control)
118 layout.addWidget(self._page_control)
119 elif paging not in ('custom', 'none'):
119 elif paging not in ('custom', 'none'):
120 raise ValueError('Paging style %s unknown.' % repr(paging))
120 raise ValueError('Paging style %s unknown.' % repr(paging))
121
121
122 # Initialize protected variables. Some variables contain useful state
122 # Initialize protected variables. Some variables contain useful state
123 # information for subclasses; they should be considered read-only.
123 # information for subclasses; they should be considered read-only.
124 self._ansi_processor = QtAnsiCodeProcessor()
124 self._ansi_processor = QtAnsiCodeProcessor()
125 self._completion_widget = CompletionWidget(self._control)
125 self._completion_widget = CompletionWidget(self._control)
126 self._continuation_prompt = '> '
126 self._continuation_prompt = '> '
127 self._continuation_prompt_html = None
127 self._continuation_prompt_html = None
128 self._executing = False
128 self._executing = False
129 self._prompt = ''
129 self._prompt = ''
130 self._prompt_html = None
130 self._prompt_html = None
131 self._prompt_pos = 0
131 self._prompt_pos = 0
132 self._reading = False
132 self._reading = False
133 self._reading_callback = None
133 self._reading_callback = None
134 self._tab_width = 8
134 self._tab_width = 8
135
135
136 # Set a monospaced font.
136 # Set a monospaced font.
137 self.reset_font()
137 self.reset_font()
138
138
139 def eventFilter(self, obj, event):
139 def eventFilter(self, obj, event):
140 """ Reimplemented to ensure a console-like behavior in the underlying
140 """ Reimplemented to ensure a console-like behavior in the underlying
141 text widget.
141 text widget.
142 """
142 """
143 # Re-map keys for all filtered widgets.
143 # Re-map keys for all filtered widgets.
144 etype = event.type()
144 etype = event.type()
145 if etype == QtCore.QEvent.KeyPress and \
145 if etype == QtCore.QEvent.KeyPress and \
146 self._control_key_down(event.modifiers()) and \
146 self._control_key_down(event.modifiers()) and \
147 event.key() in self._ctrl_down_remap:
147 event.key() in self._ctrl_down_remap:
148 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
148 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
149 self._ctrl_down_remap[event.key()],
149 self._ctrl_down_remap[event.key()],
150 QtCore.Qt.NoModifier)
150 QtCore.Qt.NoModifier)
151 QtGui.qApp.sendEvent(obj, new_event)
151 QtGui.qApp.sendEvent(obj, new_event)
152 return True
152 return True
153
153
154 # Override shortucts for all filtered widgets. Note that on Mac OS it is
154 # Override shortucts for all filtered widgets. Note that on Mac OS it is
155 # always unnecessary to override shortcuts, hence the check below (users
155 # always unnecessary to override shortcuts, hence the check below (users
156 # should just use the Control key instead of the Command key).
156 # should just use the Control key instead of the Command key).
157 elif etype == QtCore.QEvent.ShortcutOverride and \
157 elif etype == QtCore.QEvent.ShortcutOverride and \
158 sys.platform != 'darwin' and \
158 sys.platform != 'darwin' and \
159 self._control_key_down(event.modifiers()) and \
159 self._control_key_down(event.modifiers()) and \
160 event.key() in self._shortcuts:
160 event.key() in self._shortcuts:
161 event.accept()
161 event.accept()
162 return False
162 return False
163
163
164 elif obj == self._control:
164 elif obj == self._control:
165 # Disable moving text by drag and drop.
165 # Disable moving text by drag and drop.
166 if etype == QtCore.QEvent.DragMove:
166 if etype == QtCore.QEvent.DragMove:
167 return True
167 return True
168
168
169 elif etype == QtCore.QEvent.KeyPress:
169 elif etype == QtCore.QEvent.KeyPress:
170 return self._event_filter_console_keypress(event)
170 return self._event_filter_console_keypress(event)
171
171
172 elif obj == self._page_control:
172 elif obj == self._page_control:
173 if etype == QtCore.QEvent.KeyPress:
173 if etype == QtCore.QEvent.KeyPress:
174 return self._event_filter_page_keypress(event)
174 return self._event_filter_page_keypress(event)
175
175
176 return super(ConsoleWidget, self).eventFilter(obj, event)
176 return super(ConsoleWidget, self).eventFilter(obj, event)
177
177
178 #---------------------------------------------------------------------------
178 #---------------------------------------------------------------------------
179 # 'QWidget' interface
179 # 'QWidget' interface
180 #---------------------------------------------------------------------------
180 #---------------------------------------------------------------------------
181
181
182 def sizeHint(self):
182 def sizeHint(self):
183 """ Reimplemented to suggest a size that is 80 characters wide and
183 """ Reimplemented to suggest a size that is 80 characters wide and
184 25 lines high.
184 25 lines high.
185 """
185 """
186 style = self.style()
186 style = self.style()
187 opt = QtGui.QStyleOptionHeader()
187 opt = QtGui.QStyleOptionHeader()
188 font_metrics = QtGui.QFontMetrics(self.font)
188 font_metrics = QtGui.QFontMetrics(self.font)
189 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
189 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
190
190
191 width = font_metrics.width(' ') * 80
191 width = font_metrics.width(' ') * 80
192 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
192 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
193 if self._page_style == 'hsplit':
193 if self._page_style == 'hsplit':
194 width = width * 2 + splitwidth
194 width = width * 2 + splitwidth
195
195
196 height = font_metrics.height() * 25
196 height = font_metrics.height() * 25
197 if self._page_style == 'vsplit':
197 if self._page_style == 'vsplit':
198 height = height * 2 + splitwidth
198 height = height * 2 + splitwidth
199
199
200 return QtCore.QSize(width, height)
200 return QtCore.QSize(width, height)
201
201
202 #---------------------------------------------------------------------------
202 #---------------------------------------------------------------------------
203 # 'ConsoleWidget' public interface
203 # 'ConsoleWidget' public interface
204 #---------------------------------------------------------------------------
204 #---------------------------------------------------------------------------
205
205
206 def can_paste(self):
206 def can_paste(self):
207 """ Returns whether text can be pasted from the clipboard.
207 """ Returns whether text can be pasted from the clipboard.
208 """
208 """
209 # Accept only text that can be ASCII encoded.
209 # Accept only text that can be ASCII encoded.
210 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
210 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
211 text = QtGui.QApplication.clipboard().text()
211 text = QtGui.QApplication.clipboard().text()
212 if not text.isEmpty():
212 if not text.isEmpty():
213 try:
213 try:
214 str(text)
214 str(text)
215 return True
215 return True
216 except UnicodeEncodeError:
216 except UnicodeEncodeError:
217 pass
217 pass
218 return False
218 return False
219
219
220 def clear(self, keep_input=True):
220 def clear(self, keep_input=True):
221 """ Clear the console, then write a new prompt. If 'keep_input' is set,
221 """ Clear the console, then write a new prompt. If 'keep_input' is set,
222 restores the old input buffer when the new prompt is written.
222 restores the old input buffer when the new prompt is written.
223 """
223 """
224 if keep_input:
224 if keep_input:
225 input_buffer = self.input_buffer
225 input_buffer = self.input_buffer
226 self._control.clear()
226 self._control.clear()
227 self._show_prompt()
227 self._show_prompt()
228 if keep_input:
228 if keep_input:
229 self.input_buffer = input_buffer
229 self.input_buffer = input_buffer
230
230
231 def copy(self):
231 def copy(self):
232 """ Copy the current selected text to the clipboard.
232 """ Copy the current selected text to the clipboard.
233 """
233 """
234 self._control.copy()
234 self._control.copy()
235
235
236 def execute(self, source=None, hidden=False, interactive=False):
236 def execute(self, source=None, hidden=False, interactive=False):
237 """ Executes source or the input buffer, possibly prompting for more
237 """ Executes source or the input buffer, possibly prompting for more
238 input.
238 input.
239
239
240 Parameters:
240 Parameters:
241 -----------
241 -----------
242 source : str, optional
242 source : str, optional
243
243
244 The source to execute. If not specified, the input buffer will be
244 The source to execute. If not specified, the input buffer will be
245 used. If specified and 'hidden' is False, the input buffer will be
245 used. If specified and 'hidden' is False, the input buffer will be
246 replaced with the source before execution.
246 replaced with the source before execution.
247
247
248 hidden : bool, optional (default False)
248 hidden : bool, optional (default False)
249
249
250 If set, no output will be shown and the prompt will not be modified.
250 If set, no output will be shown and the prompt will not be modified.
251 In other words, it will be completely invisible to the user that
251 In other words, it will be completely invisible to the user that
252 an execution has occurred.
252 an execution has occurred.
253
253
254 interactive : bool, optional (default False)
254 interactive : bool, optional (default False)
255
255
256 Whether the console is to treat the source as having been manually
256 Whether the console is to treat the source as having been manually
257 entered by the user. The effect of this parameter depends on the
257 entered by the user. The effect of this parameter depends on the
258 subclass implementation.
258 subclass implementation.
259
259
260 Raises:
260 Raises:
261 -------
261 -------
262 RuntimeError
262 RuntimeError
263 If incomplete input is given and 'hidden' is True. In this case,
263 If incomplete input is given and 'hidden' is True. In this case,
264 it is not possible to prompt for more input.
264 it is not possible to prompt for more input.
265
265
266 Returns:
266 Returns:
267 --------
267 --------
268 A boolean indicating whether the source was executed.
268 A boolean indicating whether the source was executed.
269 """
269 """
270 if not hidden:
270 if not hidden:
271 if source is not None:
271 if source is not None:
272 self.input_buffer = source
272 self.input_buffer = source
273
273
274 self._append_plain_text('\n')
274 self._append_plain_text('\n')
275 self._executing_input_buffer = self.input_buffer
275 self._executing_input_buffer = self.input_buffer
276 self._executing = True
276 self._executing = True
277 self._prompt_finished()
277 self._prompt_finished()
278
278
279 real_source = self.input_buffer if source is None else source
279 real_source = self.input_buffer if source is None else source
280 complete = self._is_complete(real_source, interactive)
280 complete = self._is_complete(real_source, interactive)
281 if complete:
281 if complete:
282 if not hidden:
282 if not hidden:
283 # The maximum block count is only in effect during execution.
283 # The maximum block count is only in effect during execution.
284 # This ensures that _prompt_pos does not become invalid due to
284 # This ensures that _prompt_pos does not become invalid due to
285 # text truncation.
285 # text truncation.
286 self._control.document().setMaximumBlockCount(self.buffer_size)
286 self._control.document().setMaximumBlockCount(self.buffer_size)
287 self._execute(real_source, hidden)
287 self._execute(real_source, hidden)
288 elif hidden:
288 elif hidden:
289 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
289 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
290 else:
290 else:
291 self._show_continuation_prompt()
291 self._show_continuation_prompt()
292
292
293 return complete
293 return complete
294
294
295 def _get_input_buffer(self):
295 def _get_input_buffer(self):
296 """ The text that the user has entered entered at the current prompt.
296 """ The text that the user has entered entered at the current prompt.
297 """
297 """
298 # If we're executing, the input buffer may not even exist anymore due to
298 # If we're executing, the input buffer may not even exist anymore due to
299 # the limit imposed by 'buffer_size'. Therefore, we store it.
299 # the limit imposed by 'buffer_size'. Therefore, we store it.
300 if self._executing:
300 if self._executing:
301 return self._executing_input_buffer
301 return self._executing_input_buffer
302
302
303 cursor = self._get_end_cursor()
303 cursor = self._get_end_cursor()
304 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
304 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
305 input_buffer = str(cursor.selection().toPlainText())
305 input_buffer = str(cursor.selection().toPlainText())
306
306
307 # Strip out continuation prompts.
307 # Strip out continuation prompts.
308 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
308 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
309
309
310 def _set_input_buffer(self, string):
310 def _set_input_buffer(self, string):
311 """ Replaces the text in the input buffer with 'string'.
311 """ Replaces the text in the input buffer with 'string'.
312 """
312 """
313 # For now, it is an error to modify the input buffer during execution.
313 # For now, it is an error to modify the input buffer during execution.
314 if self._executing:
314 if self._executing:
315 raise RuntimeError("Cannot change input buffer during execution.")
315 raise RuntimeError("Cannot change input buffer during execution.")
316
316
317 # Remove old text.
317 # Remove old text.
318 cursor = self._get_end_cursor()
318 cursor = self._get_end_cursor()
319 cursor.beginEditBlock()
319 cursor.beginEditBlock()
320 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
320 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
321 cursor.removeSelectedText()
321 cursor.removeSelectedText()
322
322
323 # Insert new text with continuation prompts.
323 # Insert new text with continuation prompts.
324 lines = string.splitlines(True)
324 lines = string.splitlines(True)
325 if lines:
325 if lines:
326 self._append_plain_text(lines[0])
326 self._append_plain_text(lines[0])
327 for i in xrange(1, len(lines)):
327 for i in xrange(1, len(lines)):
328 if self._continuation_prompt_html is None:
328 if self._continuation_prompt_html is None:
329 self._append_plain_text(self._continuation_prompt)
329 self._append_plain_text(self._continuation_prompt)
330 else:
330 else:
331 self._append_html(self._continuation_prompt_html)
331 self._append_html(self._continuation_prompt_html)
332 self._append_plain_text(lines[i])
332 self._append_plain_text(lines[i])
333 cursor.endEditBlock()
333 cursor.endEditBlock()
334 self._control.moveCursor(QtGui.QTextCursor.End)
334 self._control.moveCursor(QtGui.QTextCursor.End)
335
335
336 input_buffer = property(_get_input_buffer, _set_input_buffer)
336 input_buffer = property(_get_input_buffer, _set_input_buffer)
337
337
338 def _get_font(self):
338 def _get_font(self):
339 """ The base font being used by the ConsoleWidget.
339 """ The base font being used by the ConsoleWidget.
340 """
340 """
341 return self._control.document().defaultFont()
341 return self._control.document().defaultFont()
342
342
343 def _set_font(self, font):
343 def _set_font(self, font):
344 """ Sets the base font for the ConsoleWidget to the specified QFont.
344 """ Sets the base font for the ConsoleWidget to the specified QFont.
345 """
345 """
346 font_metrics = QtGui.QFontMetrics(font)
346 font_metrics = QtGui.QFontMetrics(font)
347 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
347 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
348
348
349 self._completion_widget.setFont(font)
349 self._completion_widget.setFont(font)
350 self._control.document().setDefaultFont(font)
350 self._control.document().setDefaultFont(font)
351 if self._page_control:
351 if self._page_control:
352 self._page_control.document().setDefaultFont(font)
352 self._page_control.document().setDefaultFont(font)
353
353
354 font = property(_get_font, _set_font)
354 font = property(_get_font, _set_font)
355
355
356 def paste(self):
356 def paste(self):
357 """ Paste the contents of the clipboard into the input region.
357 """ Paste the contents of the clipboard into the input region.
358 """
358 """
359 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
359 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
360 try:
360 try:
361 text = str(QtGui.QApplication.clipboard().text())
361 text = str(QtGui.QApplication.clipboard().text())
362 except UnicodeEncodeError:
362 except UnicodeEncodeError:
363 pass
363 pass
364 else:
364 else:
365 self._insert_plain_text_into_buffer(dedent(text))
365 self._insert_plain_text_into_buffer(dedent(text))
366
366
367 def print_(self, printer):
367 def print_(self, printer):
368 """ Print the contents of the ConsoleWidget to the specified QPrinter.
368 """ Print the contents of the ConsoleWidget to the specified QPrinter.
369 """
369 """
370 self._control.print_(printer)
370 self._control.print_(printer)
371
371
372 def redo(self):
372 def redo(self):
373 """ Redo the last operation. If there is no operation to redo, nothing
373 """ Redo the last operation. If there is no operation to redo, nothing
374 happens.
374 happens.
375 """
375 """
376 self._control.redo()
376 self._control.redo()
377
377
378 def reset_font(self):
378 def reset_font(self):
379 """ Sets the font to the default fixed-width font for this platform.
379 """ Sets the font to the default fixed-width font for this platform.
380 """
380 """
381 if sys.platform == 'win32':
381 if sys.platform == 'win32':
382 name = 'Courier'
382 name = 'Courier'
383 elif sys.platform == 'darwin':
383 elif sys.platform == 'darwin':
384 name = 'Monaco'
384 name = 'Monaco'
385 else:
385 else:
386 name = 'Monospace'
386 name = 'Monospace'
387 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
387 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
388 font.setStyleHint(QtGui.QFont.TypeWriter)
388 font.setStyleHint(QtGui.QFont.TypeWriter)
389 self._set_font(font)
389 self._set_font(font)
390
390
391 def select_all(self):
391 def select_all(self):
392 """ Selects all the text in the buffer.
392 """ Selects all the text in the buffer.
393 """
393 """
394 self._control.selectAll()
394 self._control.selectAll()
395
395
396 def _get_tab_width(self):
396 def _get_tab_width(self):
397 """ The width (in terms of space characters) for tab characters.
397 """ The width (in terms of space characters) for tab characters.
398 """
398 """
399 return self._tab_width
399 return self._tab_width
400
400
401 def _set_tab_width(self, tab_width):
401 def _set_tab_width(self, tab_width):
402 """ Sets the width (in terms of space characters) for tab characters.
402 """ Sets the width (in terms of space characters) for tab characters.
403 """
403 """
404 font_metrics = QtGui.QFontMetrics(self.font)
404 font_metrics = QtGui.QFontMetrics(self.font)
405 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
405 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
406
406
407 self._tab_width = tab_width
407 self._tab_width = tab_width
408
408
409 tab_width = property(_get_tab_width, _set_tab_width)
409 tab_width = property(_get_tab_width, _set_tab_width)
410
410
411 def undo(self):
411 def undo(self):
412 """ Undo the last operation. If there is no operation to undo, nothing
412 """ Undo the last operation. If there is no operation to undo, nothing
413 happens.
413 happens.
414 """
414 """
415 self._control.undo()
415 self._control.undo()
416
416
417 #---------------------------------------------------------------------------
417 #---------------------------------------------------------------------------
418 # 'ConsoleWidget' abstract interface
418 # 'ConsoleWidget' abstract interface
419 #---------------------------------------------------------------------------
419 #---------------------------------------------------------------------------
420
420
421 def _is_complete(self, source, interactive):
421 def _is_complete(self, source, interactive):
422 """ Returns whether 'source' can be executed. When triggered by an
422 """ Returns whether 'source' can be executed. When triggered by an
423 Enter/Return key press, 'interactive' is True; otherwise, it is
423 Enter/Return key press, 'interactive' is True; otherwise, it is
424 False.
424 False.
425 """
425 """
426 raise NotImplementedError
426 raise NotImplementedError
427
427
428 def _execute(self, source, hidden):
428 def _execute(self, source, hidden):
429 """ Execute 'source'. If 'hidden', do not show any output.
429 """ Execute 'source'. If 'hidden', do not show any output.
430 """
430 """
431 raise NotImplementedError
431 raise NotImplementedError
432
432
433 def _execute_interrupt(self):
434 """ Attempts to stop execution. Returns whether this method has an
435 implementation.
436 """
437 return False
438
439 def _prompt_started_hook(self):
433 def _prompt_started_hook(self):
440 """ Called immediately after a new prompt is displayed.
434 """ Called immediately after a new prompt is displayed.
441 """
435 """
442 pass
436 pass
443
437
444 def _prompt_finished_hook(self):
438 def _prompt_finished_hook(self):
445 """ Called immediately after a prompt is finished, i.e. when some input
439 """ Called immediately after a prompt is finished, i.e. when some input
446 will be processed and a new prompt displayed.
440 will be processed and a new prompt displayed.
447 """
441 """
448 pass
442 pass
449
443
450 def _up_pressed(self):
444 def _up_pressed(self):
451 """ Called when the up key is pressed. Returns whether to continue
445 """ Called when the up key is pressed. Returns whether to continue
452 processing the event.
446 processing the event.
453 """
447 """
454 return True
448 return True
455
449
456 def _down_pressed(self):
450 def _down_pressed(self):
457 """ Called when the down key is pressed. Returns whether to continue
451 """ Called when the down key is pressed. Returns whether to continue
458 processing the event.
452 processing the event.
459 """
453 """
460 return True
454 return True
461
455
462 def _tab_pressed(self):
456 def _tab_pressed(self):
463 """ Called when the tab key is pressed. Returns whether to continue
457 """ Called when the tab key is pressed. Returns whether to continue
464 processing the event.
458 processing the event.
465 """
459 """
466 return False
460 return False
467
461
468 #--------------------------------------------------------------------------
462 #--------------------------------------------------------------------------
469 # 'ConsoleWidget' protected interface
463 # 'ConsoleWidget' protected interface
470 #--------------------------------------------------------------------------
464 #--------------------------------------------------------------------------
471
465
472 def _append_html(self, html):
466 def _append_html(self, html):
473 """ Appends html at the end of the console buffer.
467 """ Appends html at the end of the console buffer.
474 """
468 """
475 cursor = self._get_end_cursor()
469 cursor = self._get_end_cursor()
476 self._insert_html(cursor, html)
470 self._insert_html(cursor, html)
477
471
478 def _append_html_fetching_plain_text(self, html):
472 def _append_html_fetching_plain_text(self, html):
479 """ Appends 'html', then returns the plain text version of it.
473 """ Appends 'html', then returns the plain text version of it.
480 """
474 """
481 cursor = self._get_end_cursor()
475 cursor = self._get_end_cursor()
482 return self._insert_html_fetching_plain_text(cursor, html)
476 return self._insert_html_fetching_plain_text(cursor, html)
483
477
484 def _append_plain_text(self, text):
478 def _append_plain_text(self, text):
485 """ Appends plain text at the end of the console buffer, processing
479 """ Appends plain text at the end of the console buffer, processing
486 ANSI codes if enabled.
480 ANSI codes if enabled.
487 """
481 """
488 cursor = self._get_end_cursor()
482 cursor = self._get_end_cursor()
489 self._insert_plain_text(cursor, text)
483 self._insert_plain_text(cursor, text)
490
484
491 def _append_plain_text_keeping_prompt(self, text):
485 def _append_plain_text_keeping_prompt(self, text):
492 """ Writes 'text' after the current prompt, then restores the old prompt
486 """ Writes 'text' after the current prompt, then restores the old prompt
493 with its old input buffer.
487 with its old input buffer.
494 """
488 """
495 input_buffer = self.input_buffer
489 input_buffer = self.input_buffer
496 self._append_plain_text('\n')
490 self._append_plain_text('\n')
497 self._prompt_finished()
491 self._prompt_finished()
498
492
499 self._append_plain_text(text)
493 self._append_plain_text(text)
500 self._show_prompt()
494 self._show_prompt()
501 self.input_buffer = input_buffer
495 self.input_buffer = input_buffer
502
496
503 def _complete_with_items(self, cursor, items):
497 def _complete_with_items(self, cursor, items):
504 """ Performs completion with 'items' at the specified cursor location.
498 """ Performs completion with 'items' at the specified cursor location.
505 """
499 """
506 if len(items) == 1:
500 if len(items) == 1:
507 cursor.setPosition(self._control.textCursor().position(),
501 cursor.setPosition(self._control.textCursor().position(),
508 QtGui.QTextCursor.KeepAnchor)
502 QtGui.QTextCursor.KeepAnchor)
509 cursor.insertText(items[0])
503 cursor.insertText(items[0])
510 elif len(items) > 1:
504 elif len(items) > 1:
511 if self.gui_completion:
505 if self.gui_completion:
512 self._completion_widget.show_items(cursor, items)
506 self._completion_widget.show_items(cursor, items)
513 else:
507 else:
514 text = self._format_as_columns(items)
508 text = self._format_as_columns(items)
515 self._append_plain_text_keeping_prompt(text)
509 self._append_plain_text_keeping_prompt(text)
516
510
517 def _control_key_down(self, modifiers):
511 def _control_key_down(self, modifiers):
518 """ Given a KeyboardModifiers flags object, return whether the Control
512 """ Given a KeyboardModifiers flags object, return whether the Control
519 key is down (on Mac OS, treat the Command key as a synonym for
513 key is down (on Mac OS, treat the Command key as a synonym for
520 Control).
514 Control).
521 """
515 """
522 down = bool(modifiers & QtCore.Qt.ControlModifier)
516 down = bool(modifiers & QtCore.Qt.ControlModifier)
523
517
524 # Note: on Mac OS, ControlModifier corresponds to the Command key while
518 # Note: on Mac OS, ControlModifier corresponds to the Command key while
525 # MetaModifier corresponds to the Control key.
519 # MetaModifier corresponds to the Control key.
526 if sys.platform == 'darwin':
520 if sys.platform == 'darwin':
527 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
521 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
528
522
529 return down
523 return down
530
524
531 def _create_control(self, kind):
525 def _create_control(self, kind):
532 """ Creates and connects the underlying text widget.
526 """ Creates and connects the underlying text widget.
533 """
527 """
534 if kind == 'plain':
528 if kind == 'plain':
535 control = QtGui.QPlainTextEdit()
529 control = QtGui.QPlainTextEdit()
536 elif kind == 'rich':
530 elif kind == 'rich':
537 control = QtGui.QTextEdit()
531 control = QtGui.QTextEdit()
538 control.setAcceptRichText(False)
532 control.setAcceptRichText(False)
539 else:
533 else:
540 raise ValueError("Kind %s unknown." % repr(kind))
534 raise ValueError("Kind %s unknown." % repr(kind))
541 control.installEventFilter(self)
535 control.installEventFilter(self)
542 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
536 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
543 control.customContextMenuRequested.connect(self._show_context_menu)
537 control.customContextMenuRequested.connect(self._show_context_menu)
544 control.copyAvailable.connect(self.copy_available)
538 control.copyAvailable.connect(self.copy_available)
545 control.redoAvailable.connect(self.redo_available)
539 control.redoAvailable.connect(self.redo_available)
546 control.undoAvailable.connect(self.undo_available)
540 control.undoAvailable.connect(self.undo_available)
547 control.setReadOnly(True)
541 control.setReadOnly(True)
548 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
542 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
549 return control
543 return control
550
544
551 def _create_page_control(self):
545 def _create_page_control(self):
552 """ Creates and connects the underlying paging widget.
546 """ Creates and connects the underlying paging widget.
553 """
547 """
554 control = QtGui.QPlainTextEdit()
548 control = QtGui.QPlainTextEdit()
555 control.installEventFilter(self)
549 control.installEventFilter(self)
556 control.setReadOnly(True)
550 control.setReadOnly(True)
557 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
551 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
558 return control
552 return control
559
553
560 def _event_filter_console_keypress(self, event):
554 def _event_filter_console_keypress(self, event):
561 """ Filter key events for the underlying text widget to create a
555 """ Filter key events for the underlying text widget to create a
562 console-like interface.
556 console-like interface.
563 """
557 """
564 intercepted = False
558 intercepted = False
565 cursor = self._control.textCursor()
559 cursor = self._control.textCursor()
566 position = cursor.position()
560 position = cursor.position()
567 key = event.key()
561 key = event.key()
568 ctrl_down = self._control_key_down(event.modifiers())
562 ctrl_down = self._control_key_down(event.modifiers())
569 alt_down = event.modifiers() & QtCore.Qt.AltModifier
563 alt_down = event.modifiers() & QtCore.Qt.AltModifier
570 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
564 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
571
565
572 if event.matches(QtGui.QKeySequence.Paste):
566 if event.matches(QtGui.QKeySequence.Paste):
573 # Call our paste instead of the underlying text widget's.
567 # Call our paste instead of the underlying text widget's.
574 self.paste()
568 self.paste()
575 intercepted = True
569 intercepted = True
576
570
577 elif ctrl_down:
571 elif ctrl_down:
578 if key == QtCore.Qt.Key_C:
572 if key == QtCore.Qt.Key_K:
579 intercepted = self._executing and self._execute_interrupt()
580
581 elif key == QtCore.Qt.Key_K:
582 if self._in_buffer(position):
573 if self._in_buffer(position):
583 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
574 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
584 QtGui.QTextCursor.KeepAnchor)
575 QtGui.QTextCursor.KeepAnchor)
585 cursor.removeSelectedText()
576 cursor.removeSelectedText()
586 intercepted = True
577 intercepted = True
587
578
588 elif key == QtCore.Qt.Key_X:
579 elif key == QtCore.Qt.Key_X:
589 intercepted = True
580 intercepted = True
590
581
591 elif key == QtCore.Qt.Key_Y:
582 elif key == QtCore.Qt.Key_Y:
592 self.paste()
583 self.paste()
593 intercepted = True
584 intercepted = True
594
585
595 elif alt_down:
586 elif alt_down:
596 if key == QtCore.Qt.Key_B:
587 if key == QtCore.Qt.Key_B:
597 self._set_cursor(self._get_word_start_cursor(position))
588 self._set_cursor(self._get_word_start_cursor(position))
598 intercepted = True
589 intercepted = True
599
590
600 elif key == QtCore.Qt.Key_F:
591 elif key == QtCore.Qt.Key_F:
601 self._set_cursor(self._get_word_end_cursor(position))
592 self._set_cursor(self._get_word_end_cursor(position))
602 intercepted = True
593 intercepted = True
603
594
604 elif key == QtCore.Qt.Key_Backspace:
595 elif key == QtCore.Qt.Key_Backspace:
605 cursor = self._get_word_start_cursor(position)
596 cursor = self._get_word_start_cursor(position)
606 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
597 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
607 cursor.removeSelectedText()
598 cursor.removeSelectedText()
608 intercepted = True
599 intercepted = True
609
600
610 elif key == QtCore.Qt.Key_D:
601 elif key == QtCore.Qt.Key_D:
611 cursor = self._get_word_end_cursor(position)
602 cursor = self._get_word_end_cursor(position)
612 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
603 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
613 cursor.removeSelectedText()
604 cursor.removeSelectedText()
614 intercepted = True
605 intercepted = True
615
606
616 else:
607 else:
617 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
608 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
618 if self._reading:
609 if self._reading:
619 self._append_plain_text('\n')
610 self._append_plain_text('\n')
620 self._reading = False
611 self._reading = False
621 if self._reading_callback:
612 if self._reading_callback:
622 self._reading_callback()
613 self._reading_callback()
623 elif not self._executing:
614 elif not self._executing:
624 self.execute(interactive=True)
615 self.execute(interactive=True)
625 intercepted = True
616 intercepted = True
626
617
627 elif key == QtCore.Qt.Key_Up:
618 elif key == QtCore.Qt.Key_Up:
628 if self._reading or not self._up_pressed():
619 if self._reading or not self._up_pressed():
629 intercepted = True
620 intercepted = True
630 else:
621 else:
631 prompt_line = self._get_prompt_cursor().blockNumber()
622 prompt_line = self._get_prompt_cursor().blockNumber()
632 intercepted = cursor.blockNumber() <= prompt_line
623 intercepted = cursor.blockNumber() <= prompt_line
633
624
634 elif key == QtCore.Qt.Key_Down:
625 elif key == QtCore.Qt.Key_Down:
635 if self._reading or not self._down_pressed():
626 if self._reading or not self._down_pressed():
636 intercepted = True
627 intercepted = True
637 else:
628 else:
638 end_line = self._get_end_cursor().blockNumber()
629 end_line = self._get_end_cursor().blockNumber()
639 intercepted = cursor.blockNumber() == end_line
630 intercepted = cursor.blockNumber() == end_line
640
631
641 elif key == QtCore.Qt.Key_Tab:
632 elif key == QtCore.Qt.Key_Tab:
642 if not self._reading:
633 if not self._reading:
643 intercepted = not self._tab_pressed()
634 intercepted = not self._tab_pressed()
644
635
645 elif key == QtCore.Qt.Key_Left:
636 elif key == QtCore.Qt.Key_Left:
646 intercepted = not self._in_buffer(position - 1)
637 intercepted = not self._in_buffer(position - 1)
647
638
648 elif key == QtCore.Qt.Key_Home:
639 elif key == QtCore.Qt.Key_Home:
649 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
640 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
650 start_line = cursor.blockNumber()
641 start_line = cursor.blockNumber()
651 if start_line == self._get_prompt_cursor().blockNumber():
642 if start_line == self._get_prompt_cursor().blockNumber():
652 start_pos = self._prompt_pos
643 start_pos = self._prompt_pos
653 else:
644 else:
654 start_pos = cursor.position()
645 start_pos = cursor.position()
655 start_pos += len(self._continuation_prompt)
646 start_pos += len(self._continuation_prompt)
656 if shift_down and self._in_buffer(position):
647 if shift_down and self._in_buffer(position):
657 self._set_selection(position, start_pos)
648 self._set_selection(position, start_pos)
658 else:
649 else:
659 self._set_position(start_pos)
650 self._set_position(start_pos)
660 intercepted = True
651 intercepted = True
661
652
662 elif key == QtCore.Qt.Key_Backspace:
653 elif key == QtCore.Qt.Key_Backspace:
663
654
664 # Line deletion (remove continuation prompt)
655 # Line deletion (remove continuation prompt)
665 len_prompt = len(self._continuation_prompt)
656 len_prompt = len(self._continuation_prompt)
666 if not self._reading and \
657 if not self._reading and \
667 cursor.columnNumber() == len_prompt and \
658 cursor.columnNumber() == len_prompt and \
668 position != self._prompt_pos:
659 position != self._prompt_pos:
669 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
660 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
670 QtGui.QTextCursor.KeepAnchor)
661 QtGui.QTextCursor.KeepAnchor)
671 cursor.removeSelectedText()
662 cursor.removeSelectedText()
672 cursor.deletePreviousChar()
663 cursor.deletePreviousChar()
673 intercepted = True
664 intercepted = True
674
665
675 # Regular backwards deletion
666 # Regular backwards deletion
676 else:
667 else:
677 anchor = cursor.anchor()
668 anchor = cursor.anchor()
678 if anchor == position:
669 if anchor == position:
679 intercepted = not self._in_buffer(position - 1)
670 intercepted = not self._in_buffer(position - 1)
680 else:
671 else:
681 intercepted = not self._in_buffer(min(anchor, position))
672 intercepted = not self._in_buffer(min(anchor, position))
682
673
683 elif key == QtCore.Qt.Key_Delete:
674 elif key == QtCore.Qt.Key_Delete:
684 anchor = cursor.anchor()
675 anchor = cursor.anchor()
685 intercepted = not self._in_buffer(min(anchor, position))
676 intercepted = not self._in_buffer(min(anchor, position))
686
677
687 # Don't move the cursor if control is down to allow copy-paste using
678 # Don't move the cursor if control is down to allow copy-paste using
688 # the keyboard in any part of the buffer.
679 # the keyboard in any part of the buffer.
689 if not ctrl_down:
680 if not ctrl_down:
690 self._keep_cursor_in_buffer()
681 self._keep_cursor_in_buffer()
691
682
692 return intercepted
683 return intercepted
693
684
694 def _event_filter_page_keypress(self, event):
685 def _event_filter_page_keypress(self, event):
695 """ Filter key events for the paging widget to create console-like
686 """ Filter key events for the paging widget to create console-like
696 interface.
687 interface.
697 """
688 """
698 key = event.key()
689 key = event.key()
699
690
700 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
691 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
701 if self._splitter:
692 if self._splitter:
702 self._page_control.hide()
693 self._page_control.hide()
703 else:
694 else:
704 self.layout().setCurrentWidget(self._control)
695 self.layout().setCurrentWidget(self._control)
705 return True
696 return True
706
697
707 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
698 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
708 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
699 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
709 QtCore.Qt.Key_Down,
700 QtCore.Qt.Key_Down,
710 QtCore.Qt.NoModifier)
701 QtCore.Qt.NoModifier)
711 QtGui.qApp.sendEvent(self._page_control, new_event)
702 QtGui.qApp.sendEvent(self._page_control, new_event)
712 return True
703 return True
713
704
714 return False
705 return False
715
706
716 def _format_as_columns(self, items, separator=' '):
707 def _format_as_columns(self, items, separator=' '):
717 """ Transform a list of strings into a single string with columns.
708 """ Transform a list of strings into a single string with columns.
718
709
719 Parameters
710 Parameters
720 ----------
711 ----------
721 items : sequence of strings
712 items : sequence of strings
722 The strings to process.
713 The strings to process.
723
714
724 separator : str, optional [default is two spaces]
715 separator : str, optional [default is two spaces]
725 The string that separates columns.
716 The string that separates columns.
726
717
727 Returns
718 Returns
728 -------
719 -------
729 The formatted string.
720 The formatted string.
730 """
721 """
731 # Note: this code is adapted from columnize 0.3.2.
722 # Note: this code is adapted from columnize 0.3.2.
732 # See http://code.google.com/p/pycolumnize/
723 # See http://code.google.com/p/pycolumnize/
733
724
734 width = self._control.viewport().width()
725 width = self._control.viewport().width()
735 char_width = QtGui.QFontMetrics(self.font).width(' ')
726 char_width = QtGui.QFontMetrics(self.font).width(' ')
736 displaywidth = max(5, width / char_width)
727 displaywidth = max(5, width / char_width)
737
728
738 # Some degenerate cases.
729 # Some degenerate cases.
739 size = len(items)
730 size = len(items)
740 if size == 0:
731 if size == 0:
741 return '\n'
732 return '\n'
742 elif size == 1:
733 elif size == 1:
743 return '%s\n' % str(items[0])
734 return '%s\n' % str(items[0])
744
735
745 # Try every row count from 1 upwards
736 # Try every row count from 1 upwards
746 array_index = lambda nrows, row, col: nrows*col + row
737 array_index = lambda nrows, row, col: nrows*col + row
747 for nrows in range(1, size):
738 for nrows in range(1, size):
748 ncols = (size + nrows - 1) // nrows
739 ncols = (size + nrows - 1) // nrows
749 colwidths = []
740 colwidths = []
750 totwidth = -len(separator)
741 totwidth = -len(separator)
751 for col in range(ncols):
742 for col in range(ncols):
752 # Get max column width for this column
743 # Get max column width for this column
753 colwidth = 0
744 colwidth = 0
754 for row in range(nrows):
745 for row in range(nrows):
755 i = array_index(nrows, row, col)
746 i = array_index(nrows, row, col)
756 if i >= size: break
747 if i >= size: break
757 x = items[i]
748 x = items[i]
758 colwidth = max(colwidth, len(x))
749 colwidth = max(colwidth, len(x))
759 colwidths.append(colwidth)
750 colwidths.append(colwidth)
760 totwidth += colwidth + len(separator)
751 totwidth += colwidth + len(separator)
761 if totwidth > displaywidth:
752 if totwidth > displaywidth:
762 break
753 break
763 if totwidth <= displaywidth:
754 if totwidth <= displaywidth:
764 break
755 break
765
756
766 # The smallest number of rows computed and the max widths for each
757 # The smallest number of rows computed and the max widths for each
767 # column has been obtained. Now we just have to format each of the rows.
758 # column has been obtained. Now we just have to format each of the rows.
768 string = ''
759 string = ''
769 for row in range(nrows):
760 for row in range(nrows):
770 texts = []
761 texts = []
771 for col in range(ncols):
762 for col in range(ncols):
772 i = row + nrows*col
763 i = row + nrows*col
773 if i >= size:
764 if i >= size:
774 texts.append('')
765 texts.append('')
775 else:
766 else:
776 texts.append(items[i])
767 texts.append(items[i])
777 while texts and not texts[-1]:
768 while texts and not texts[-1]:
778 del texts[-1]
769 del texts[-1]
779 for col in range(len(texts)):
770 for col in range(len(texts)):
780 texts[col] = texts[col].ljust(colwidths[col])
771 texts[col] = texts[col].ljust(colwidths[col])
781 string += '%s\n' % str(separator.join(texts))
772 string += '%s\n' % str(separator.join(texts))
782 return string
773 return string
783
774
784 def _get_block_plain_text(self, block):
775 def _get_block_plain_text(self, block):
785 """ Given a QTextBlock, return its unformatted text.
776 """ Given a QTextBlock, return its unformatted text.
786 """
777 """
787 cursor = QtGui.QTextCursor(block)
778 cursor = QtGui.QTextCursor(block)
788 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
779 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
789 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
780 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
790 QtGui.QTextCursor.KeepAnchor)
781 QtGui.QTextCursor.KeepAnchor)
791 return str(cursor.selection().toPlainText())
782 return str(cursor.selection().toPlainText())
792
783
793 def _get_cursor(self):
784 def _get_cursor(self):
794 """ Convenience method that returns a cursor for the current position.
785 """ Convenience method that returns a cursor for the current position.
795 """
786 """
796 return self._control.textCursor()
787 return self._control.textCursor()
797
788
798 def _get_end_cursor(self):
789 def _get_end_cursor(self):
799 """ Convenience method that returns a cursor for the last character.
790 """ Convenience method that returns a cursor for the last character.
800 """
791 """
801 cursor = self._control.textCursor()
792 cursor = self._control.textCursor()
802 cursor.movePosition(QtGui.QTextCursor.End)
793 cursor.movePosition(QtGui.QTextCursor.End)
803 return cursor
794 return cursor
804
795
805 def _get_input_buffer_cursor_column(self):
796 def _get_input_buffer_cursor_column(self):
806 """ Returns the column of the cursor in the input buffer, excluding the
797 """ Returns the column of the cursor in the input buffer, excluding the
807 contribution by the prompt, or -1 if there is no such column.
798 contribution by the prompt, or -1 if there is no such column.
808 """
799 """
809 prompt = self._get_input_buffer_cursor_prompt()
800 prompt = self._get_input_buffer_cursor_prompt()
810 if prompt is None:
801 if prompt is None:
811 return -1
802 return -1
812 else:
803 else:
813 cursor = self._control.textCursor()
804 cursor = self._control.textCursor()
814 return cursor.columnNumber() - len(prompt)
805 return cursor.columnNumber() - len(prompt)
815
806
816 def _get_input_buffer_cursor_line(self):
807 def _get_input_buffer_cursor_line(self):
817 """ Returns line of the input buffer that contains the cursor, or None
808 """ Returns line of the input buffer that contains the cursor, or None
818 if there is no such line.
809 if there is no such line.
819 """
810 """
820 prompt = self._get_input_buffer_cursor_prompt()
811 prompt = self._get_input_buffer_cursor_prompt()
821 if prompt is None:
812 if prompt is None:
822 return None
813 return None
823 else:
814 else:
824 cursor = self._control.textCursor()
815 cursor = self._control.textCursor()
825 text = self._get_block_plain_text(cursor.block())
816 text = self._get_block_plain_text(cursor.block())
826 return text[len(prompt):]
817 return text[len(prompt):]
827
818
828 def _get_input_buffer_cursor_prompt(self):
819 def _get_input_buffer_cursor_prompt(self):
829 """ Returns the (plain text) prompt for line of the input buffer that
820 """ Returns the (plain text) prompt for line of the input buffer that
830 contains the cursor, or None if there is no such line.
821 contains the cursor, or None if there is no such line.
831 """
822 """
832 if self._executing:
823 if self._executing:
833 return None
824 return None
834 cursor = self._control.textCursor()
825 cursor = self._control.textCursor()
835 if cursor.position() >= self._prompt_pos:
826 if cursor.position() >= self._prompt_pos:
836 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
827 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
837 return self._prompt
828 return self._prompt
838 else:
829 else:
839 return self._continuation_prompt
830 return self._continuation_prompt
840 else:
831 else:
841 return None
832 return None
842
833
843 def _get_prompt_cursor(self):
834 def _get_prompt_cursor(self):
844 """ Convenience method that returns a cursor for the prompt position.
835 """ Convenience method that returns a cursor for the prompt position.
845 """
836 """
846 cursor = self._control.textCursor()
837 cursor = self._control.textCursor()
847 cursor.setPosition(self._prompt_pos)
838 cursor.setPosition(self._prompt_pos)
848 return cursor
839 return cursor
849
840
850 def _get_selection_cursor(self, start, end):
841 def _get_selection_cursor(self, start, end):
851 """ Convenience method that returns a cursor with text selected between
842 """ Convenience method that returns a cursor with text selected between
852 the positions 'start' and 'end'.
843 the positions 'start' and 'end'.
853 """
844 """
854 cursor = self._control.textCursor()
845 cursor = self._control.textCursor()
855 cursor.setPosition(start)
846 cursor.setPosition(start)
856 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
847 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
857 return cursor
848 return cursor
858
849
859 def _get_word_start_cursor(self, position):
850 def _get_word_start_cursor(self, position):
860 """ Find the start of the word to the left the given position. If a
851 """ Find the start of the word to the left the given position. If a
861 sequence of non-word characters precedes the first word, skip over
852 sequence of non-word characters precedes the first word, skip over
862 them. (This emulates the behavior of bash, emacs, etc.)
853 them. (This emulates the behavior of bash, emacs, etc.)
863 """
854 """
864 document = self._control.document()
855 document = self._control.document()
865 position -= 1
856 position -= 1
866 while position >= self._prompt_pos and \
857 while position >= self._prompt_pos and \
867 not document.characterAt(position).isLetterOrNumber():
858 not document.characterAt(position).isLetterOrNumber():
868 position -= 1
859 position -= 1
869 while position >= self._prompt_pos and \
860 while position >= self._prompt_pos and \
870 document.characterAt(position).isLetterOrNumber():
861 document.characterAt(position).isLetterOrNumber():
871 position -= 1
862 position -= 1
872 cursor = self._control.textCursor()
863 cursor = self._control.textCursor()
873 cursor.setPosition(position + 1)
864 cursor.setPosition(position + 1)
874 return cursor
865 return cursor
875
866
876 def _get_word_end_cursor(self, position):
867 def _get_word_end_cursor(self, position):
877 """ Find the end of the word to the right the given position. If a
868 """ Find the end of the word to the right the given position. If a
878 sequence of non-word characters precedes the first word, skip over
869 sequence of non-word characters precedes the first word, skip over
879 them. (This emulates the behavior of bash, emacs, etc.)
870 them. (This emulates the behavior of bash, emacs, etc.)
880 """
871 """
881 document = self._control.document()
872 document = self._control.document()
882 end = self._get_end_cursor().position()
873 end = self._get_end_cursor().position()
883 while position < end and \
874 while position < end and \
884 not document.characterAt(position).isLetterOrNumber():
875 not document.characterAt(position).isLetterOrNumber():
885 position += 1
876 position += 1
886 while position < end and \
877 while position < end and \
887 document.characterAt(position).isLetterOrNumber():
878 document.characterAt(position).isLetterOrNumber():
888 position += 1
879 position += 1
889 cursor = self._control.textCursor()
880 cursor = self._control.textCursor()
890 cursor.setPosition(position)
881 cursor.setPosition(position)
891 return cursor
882 return cursor
892
883
893 def _insert_html(self, cursor, html):
884 def _insert_html(self, cursor, html):
894 """ Inserts HTML using the specified cursor in such a way that future
885 """ Inserts HTML using the specified cursor in such a way that future
895 formatting is unaffected.
886 formatting is unaffected.
896 """
887 """
897 cursor.beginEditBlock()
888 cursor.beginEditBlock()
898 cursor.insertHtml(html)
889 cursor.insertHtml(html)
899
890
900 # After inserting HTML, the text document "remembers" it's in "html
891 # After inserting HTML, the text document "remembers" it's in "html
901 # mode", which means that subsequent calls adding plain text will result
892 # mode", which means that subsequent calls adding plain text will result
902 # in unwanted formatting, lost tab characters, etc. The following code
893 # in unwanted formatting, lost tab characters, etc. The following code
903 # hacks around this behavior, which I consider to be a bug in Qt, by
894 # hacks around this behavior, which I consider to be a bug in Qt, by
904 # (crudely) resetting the document's style state.
895 # (crudely) resetting the document's style state.
905 cursor.movePosition(QtGui.QTextCursor.Left,
896 cursor.movePosition(QtGui.QTextCursor.Left,
906 QtGui.QTextCursor.KeepAnchor)
897 QtGui.QTextCursor.KeepAnchor)
907 if cursor.selection().toPlainText() == ' ':
898 if cursor.selection().toPlainText() == ' ':
908 cursor.removeSelectedText()
899 cursor.removeSelectedText()
909 else:
900 else:
910 cursor.movePosition(QtGui.QTextCursor.Right)
901 cursor.movePosition(QtGui.QTextCursor.Right)
911 cursor.insertText(' ', QtGui.QTextCharFormat())
902 cursor.insertText(' ', QtGui.QTextCharFormat())
912 cursor.endEditBlock()
903 cursor.endEditBlock()
913
904
914 def _insert_html_fetching_plain_text(self, cursor, html):
905 def _insert_html_fetching_plain_text(self, cursor, html):
915 """ Inserts HTML using the specified cursor, then returns its plain text
906 """ Inserts HTML using the specified cursor, then returns its plain text
916 version.
907 version.
917 """
908 """
918 cursor.beginEditBlock()
909 cursor.beginEditBlock()
919 cursor.removeSelectedText()
910 cursor.removeSelectedText()
920
911
921 start = cursor.position()
912 start = cursor.position()
922 self._insert_html(cursor, html)
913 self._insert_html(cursor, html)
923 end = cursor.position()
914 end = cursor.position()
924 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
915 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
925 text = str(cursor.selection().toPlainText())
916 text = str(cursor.selection().toPlainText())
926
917
927 cursor.setPosition(end)
918 cursor.setPosition(end)
928 cursor.endEditBlock()
919 cursor.endEditBlock()
929 return text
920 return text
930
921
931 def _insert_plain_text(self, cursor, text):
922 def _insert_plain_text(self, cursor, text):
932 """ Inserts plain text using the specified cursor, processing ANSI codes
923 """ Inserts plain text using the specified cursor, processing ANSI codes
933 if enabled.
924 if enabled.
934 """
925 """
935 cursor.beginEditBlock()
926 cursor.beginEditBlock()
936 if self.ansi_codes:
927 if self.ansi_codes:
937 for substring in self._ansi_processor.split_string(text):
928 for substring in self._ansi_processor.split_string(text):
938 for action in self._ansi_processor.actions:
929 for action in self._ansi_processor.actions:
939 if action.kind == 'erase' and action.area == 'screen':
930 if action.kind == 'erase' and action.area == 'screen':
940 cursor.select(QtGui.QTextCursor.Document)
931 cursor.select(QtGui.QTextCursor.Document)
941 cursor.removeSelectedText()
932 cursor.removeSelectedText()
942 format = self._ansi_processor.get_format()
933 format = self._ansi_processor.get_format()
943 cursor.insertText(substring, format)
934 cursor.insertText(substring, format)
944 else:
935 else:
945 cursor.insertText(text)
936 cursor.insertText(text)
946 cursor.endEditBlock()
937 cursor.endEditBlock()
947
938
948 def _insert_plain_text_into_buffer(self, text):
939 def _insert_plain_text_into_buffer(self, text):
949 """ Inserts text into the input buffer at the current cursor position,
940 """ Inserts text into the input buffer at the current cursor position,
950 ensuring that continuation prompts are inserted as necessary.
941 ensuring that continuation prompts are inserted as necessary.
951 """
942 """
952 lines = str(text).splitlines(True)
943 lines = str(text).splitlines(True)
953 if lines:
944 if lines:
954 self._keep_cursor_in_buffer()
945 self._keep_cursor_in_buffer()
955 cursor = self._control.textCursor()
946 cursor = self._control.textCursor()
956 cursor.beginEditBlock()
947 cursor.beginEditBlock()
957 cursor.insertText(lines[0])
948 cursor.insertText(lines[0])
958 for line in lines[1:]:
949 for line in lines[1:]:
959 if self._continuation_prompt_html is None:
950 if self._continuation_prompt_html is None:
960 cursor.insertText(self._continuation_prompt)
951 cursor.insertText(self._continuation_prompt)
961 else:
952 else:
962 self._continuation_prompt = \
953 self._continuation_prompt = \
963 self._insert_html_fetching_plain_text(
954 self._insert_html_fetching_plain_text(
964 cursor, self._continuation_prompt_html)
955 cursor, self._continuation_prompt_html)
965 cursor.insertText(line)
956 cursor.insertText(line)
966 cursor.endEditBlock()
957 cursor.endEditBlock()
967 self._control.setTextCursor(cursor)
958 self._control.setTextCursor(cursor)
968
959
969 def _in_buffer(self, position=None):
960 def _in_buffer(self, position=None):
970 """ Returns whether the current cursor (or, if specified, a position) is
961 """ Returns whether the current cursor (or, if specified, a position) is
971 inside the editing region.
962 inside the editing region.
972 """
963 """
973 cursor = self._control.textCursor()
964 cursor = self._control.textCursor()
974 if position is None:
965 if position is None:
975 position = cursor.position()
966 position = cursor.position()
976 else:
967 else:
977 cursor.setPosition(position)
968 cursor.setPosition(position)
978 line = cursor.blockNumber()
969 line = cursor.blockNumber()
979 prompt_line = self._get_prompt_cursor().blockNumber()
970 prompt_line = self._get_prompt_cursor().blockNumber()
980 if line == prompt_line:
971 if line == prompt_line:
981 return position >= self._prompt_pos
972 return position >= self._prompt_pos
982 elif line > prompt_line:
973 elif line > prompt_line:
983 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
974 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
984 prompt_pos = cursor.position() + len(self._continuation_prompt)
975 prompt_pos = cursor.position() + len(self._continuation_prompt)
985 return position >= prompt_pos
976 return position >= prompt_pos
986 return False
977 return False
987
978
988 def _keep_cursor_in_buffer(self):
979 def _keep_cursor_in_buffer(self):
989 """ Ensures that the cursor is inside the editing region. Returns
980 """ Ensures that the cursor is inside the editing region. Returns
990 whether the cursor was moved.
981 whether the cursor was moved.
991 """
982 """
992 moved = not self._in_buffer()
983 moved = not self._in_buffer()
993 if moved:
984 if moved:
994 cursor = self._control.textCursor()
985 cursor = self._control.textCursor()
995 cursor.movePosition(QtGui.QTextCursor.End)
986 cursor.movePosition(QtGui.QTextCursor.End)
996 self._control.setTextCursor(cursor)
987 self._control.setTextCursor(cursor)
997 return moved
988 return moved
998
989
999 def _page(self, text):
990 def _page(self, text):
1000 """ Displays text using the pager if it exceeds the height of the
991 """ Displays text using the pager if it exceeds the height of the
1001 visible area.
992 visible area.
1002 """
993 """
1003 if self._page_style == 'none':
994 if self._page_style == 'none':
1004 self._append_plain_text(text)
995 self._append_plain_text(text)
1005 else:
996 else:
1006 line_height = QtGui.QFontMetrics(self.font).height()
997 line_height = QtGui.QFontMetrics(self.font).height()
1007 minlines = self._control.viewport().height() / line_height
998 minlines = self._control.viewport().height() / line_height
1008 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
999 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1009 if self._page_style == 'custom':
1000 if self._page_style == 'custom':
1010 self.custom_page_requested.emit(text)
1001 self.custom_page_requested.emit(text)
1011 else:
1002 else:
1012 self._page_control.clear()
1003 self._page_control.clear()
1013 cursor = self._page_control.textCursor()
1004 cursor = self._page_control.textCursor()
1014 self._insert_plain_text(cursor, text)
1005 self._insert_plain_text(cursor, text)
1015 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1006 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1016
1007
1017 self._page_control.viewport().resize(self._control.size())
1008 self._page_control.viewport().resize(self._control.size())
1018 if self._splitter:
1009 if self._splitter:
1019 self._page_control.show()
1010 self._page_control.show()
1020 self._page_control.setFocus()
1011 self._page_control.setFocus()
1021 else:
1012 else:
1022 self.layout().setCurrentWidget(self._page_control)
1013 self.layout().setCurrentWidget(self._page_control)
1023 else:
1014 else:
1024 self._append_plain_text(text)
1015 self._append_plain_text(text)
1025
1016
1026 def _prompt_started(self):
1017 def _prompt_started(self):
1027 """ Called immediately after a new prompt is displayed.
1018 """ Called immediately after a new prompt is displayed.
1028 """
1019 """
1029 # Temporarily disable the maximum block count to permit undo/redo and
1020 # Temporarily disable the maximum block count to permit undo/redo and
1030 # to ensure that the prompt position does not change due to truncation.
1021 # to ensure that the prompt position does not change due to truncation.
1031 self._control.document().setMaximumBlockCount(0)
1022 self._control.document().setMaximumBlockCount(0)
1032 self._control.setUndoRedoEnabled(True)
1023 self._control.setUndoRedoEnabled(True)
1033
1024
1034 self._control.setReadOnly(False)
1025 self._control.setReadOnly(False)
1035 self._control.moveCursor(QtGui.QTextCursor.End)
1026 self._control.moveCursor(QtGui.QTextCursor.End)
1036
1027
1037 self._executing = False
1028 self._executing = False
1038 self._prompt_started_hook()
1029 self._prompt_started_hook()
1039
1030
1040 def _prompt_finished(self):
1031 def _prompt_finished(self):
1041 """ Called immediately after a prompt is finished, i.e. when some input
1032 """ Called immediately after a prompt is finished, i.e. when some input
1042 will be processed and a new prompt displayed.
1033 will be processed and a new prompt displayed.
1043 """
1034 """
1044 self._control.setUndoRedoEnabled(False)
1035 self._control.setUndoRedoEnabled(False)
1045 self._control.setReadOnly(True)
1036 self._control.setReadOnly(True)
1046 self._prompt_finished_hook()
1037 self._prompt_finished_hook()
1047
1038
1048 def _readline(self, prompt='', callback=None):
1039 def _readline(self, prompt='', callback=None):
1049 """ Reads one line of input from the user.
1040 """ Reads one line of input from the user.
1050
1041
1051 Parameters
1042 Parameters
1052 ----------
1043 ----------
1053 prompt : str, optional
1044 prompt : str, optional
1054 The prompt to print before reading the line.
1045 The prompt to print before reading the line.
1055
1046
1056 callback : callable, optional
1047 callback : callable, optional
1057 A callback to execute with the read line. If not specified, input is
1048 A callback to execute with the read line. If not specified, input is
1058 read *synchronously* and this method does not return until it has
1049 read *synchronously* and this method does not return until it has
1059 been read.
1050 been read.
1060
1051
1061 Returns
1052 Returns
1062 -------
1053 -------
1063 If a callback is specified, returns nothing. Otherwise, returns the
1054 If a callback is specified, returns nothing. Otherwise, returns the
1064 input string with the trailing newline stripped.
1055 input string with the trailing newline stripped.
1065 """
1056 """
1066 if self._reading:
1057 if self._reading:
1067 raise RuntimeError('Cannot read a line. Widget is already reading.')
1058 raise RuntimeError('Cannot read a line. Widget is already reading.')
1068
1059
1069 if not callback and not self.isVisible():
1060 if not callback and not self.isVisible():
1070 # If the user cannot see the widget, this function cannot return.
1061 # If the user cannot see the widget, this function cannot return.
1071 raise RuntimeError('Cannot synchronously read a line if the widget'
1062 raise RuntimeError('Cannot synchronously read a line if the widget'
1072 'is not visible!')
1063 'is not visible!')
1073
1064
1074 self._reading = True
1065 self._reading = True
1075 self._show_prompt(prompt, newline=False)
1066 self._show_prompt(prompt, newline=False)
1076
1067
1077 if callback is None:
1068 if callback is None:
1078 self._reading_callback = None
1069 self._reading_callback = None
1079 while self._reading:
1070 while self._reading:
1080 QtCore.QCoreApplication.processEvents()
1071 QtCore.QCoreApplication.processEvents()
1081 return self.input_buffer.rstrip('\n')
1072 return self.input_buffer.rstrip('\n')
1082
1073
1083 else:
1074 else:
1084 self._reading_callback = lambda: \
1075 self._reading_callback = lambda: \
1085 callback(self.input_buffer.rstrip('\n'))
1076 callback(self.input_buffer.rstrip('\n'))
1086
1077
1087 def _set_continuation_prompt(self, prompt, html=False):
1078 def _set_continuation_prompt(self, prompt, html=False):
1088 """ Sets the continuation prompt.
1079 """ Sets the continuation prompt.
1089
1080
1090 Parameters
1081 Parameters
1091 ----------
1082 ----------
1092 prompt : str
1083 prompt : str
1093 The prompt to show when more input is needed.
1084 The prompt to show when more input is needed.
1094
1085
1095 html : bool, optional (default False)
1086 html : bool, optional (default False)
1096 If set, the prompt will be inserted as formatted HTML. Otherwise,
1087 If set, the prompt will be inserted as formatted HTML. Otherwise,
1097 the prompt will be treated as plain text, though ANSI color codes
1088 the prompt will be treated as plain text, though ANSI color codes
1098 will be handled.
1089 will be handled.
1099 """
1090 """
1100 if html:
1091 if html:
1101 self._continuation_prompt_html = prompt
1092 self._continuation_prompt_html = prompt
1102 else:
1093 else:
1103 self._continuation_prompt = prompt
1094 self._continuation_prompt = prompt
1104 self._continuation_prompt_html = None
1095 self._continuation_prompt_html = None
1105
1096
1106 def _set_cursor(self, cursor):
1097 def _set_cursor(self, cursor):
1107 """ Convenience method to set the current cursor.
1098 """ Convenience method to set the current cursor.
1108 """
1099 """
1109 self._control.setTextCursor(cursor)
1100 self._control.setTextCursor(cursor)
1110
1101
1111 def _set_position(self, position):
1102 def _set_position(self, position):
1112 """ Convenience method to set the position of the cursor.
1103 """ Convenience method to set the position of the cursor.
1113 """
1104 """
1114 cursor = self._control.textCursor()
1105 cursor = self._control.textCursor()
1115 cursor.setPosition(position)
1106 cursor.setPosition(position)
1116 self._control.setTextCursor(cursor)
1107 self._control.setTextCursor(cursor)
1117
1108
1118 def _set_selection(self, start, end):
1109 def _set_selection(self, start, end):
1119 """ Convenience method to set the current selected text.
1110 """ Convenience method to set the current selected text.
1120 """
1111 """
1121 self._control.setTextCursor(self._get_selection_cursor(start, end))
1112 self._control.setTextCursor(self._get_selection_cursor(start, end))
1122
1113
1123 def _show_context_menu(self, pos):
1114 def _show_context_menu(self, pos):
1124 """ Shows a context menu at the given QPoint (in widget coordinates).
1115 """ Shows a context menu at the given QPoint (in widget coordinates).
1125 """
1116 """
1126 menu = QtGui.QMenu()
1117 menu = QtGui.QMenu()
1127
1118
1128 copy_action = menu.addAction('Copy', self.copy)
1119 copy_action = menu.addAction('Copy', self.copy)
1129 copy_action.setEnabled(self._get_cursor().hasSelection())
1120 copy_action.setEnabled(self._get_cursor().hasSelection())
1130 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1121 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1131
1122
1132 paste_action = menu.addAction('Paste', self.paste)
1123 paste_action = menu.addAction('Paste', self.paste)
1133 paste_action.setEnabled(self.can_paste())
1124 paste_action.setEnabled(self.can_paste())
1134 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1125 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1135
1126
1136 menu.addSeparator()
1127 menu.addSeparator()
1137 menu.addAction('Select All', self.select_all)
1128 menu.addAction('Select All', self.select_all)
1138
1129
1139 menu.exec_(self._control.mapToGlobal(pos))
1130 menu.exec_(self._control.mapToGlobal(pos))
1140
1131
1141 def _show_prompt(self, prompt=None, html=False, newline=True):
1132 def _show_prompt(self, prompt=None, html=False, newline=True):
1142 """ Writes a new prompt at the end of the buffer.
1133 """ Writes a new prompt at the end of the buffer.
1143
1134
1144 Parameters
1135 Parameters
1145 ----------
1136 ----------
1146 prompt : str, optional
1137 prompt : str, optional
1147 The prompt to show. If not specified, the previous prompt is used.
1138 The prompt to show. If not specified, the previous prompt is used.
1148
1139
1149 html : bool, optional (default False)
1140 html : bool, optional (default False)
1150 Only relevant when a prompt is specified. If set, the prompt will
1141 Only relevant when a prompt is specified. If set, the prompt will
1151 be inserted as formatted HTML. Otherwise, the prompt will be treated
1142 be inserted as formatted HTML. Otherwise, the prompt will be treated
1152 as plain text, though ANSI color codes will be handled.
1143 as plain text, though ANSI color codes will be handled.
1153
1144
1154 newline : bool, optional (default True)
1145 newline : bool, optional (default True)
1155 If set, a new line will be written before showing the prompt if
1146 If set, a new line will be written before showing the prompt if
1156 there is not already a newline at the end of the buffer.
1147 there is not already a newline at the end of the buffer.
1157 """
1148 """
1158 # Insert a preliminary newline, if necessary.
1149 # Insert a preliminary newline, if necessary.
1159 if newline:
1150 if newline:
1160 cursor = self._get_end_cursor()
1151 cursor = self._get_end_cursor()
1161 if cursor.position() > 0:
1152 if cursor.position() > 0:
1162 cursor.movePosition(QtGui.QTextCursor.Left,
1153 cursor.movePosition(QtGui.QTextCursor.Left,
1163 QtGui.QTextCursor.KeepAnchor)
1154 QtGui.QTextCursor.KeepAnchor)
1164 if str(cursor.selection().toPlainText()) != '\n':
1155 if str(cursor.selection().toPlainText()) != '\n':
1165 self._append_plain_text('\n')
1156 self._append_plain_text('\n')
1166
1157
1167 # Write the prompt.
1158 # Write the prompt.
1168 if prompt is None:
1159 if prompt is None:
1169 if self._prompt_html is None:
1160 if self._prompt_html is None:
1170 self._append_plain_text(self._prompt)
1161 self._append_plain_text(self._prompt)
1171 else:
1162 else:
1172 self._append_html(self._prompt_html)
1163 self._append_html(self._prompt_html)
1173 else:
1164 else:
1174 if html:
1165 if html:
1175 self._prompt = self._append_html_fetching_plain_text(prompt)
1166 self._prompt = self._append_html_fetching_plain_text(prompt)
1176 self._prompt_html = prompt
1167 self._prompt_html = prompt
1177 else:
1168 else:
1178 self._append_plain_text(prompt)
1169 self._append_plain_text(prompt)
1179 self._prompt = prompt
1170 self._prompt = prompt
1180 self._prompt_html = None
1171 self._prompt_html = None
1181
1172
1182 self._prompt_pos = self._get_end_cursor().position()
1173 self._prompt_pos = self._get_end_cursor().position()
1183 self._prompt_started()
1174 self._prompt_started()
1184
1175
1185 def _show_continuation_prompt(self):
1176 def _show_continuation_prompt(self):
1186 """ Writes a new continuation prompt at the end of the buffer.
1177 """ Writes a new continuation prompt at the end of the buffer.
1187 """
1178 """
1188 if self._continuation_prompt_html is None:
1179 if self._continuation_prompt_html is None:
1189 self._append_plain_text(self._continuation_prompt)
1180 self._append_plain_text(self._continuation_prompt)
1190 else:
1181 else:
1191 self._continuation_prompt = self._append_html_fetching_plain_text(
1182 self._continuation_prompt = self._append_html_fetching_plain_text(
1192 self._continuation_prompt_html)
1183 self._continuation_prompt_html)
1193
1184
1194 self._prompt_started()
1185 self._prompt_started()
1195
1186
1196
1187
1197 class HistoryConsoleWidget(ConsoleWidget):
1188 class HistoryConsoleWidget(ConsoleWidget):
1198 """ A ConsoleWidget that keeps a history of the commands that have been
1189 """ A ConsoleWidget that keeps a history of the commands that have been
1199 executed.
1190 executed.
1200 """
1191 """
1201
1192
1202 #---------------------------------------------------------------------------
1193 #---------------------------------------------------------------------------
1203 # 'object' interface
1194 # 'object' interface
1204 #---------------------------------------------------------------------------
1195 #---------------------------------------------------------------------------
1205
1196
1206 def __init__(self, *args, **kw):
1197 def __init__(self, *args, **kw):
1207 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1198 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1208 self._history = []
1199 self._history = []
1209 self._history_index = 0
1200 self._history_index = 0
1210
1201
1211 #---------------------------------------------------------------------------
1202 #---------------------------------------------------------------------------
1212 # 'ConsoleWidget' public interface
1203 # 'ConsoleWidget' public interface
1213 #---------------------------------------------------------------------------
1204 #---------------------------------------------------------------------------
1214
1205
1215 def execute(self, source=None, hidden=False, interactive=False):
1206 def execute(self, source=None, hidden=False, interactive=False):
1216 """ Reimplemented to the store history.
1207 """ Reimplemented to the store history.
1217 """
1208 """
1218 if not hidden:
1209 if not hidden:
1219 history = self.input_buffer if source is None else source
1210 history = self.input_buffer if source is None else source
1220
1211
1221 executed = super(HistoryConsoleWidget, self).execute(
1212 executed = super(HistoryConsoleWidget, self).execute(
1222 source, hidden, interactive)
1213 source, hidden, interactive)
1223
1214
1224 if executed and not hidden:
1215 if executed and not hidden:
1225 self._history.append(history.rstrip())
1216 self._history.append(history.rstrip())
1226 self._history_index = len(self._history)
1217 self._history_index = len(self._history)
1227
1218
1228 return executed
1219 return executed
1229
1220
1230 #---------------------------------------------------------------------------
1221 #---------------------------------------------------------------------------
1231 # 'ConsoleWidget' abstract interface
1222 # 'ConsoleWidget' abstract interface
1232 #---------------------------------------------------------------------------
1223 #---------------------------------------------------------------------------
1233
1224
1234 def _up_pressed(self):
1225 def _up_pressed(self):
1235 """ Called when the up key is pressed. Returns whether to continue
1226 """ Called when the up key is pressed. Returns whether to continue
1236 processing the event.
1227 processing the event.
1237 """
1228 """
1238 prompt_cursor = self._get_prompt_cursor()
1229 prompt_cursor = self._get_prompt_cursor()
1239 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1230 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1240 self.history_previous()
1231 self.history_previous()
1241
1232
1242 # Go to the first line of prompt for seemless history scrolling.
1233 # Go to the first line of prompt for seemless history scrolling.
1243 cursor = self._get_prompt_cursor()
1234 cursor = self._get_prompt_cursor()
1244 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1235 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1245 self._set_cursor(cursor)
1236 self._set_cursor(cursor)
1246
1237
1247 return False
1238 return False
1248 return True
1239 return True
1249
1240
1250 def _down_pressed(self):
1241 def _down_pressed(self):
1251 """ Called when the down key is pressed. Returns whether to continue
1242 """ Called when the down key is pressed. Returns whether to continue
1252 processing the event.
1243 processing the event.
1253 """
1244 """
1254 end_cursor = self._get_end_cursor()
1245 end_cursor = self._get_end_cursor()
1255 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1246 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1256 self.history_next()
1247 self.history_next()
1257 return False
1248 return False
1258 return True
1249 return True
1259
1250
1260 #---------------------------------------------------------------------------
1251 #---------------------------------------------------------------------------
1261 # 'HistoryConsoleWidget' public interface
1252 # 'HistoryConsoleWidget' public interface
1262 #---------------------------------------------------------------------------
1253 #---------------------------------------------------------------------------
1263
1254
1264 def history_previous(self):
1255 def history_previous(self):
1265 """ If possible, set the input buffer to the previous item in the
1256 """ If possible, set the input buffer to the previous item in the
1266 history.
1257 history.
1267 """
1258 """
1268 if self._history_index > 0:
1259 if self._history_index > 0:
1269 self._history_index -= 1
1260 self._history_index -= 1
1270 self.input_buffer = self._history[self._history_index]
1261 self.input_buffer = self._history[self._history_index]
1271
1262
1272 def history_next(self):
1263 def history_next(self):
1273 """ Set the input buffer to the next item in the history, or a blank
1264 """ Set the input buffer to the next item in the history, or a blank
1274 line if there is no subsequent item.
1265 line if there is no subsequent item.
1275 """
1266 """
1276 if self._history_index < len(self._history):
1267 if self._history_index < len(self._history):
1277 self._history_index += 1
1268 self._history_index += 1
1278 if self._history_index < len(self._history):
1269 if self._history_index < len(self._history):
1279 self.input_buffer = self._history[self._history_index]
1270 self.input_buffer = self._history[self._history_index]
1280 else:
1271 else:
1281 self.input_buffer = ''
1272 self.input_buffer = ''
1282
1273
1283 #---------------------------------------------------------------------------
1274 #---------------------------------------------------------------------------
1284 # 'HistoryConsoleWidget' protected interface
1275 # 'HistoryConsoleWidget' protected interface
1285 #---------------------------------------------------------------------------
1276 #---------------------------------------------------------------------------
1286
1277
1287 def _set_history(self, history):
1278 def _set_history(self, history):
1288 """ Replace the current history with a sequence of history items.
1279 """ Replace the current history with a sequence of history items.
1289 """
1280 """
1290 self._history = list(history)
1281 self._history = list(history)
1291 self._history_index = len(self._history)
1282 self._history_index = len(self._history)
@@ -1,386 +1,423 b''
1 # Standard library imports
1 # Standard library imports
2 import signal
2 import signal
3 import sys
3 import sys
4
4
5 # System library imports
5 # System library imports
6 from pygments.lexers import PythonLexer
6 from pygments.lexers import PythonLexer
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8 import zmq
8 import zmq
9
9
10 # Local imports
10 # Local imports
11 from IPython.core.inputsplitter import InputSplitter
11 from IPython.core.inputsplitter import InputSplitter
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
13 from call_tip_widget import CallTipWidget
13 from call_tip_widget import CallTipWidget
14 from completion_lexer import CompletionLexer
14 from completion_lexer import CompletionLexer
15 from console_widget import HistoryConsoleWidget
15 from console_widget import HistoryConsoleWidget
16 from pygments_highlighter import PygmentsHighlighter
16 from pygments_highlighter import PygmentsHighlighter
17
17
18
18
19 class FrontendHighlighter(PygmentsHighlighter):
19 class FrontendHighlighter(PygmentsHighlighter):
20 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 prompts.
21 prompts.
22 """
22 """
23
23
24 def __init__(self, frontend):
24 def __init__(self, frontend):
25 super(FrontendHighlighter, self).__init__(frontend._control.document())
25 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 self._current_offset = 0
26 self._current_offset = 0
27 self._frontend = frontend
27 self._frontend = frontend
28 self.highlighting_on = False
28 self.highlighting_on = False
29
29
30 def highlightBlock(self, qstring):
30 def highlightBlock(self, qstring):
31 """ Highlight a block of text. Reimplemented to highlight selectively.
31 """ Highlight a block of text. Reimplemented to highlight selectively.
32 """
32 """
33 if not self.highlighting_on:
33 if not self.highlighting_on:
34 return
34 return
35
35
36 # The input to this function is unicode string that may contain
36 # The input to this function is unicode string that may contain
37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 # the string as plain text so we can compare it.
38 # the string as plain text so we can compare it.
39 current_block = self.currentBlock()
39 current_block = self.currentBlock()
40 string = self._frontend._get_block_plain_text(current_block)
40 string = self._frontend._get_block_plain_text(current_block)
41
41
42 # Decide whether to check for the regular or continuation prompt.
42 # Decide whether to check for the regular or continuation prompt.
43 if current_block.contains(self._frontend._prompt_pos):
43 if current_block.contains(self._frontend._prompt_pos):
44 prompt = self._frontend._prompt
44 prompt = self._frontend._prompt
45 else:
45 else:
46 prompt = self._frontend._continuation_prompt
46 prompt = self._frontend._continuation_prompt
47
47
48 # Don't highlight the part of the string that contains the prompt.
48 # Don't highlight the part of the string that contains the prompt.
49 if string.startswith(prompt):
49 if string.startswith(prompt):
50 self._current_offset = len(prompt)
50 self._current_offset = len(prompt)
51 qstring.remove(0, len(prompt))
51 qstring.remove(0, len(prompt))
52 else:
52 else:
53 self._current_offset = 0
53 self._current_offset = 0
54
54
55 PygmentsHighlighter.highlightBlock(self, qstring)
55 PygmentsHighlighter.highlightBlock(self, qstring)
56
56
57 def rehighlightBlock(self, block):
57 def rehighlightBlock(self, block):
58 """ Reimplemented to temporarily enable highlighting if disabled.
58 """ Reimplemented to temporarily enable highlighting if disabled.
59 """
59 """
60 old = self.highlighting_on
60 old = self.highlighting_on
61 self.highlighting_on = True
61 self.highlighting_on = True
62 super(FrontendHighlighter, self).rehighlightBlock(block)
62 super(FrontendHighlighter, self).rehighlightBlock(block)
63 self.highlighting_on = old
63 self.highlighting_on = old
64
64
65 def setFormat(self, start, count, format):
65 def setFormat(self, start, count, format):
66 """ Reimplemented to highlight selectively.
66 """ Reimplemented to highlight selectively.
67 """
67 """
68 start += self._current_offset
68 start += self._current_offset
69 PygmentsHighlighter.setFormat(self, start, count, format)
69 PygmentsHighlighter.setFormat(self, start, count, format)
70
70
71
71
72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
73 """ A Qt frontend for a generic Python kernel.
73 """ A Qt frontend for a generic Python kernel.
74 """
74 """
75
76 # An option and corresponding signal for overriding the default kernel
77 # interrupt behavior.
78 custom_interrupt = False
79 custom_interrupt_requested = QtCore.pyqtSignal()
80
81 # An option and corresponding signal for overriding the default kernel
82 # restart behavior.
83 custom_restart = False
84 custom_restart_requested = QtCore.pyqtSignal()
75
85
76 # Emitted when an 'execute_reply' has been received from the kernel and
86 # Emitted when an 'execute_reply' has been received from the kernel and
77 # processed by the FrontendWidget.
87 # processed by the FrontendWidget.
78 executed = QtCore.pyqtSignal(object)
88 executed = QtCore.pyqtSignal(object)
79
89
80 # Protected class attributes.
90 # Protected class variables.
81 _highlighter_class = FrontendHighlighter
91 _highlighter_class = FrontendHighlighter
82 _input_splitter_class = InputSplitter
92 _input_splitter_class = InputSplitter
83
93
84 #---------------------------------------------------------------------------
94 #---------------------------------------------------------------------------
85 # 'object' interface
95 # 'object' interface
86 #---------------------------------------------------------------------------
96 #---------------------------------------------------------------------------
87
97
88 def __init__(self, *args, **kw):
98 def __init__(self, *args, **kw):
89 super(FrontendWidget, self).__init__(*args, **kw)
99 super(FrontendWidget, self).__init__(*args, **kw)
90
100
91 # FrontendWidget protected variables.
101 # FrontendWidget protected variables.
92 self._call_tip_widget = CallTipWidget(self._control)
102 self._call_tip_widget = CallTipWidget(self._control)
93 self._completion_lexer = CompletionLexer(PythonLexer())
103 self._completion_lexer = CompletionLexer(PythonLexer())
94 self._hidden = False
104 self._hidden = False
95 self._highlighter = self._highlighter_class(self)
105 self._highlighter = self._highlighter_class(self)
96 self._input_splitter = self._input_splitter_class(input_mode='replace')
106 self._input_splitter = self._input_splitter_class(input_mode='replace')
97 self._kernel_manager = None
107 self._kernel_manager = None
98
108
99 # Configure the ConsoleWidget.
109 # Configure the ConsoleWidget.
100 self.tab_width = 4
110 self.tab_width = 4
101 self._set_continuation_prompt('... ')
111 self._set_continuation_prompt('... ')
102
112
103 # Connect signal handlers.
113 # Connect signal handlers.
104 document = self._control.document()
114 document = self._control.document()
105 document.contentsChange.connect(self._document_contents_change)
115 document.contentsChange.connect(self._document_contents_change)
106
116
107 #---------------------------------------------------------------------------
117 #---------------------------------------------------------------------------
108 # 'ConsoleWidget' abstract interface
118 # 'ConsoleWidget' abstract interface
109 #---------------------------------------------------------------------------
119 #---------------------------------------------------------------------------
110
120
111 def _is_complete(self, source, interactive):
121 def _is_complete(self, source, interactive):
112 """ Returns whether 'source' can be completely processed and a new
122 """ Returns whether 'source' can be completely processed and a new
113 prompt created. When triggered by an Enter/Return key press,
123 prompt created. When triggered by an Enter/Return key press,
114 'interactive' is True; otherwise, it is False.
124 'interactive' is True; otherwise, it is False.
115 """
125 """
116 complete = self._input_splitter.push(source.expandtabs(4))
126 complete = self._input_splitter.push(source.expandtabs(4))
117 if interactive:
127 if interactive:
118 complete = not self._input_splitter.push_accepts_more()
128 complete = not self._input_splitter.push_accepts_more()
119 return complete
129 return complete
120
130
121 def _execute(self, source, hidden):
131 def _execute(self, source, hidden):
122 """ Execute 'source'. If 'hidden', do not show any output.
132 """ Execute 'source'. If 'hidden', do not show any output.
123 """
133 """
124 self.kernel_manager.xreq_channel.execute(source, hidden)
134 self.kernel_manager.xreq_channel.execute(source, hidden)
125 self._hidden = hidden
135 self._hidden = hidden
126
127 def _execute_interrupt(self):
128 """ Attempts to stop execution. Returns whether this method has an
129 implementation.
130 """
131 self._interrupt_kernel()
132 return True
133
136
134 def _prompt_started_hook(self):
137 def _prompt_started_hook(self):
135 """ Called immediately after a new prompt is displayed.
138 """ Called immediately after a new prompt is displayed.
136 """
139 """
137 if not self._reading:
140 if not self._reading:
138 self._highlighter.highlighting_on = True
141 self._highlighter.highlighting_on = True
139
142
140 def _prompt_finished_hook(self):
143 def _prompt_finished_hook(self):
141 """ Called immediately after a prompt is finished, i.e. when some input
144 """ Called immediately after a prompt is finished, i.e. when some input
142 will be processed and a new prompt displayed.
145 will be processed and a new prompt displayed.
143 """
146 """
144 if not self._reading:
147 if not self._reading:
145 self._highlighter.highlighting_on = False
148 self._highlighter.highlighting_on = False
146
149
147 def _tab_pressed(self):
150 def _tab_pressed(self):
148 """ Called when the tab key is pressed. Returns whether to continue
151 """ Called when the tab key is pressed. Returns whether to continue
149 processing the event.
152 processing the event.
150 """
153 """
151 # Perform tab completion if:
154 # Perform tab completion if:
152 # 1) The cursor is in the input buffer.
155 # 1) The cursor is in the input buffer.
153 # 2) There is a non-whitespace character before the cursor.
156 # 2) There is a non-whitespace character before the cursor.
154 text = self._get_input_buffer_cursor_line()
157 text = self._get_input_buffer_cursor_line()
155 if text is None:
158 if text is None:
156 return False
159 return False
157 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
160 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
158 if complete:
161 if complete:
159 self._complete()
162 self._complete()
160 return not complete
163 return not complete
161
164
162 #---------------------------------------------------------------------------
165 #---------------------------------------------------------------------------
163 # 'ConsoleWidget' protected interface
166 # 'ConsoleWidget' protected interface
164 #---------------------------------------------------------------------------
167 #---------------------------------------------------------------------------
165
168
169 def _event_filter_console_keypress(self, event):
170 """ Reimplemented to allow execution interruption.
171 """
172 key = event.key()
173 if self._executing and self._control_key_down(event.modifiers()):
174 if key == QtCore.Qt.Key_C:
175 self._kernel_interrupt()
176 return True
177 elif key == QtCore.Qt.Key_Period:
178 self._kernel_restart()
179 return True
180 return super(FrontendWidget, self)._event_filter_console_keypress(event)
181
166 def _show_continuation_prompt(self):
182 def _show_continuation_prompt(self):
167 """ Reimplemented for auto-indentation.
183 """ Reimplemented for auto-indentation.
168 """
184 """
169 super(FrontendWidget, self)._show_continuation_prompt()
185 super(FrontendWidget, self)._show_continuation_prompt()
170 spaces = self._input_splitter.indent_spaces
186 spaces = self._input_splitter.indent_spaces
171 self._append_plain_text('\t' * (spaces / self.tab_width))
187 self._append_plain_text('\t' * (spaces / self.tab_width))
172 self._append_plain_text(' ' * (spaces % self.tab_width))
188 self._append_plain_text(' ' * (spaces % self.tab_width))
173
189
174 #---------------------------------------------------------------------------
190 #---------------------------------------------------------------------------
175 # 'BaseFrontendMixin' abstract interface
191 # 'BaseFrontendMixin' abstract interface
176 #---------------------------------------------------------------------------
192 #---------------------------------------------------------------------------
177
193
178 def _handle_complete_reply(self, rep):
194 def _handle_complete_reply(self, rep):
179 """ Handle replies for tab completion.
195 """ Handle replies for tab completion.
180 """
196 """
181 cursor = self._get_cursor()
197 cursor = self._get_cursor()
182 if rep['parent_header']['msg_id'] == self._complete_id and \
198 if rep['parent_header']['msg_id'] == self._complete_id and \
183 cursor.position() == self._complete_pos:
199 cursor.position() == self._complete_pos:
184 text = '.'.join(self._get_context())
200 text = '.'.join(self._get_context())
185 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
201 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
186 self._complete_with_items(cursor, rep['content']['matches'])
202 self._complete_with_items(cursor, rep['content']['matches'])
187
203
188 def _handle_execute_reply(self, msg):
204 def _handle_execute_reply(self, msg):
189 """ Handles replies for code execution.
205 """ Handles replies for code execution.
190 """
206 """
191 if not self._hidden:
207 if not self._hidden:
192 # Make sure that all output from the SUB channel has been processed
208 # Make sure that all output from the SUB channel has been processed
193 # before writing a new prompt.
209 # before writing a new prompt.
194 self.kernel_manager.sub_channel.flush()
210 self.kernel_manager.sub_channel.flush()
195
211
196 content = msg['content']
212 content = msg['content']
197 status = content['status']
213 status = content['status']
198 if status == 'ok':
214 if status == 'ok':
199 self._process_execute_ok(msg)
215 self._process_execute_ok(msg)
200 elif status == 'error':
216 elif status == 'error':
201 self._process_execute_error(msg)
217 self._process_execute_error(msg)
202 elif status == 'abort':
218 elif status == 'abort':
203 self._process_execute_abort(msg)
219 self._process_execute_abort(msg)
204
220
205 self._show_interpreter_prompt_for_reply(msg)
221 self._show_interpreter_prompt_for_reply(msg)
206 self.executed.emit(msg)
222 self.executed.emit(msg)
207
223
208 def _handle_input_request(self, msg):
224 def _handle_input_request(self, msg):
209 """ Handle requests for raw_input.
225 """ Handle requests for raw_input.
210 """
226 """
211 if self._hidden:
227 if self._hidden:
212 raise RuntimeError('Request for raw input during hidden execution.')
228 raise RuntimeError('Request for raw input during hidden execution.')
213
229
214 # Make sure that all output from the SUB channel has been processed
230 # Make sure that all output from the SUB channel has been processed
215 # before entering readline mode.
231 # before entering readline mode.
216 self.kernel_manager.sub_channel.flush()
232 self.kernel_manager.sub_channel.flush()
217
233
218 def callback(line):
234 def callback(line):
219 self.kernel_manager.rep_channel.input(line)
235 self.kernel_manager.rep_channel.input(line)
220 self._readline(msg['content']['prompt'], callback=callback)
236 self._readline(msg['content']['prompt'], callback=callback)
221
237
222 def _handle_object_info_reply(self, rep):
238 def _handle_object_info_reply(self, rep):
223 """ Handle replies for call tips.
239 """ Handle replies for call tips.
224 """
240 """
225 cursor = self._get_cursor()
241 cursor = self._get_cursor()
226 if rep['parent_header']['msg_id'] == self._call_tip_id and \
242 if rep['parent_header']['msg_id'] == self._call_tip_id and \
227 cursor.position() == self._call_tip_pos:
243 cursor.position() == self._call_tip_pos:
228 doc = rep['content']['docstring']
244 doc = rep['content']['docstring']
229 if doc:
245 if doc:
230 self._call_tip_widget.show_docstring(doc)
246 self._call_tip_widget.show_docstring(doc)
231
247
232 def _handle_pyout(self, msg):
248 def _handle_pyout(self, msg):
233 """ Handle display hook output.
249 """ Handle display hook output.
234 """
250 """
235 if not self._hidden and self._is_from_this_session(msg):
251 if not self._hidden and self._is_from_this_session(msg):
236 self._append_plain_text(msg['content']['data'] + '\n')
252 self._append_plain_text(msg['content']['data'] + '\n')
237
253
238 def _handle_stream(self, msg):
254 def _handle_stream(self, msg):
239 """ Handle stdout, stderr, and stdin.
255 """ Handle stdout, stderr, and stdin.
240 """
256 """
241 if not self._hidden and self._is_from_this_session(msg):
257 if not self._hidden and self._is_from_this_session(msg):
242 self._append_plain_text(msg['content']['data'])
258 self._append_plain_text(msg['content']['data'])
243 self._control.moveCursor(QtGui.QTextCursor.End)
259 self._control.moveCursor(QtGui.QTextCursor.End)
244
260
245 def _started_channels(self):
261 def _started_channels(self):
246 """ Called when the KernelManager channels have started listening or
262 """ Called when the KernelManager channels have started listening or
247 when the frontend is assigned an already listening KernelManager.
263 when the frontend is assigned an already listening KernelManager.
248 """
264 """
249 self._control.clear()
265 self._control.clear()
250 self._append_plain_text(self._get_banner())
266 self._append_plain_text(self._get_banner())
251 self._show_interpreter_prompt()
267 self._show_interpreter_prompt()
252
268
253 def _stopped_channels(self):
269 def _stopped_channels(self):
254 """ Called when the KernelManager channels have stopped listening or
270 """ Called when the KernelManager channels have stopped listening or
255 when a listening KernelManager is removed from the frontend.
271 when a listening KernelManager is removed from the frontend.
256 """
272 """
257 self._executing = self._reading = False
273 self._executing = self._reading = False
258 self._highlighter.highlighting_on = False
274 self._highlighter.highlighting_on = False
259
275
260 #---------------------------------------------------------------------------
276 #---------------------------------------------------------------------------
261 # 'FrontendWidget' interface
277 # 'FrontendWidget' interface
262 #---------------------------------------------------------------------------
278 #---------------------------------------------------------------------------
263
279
264 def execute_file(self, path, hidden=False):
280 def execute_file(self, path, hidden=False):
265 """ Attempts to execute file with 'path'. If 'hidden', no output is
281 """ Attempts to execute file with 'path'. If 'hidden', no output is
266 shown.
282 shown.
267 """
283 """
268 self.execute('execfile("%s")' % path, hidden=hidden)
284 self.execute('execfile("%s")' % path, hidden=hidden)
269
285
270 #---------------------------------------------------------------------------
286 #---------------------------------------------------------------------------
271 # 'FrontendWidget' protected interface
287 # 'FrontendWidget' protected interface
272 #---------------------------------------------------------------------------
288 #---------------------------------------------------------------------------
273
289
274 def _call_tip(self):
290 def _call_tip(self):
275 """ Shows a call tip, if appropriate, at the current cursor location.
291 """ Shows a call tip, if appropriate, at the current cursor location.
276 """
292 """
277 # Decide if it makes sense to show a call tip
293 # Decide if it makes sense to show a call tip
278 cursor = self._get_cursor()
294 cursor = self._get_cursor()
279 cursor.movePosition(QtGui.QTextCursor.Left)
295 cursor.movePosition(QtGui.QTextCursor.Left)
280 document = self._control.document()
296 document = self._control.document()
281 if document.characterAt(cursor.position()).toAscii() != '(':
297 if document.characterAt(cursor.position()).toAscii() != '(':
282 return False
298 return False
283 context = self._get_context(cursor)
299 context = self._get_context(cursor)
284 if not context:
300 if not context:
285 return False
301 return False
286
302
287 # Send the metadata request to the kernel
303 # Send the metadata request to the kernel
288 name = '.'.join(context)
304 name = '.'.join(context)
289 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
305 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
290 self._call_tip_pos = self._get_cursor().position()
306 self._call_tip_pos = self._get_cursor().position()
291 return True
307 return True
292
308
293 def _complete(self):
309 def _complete(self):
294 """ Performs completion at the current cursor location.
310 """ Performs completion at the current cursor location.
295 """
311 """
296 # Decide if it makes sense to do completion
312 # Decide if it makes sense to do completion
297 context = self._get_context()
313 context = self._get_context()
298 if not context:
314 if not context:
299 return False
315 return False
300
316
301 # Send the completion request to the kernel
317 # Send the completion request to the kernel
302 self._complete_id = self.kernel_manager.xreq_channel.complete(
318 self._complete_id = self.kernel_manager.xreq_channel.complete(
303 '.'.join(context), # text
319 '.'.join(context), # text
304 self._get_input_buffer_cursor_line(), # line
320 self._get_input_buffer_cursor_line(), # line
305 self._get_input_buffer_cursor_column(), # cursor_pos
321 self._get_input_buffer_cursor_column(), # cursor_pos
306 self.input_buffer) # block
322 self.input_buffer) # block
307 self._complete_pos = self._get_cursor().position()
323 self._complete_pos = self._get_cursor().position()
308 return True
324 return True
309
325
310 def _get_banner(self):
326 def _get_banner(self):
311 """ Gets a banner to display at the beginning of a session.
327 """ Gets a banner to display at the beginning of a session.
312 """
328 """
313 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
329 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
314 '"license" for more information.'
330 '"license" for more information.'
315 return banner % (sys.version, sys.platform)
331 return banner % (sys.version, sys.platform)
316
332
317 def _get_context(self, cursor=None):
333 def _get_context(self, cursor=None):
318 """ Gets the context at the current cursor location.
334 """ Gets the context at the current cursor location.
319 """
335 """
320 if cursor is None:
336 if cursor is None:
321 cursor = self._get_cursor()
337 cursor = self._get_cursor()
322 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
338 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
323 QtGui.QTextCursor.KeepAnchor)
339 QtGui.QTextCursor.KeepAnchor)
324 text = str(cursor.selection().toPlainText())
340 text = str(cursor.selection().toPlainText())
325 return self._completion_lexer.get_context(text)
341 return self._completion_lexer.get_context(text)
326
342
327 def _interrupt_kernel(self):
343 def _kernel_interrupt(self):
328 """ Attempts to the interrupt the kernel.
344 """ Attempts to interrupt the running kernel.
329 """
345 """
330 if self.kernel_manager.has_kernel:
346 if self.custom_interrupt:
347 self.custom_interrupt_requested.emit()
348 elif self.kernel_manager.has_kernel:
331 self.kernel_manager.signal_kernel(signal.SIGINT)
349 self.kernel_manager.signal_kernel(signal.SIGINT)
332 else:
350 else:
333 self._append_plain_text('Kernel process is either remote or '
351 self._append_plain_text('Kernel process is either remote or '
334 'unspecified. Cannot interrupt.\n')
352 'unspecified. Cannot interrupt.\n')
335
353
354 def _kernel_restart(self):
355 """ Attempts to restart the running kernel.
356 """
357 if self.custom_restart:
358 self.custom_restart_requested.emit()
359 elif self.kernel_manager.has_kernel:
360 try:
361 self.kernel_manager.restart_kernel()
362 except RuntimeError:
363 message = 'Kernel started externally. Cannot restart.\n'
364 self._append_plain_text(message)
365 else:
366 self._stopped_channels()
367 self._append_plain_text('Kernel restarting...\n')
368 self._show_interpreter_prompt()
369 else:
370 self._append_plain_text('Kernel process is either remote or '
371 'unspecified. Cannot restart.\n')
372
336 def _process_execute_abort(self, msg):
373 def _process_execute_abort(self, msg):
337 """ Process a reply for an aborted execution request.
374 """ Process a reply for an aborted execution request.
338 """
375 """
339 self._append_plain_text("ERROR: execution aborted\n")
376 self._append_plain_text("ERROR: execution aborted\n")
340
377
341 def _process_execute_error(self, msg):
378 def _process_execute_error(self, msg):
342 """ Process a reply for an execution request that resulted in an error.
379 """ Process a reply for an execution request that resulted in an error.
343 """
380 """
344 content = msg['content']
381 content = msg['content']
345 traceback = ''.join(content['traceback'])
382 traceback = ''.join(content['traceback'])
346 self._append_plain_text(traceback)
383 self._append_plain_text(traceback)
347
384
348 def _process_execute_ok(self, msg):
385 def _process_execute_ok(self, msg):
349 """ Process a reply for a successful execution equest.
386 """ Process a reply for a successful execution equest.
350 """
387 """
351 payload = msg['content']['payload']
388 payload = msg['content']['payload']
352 for item in payload:
389 for item in payload:
353 if not self._process_execute_payload(item):
390 if not self._process_execute_payload(item):
354 warning = 'Received unknown payload of type %s\n'
391 warning = 'Received unknown payload of type %s\n'
355 self._append_plain_text(warning % repr(item['source']))
392 self._append_plain_text(warning % repr(item['source']))
356
393
357 def _process_execute_payload(self, item):
394 def _process_execute_payload(self, item):
358 """ Process a single payload item from the list of payload items in an
395 """ Process a single payload item from the list of payload items in an
359 execution reply. Returns whether the payload was handled.
396 execution reply. Returns whether the payload was handled.
360 """
397 """
361 # The basic FrontendWidget doesn't handle payloads, as they are a
398 # The basic FrontendWidget doesn't handle payloads, as they are a
362 # mechanism for going beyond the standard Python interpreter model.
399 # mechanism for going beyond the standard Python interpreter model.
363 return False
400 return False
364
401
365 def _show_interpreter_prompt(self):
402 def _show_interpreter_prompt(self):
366 """ Shows a prompt for the interpreter.
403 """ Shows a prompt for the interpreter.
367 """
404 """
368 self._show_prompt('>>> ')
405 self._show_prompt('>>> ')
369
406
370 def _show_interpreter_prompt_for_reply(self, msg):
407 def _show_interpreter_prompt_for_reply(self, msg):
371 """ Shows a prompt for the interpreter given an 'execute_reply' message.
408 """ Shows a prompt for the interpreter given an 'execute_reply' message.
372 """
409 """
373 self._show_interpreter_prompt()
410 self._show_interpreter_prompt()
374
411
375 #------ Signal handlers ----------------------------------------------------
412 #------ Signal handlers ----------------------------------------------------
376
413
377 def _document_contents_change(self, position, removed, added):
414 def _document_contents_change(self, position, removed, added):
378 """ Called whenever the document's content changes. Display a call tip
415 """ Called whenever the document's content changes. Display a call tip
379 if appropriate.
416 if appropriate.
380 """
417 """
381 # Calculate where the cursor should be *after* the change:
418 # Calculate where the cursor should be *after* the change:
382 position += added
419 position += added
383
420
384 document = self._control.document()
421 document = self._control.document()
385 if position == self._get_cursor().position():
422 if position == self._get_cursor().position():
386 self._call_tip()
423 self._call_tip()
@@ -1,126 +1,124 b''
1 import os
2
3 # System library imports
1 # System library imports
4 from PyQt4 import QtCore, QtGui
2 from PyQt4 import QtCore, QtGui
5
3
6 # Local imports
4 # Local imports
7 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
5 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
8 from ipython_widget import IPythonWidget
6 from ipython_widget import IPythonWidget
9
7
10
8
11 class RichIPythonWidget(IPythonWidget):
9 class RichIPythonWidget(IPythonWidget):
12 """ An IPythonWidget that supports rich text, including lists, images, and
10 """ An IPythonWidget that supports rich text, including lists, images, and
13 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
14 text version.
12 text version.
15 """
13 """
16
14
17 # RichIPythonWidget protected class variables.
15 # RichIPythonWidget protected class variables.
18 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
16 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
19 _svg_text_format_property = 1
17 _svg_text_format_property = 1
20
18
21 #---------------------------------------------------------------------------
19 #---------------------------------------------------------------------------
22 # 'object' interface
20 # 'object' interface
23 #---------------------------------------------------------------------------
21 #---------------------------------------------------------------------------
24
22
25 def __init__(self, *args, **kw):
23 def __init__(self, *args, **kw):
26 """ Create a RichIPythonWidget.
24 """ Create a RichIPythonWidget.
27 """
25 """
28 kw['kind'] = 'rich'
26 kw['kind'] = 'rich'
29 super(RichIPythonWidget, self).__init__(*args, **kw)
27 super(RichIPythonWidget, self).__init__(*args, **kw)
30
28
31 #---------------------------------------------------------------------------
29 #---------------------------------------------------------------------------
32 # 'ConsoleWidget' protected interface
30 # 'ConsoleWidget' protected interface
33 #---------------------------------------------------------------------------
31 #---------------------------------------------------------------------------
34
32
35 def _show_context_menu(self, pos):
33 def _show_context_menu(self, pos):
36 """ Reimplemented to show a custom context menu for images.
34 """ Reimplemented to show a custom context menu for images.
37 """
35 """
38 format = self._control.cursorForPosition(pos).charFormat()
36 format = self._control.cursorForPosition(pos).charFormat()
39 name = format.stringProperty(QtGui.QTextFormat.ImageName)
37 name = format.stringProperty(QtGui.QTextFormat.ImageName)
40 if name.isEmpty():
38 if name.isEmpty():
41 super(RichIPythonWidget, self)._show_context_menu(pos)
39 super(RichIPythonWidget, self)._show_context_menu(pos)
42 else:
40 else:
43 menu = QtGui.QMenu()
41 menu = QtGui.QMenu()
44
42
45 menu.addAction('Copy Image', lambda: self._copy_image(name))
43 menu.addAction('Copy Image', lambda: self._copy_image(name))
46 menu.addAction('Save Image As...', lambda: self._save_image(name))
44 menu.addAction('Save Image As...', lambda: self._save_image(name))
47 menu.addSeparator()
45 menu.addSeparator()
48
46
49 svg = format.stringProperty(self._svg_text_format_property)
47 svg = format.stringProperty(self._svg_text_format_property)
50 if not svg.isEmpty():
48 if not svg.isEmpty():
51 menu.addSeparator()
49 menu.addSeparator()
52 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
50 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
53 menu.addAction('Save SVG As...',
51 menu.addAction('Save SVG As...',
54 lambda: save_svg(svg, self._control))
52 lambda: save_svg(svg, self._control))
55
53
56 menu.exec_(self._control.mapToGlobal(pos))
54 menu.exec_(self._control.mapToGlobal(pos))
57
55
58 #---------------------------------------------------------------------------
56 #---------------------------------------------------------------------------
59 # 'FrontendWidget' protected interface
57 # 'FrontendWidget' protected interface
60 #---------------------------------------------------------------------------
58 #---------------------------------------------------------------------------
61
59
62 def _process_execute_payload(self, item):
60 def _process_execute_payload(self, item):
63 """ Reimplemented to handle matplotlib plot payloads.
61 """ Reimplemented to handle matplotlib plot payloads.
64 """
62 """
65 if item['source'] == self._payload_source_plot:
63 if item['source'] == self._payload_source_plot:
66 if item['format'] == 'svg':
64 if item['format'] == 'svg':
67 svg = item['data']
65 svg = item['data']
68 try:
66 try:
69 image = svg_to_image(svg)
67 image = svg_to_image(svg)
70 except ValueError:
68 except ValueError:
71 self._append_plain_text('Received invalid plot data.')
69 self._append_plain_text('Received invalid plot data.')
72 else:
70 else:
73 format = self._add_image(image)
71 format = self._add_image(image)
74 format.setProperty(self._svg_text_format_property, svg)
72 format.setProperty(self._svg_text_format_property, svg)
75 cursor = self._get_end_cursor()
73 cursor = self._get_end_cursor()
76 cursor.insertBlock()
74 cursor.insertBlock()
77 cursor.insertImage(format)
75 cursor.insertImage(format)
78 cursor.insertBlock()
76 cursor.insertBlock()
79 return True
77 return True
80 else:
78 else:
81 # Add other plot formats here!
79 # Add other plot formats here!
82 return False
80 return False
83 else:
81 else:
84 return super(RichIPythonWidget, self)._process_execute_payload(item)
82 return super(RichIPythonWidget, self)._process_execute_payload(item)
85
83
86 #---------------------------------------------------------------------------
84 #---------------------------------------------------------------------------
87 # 'RichIPythonWidget' protected interface
85 # 'RichIPythonWidget' protected interface
88 #---------------------------------------------------------------------------
86 #---------------------------------------------------------------------------
89
87
90 def _add_image(self, image):
88 def _add_image(self, image):
91 """ Adds the specified QImage to the document and returns a
89 """ Adds the specified QImage to the document and returns a
92 QTextImageFormat that references it.
90 QTextImageFormat that references it.
93 """
91 """
94 document = self._control.document()
92 document = self._control.document()
95 name = QtCore.QString.number(image.cacheKey())
93 name = QtCore.QString.number(image.cacheKey())
96 document.addResource(QtGui.QTextDocument.ImageResource,
94 document.addResource(QtGui.QTextDocument.ImageResource,
97 QtCore.QUrl(name), image)
95 QtCore.QUrl(name), image)
98 format = QtGui.QTextImageFormat()
96 format = QtGui.QTextImageFormat()
99 format.setName(name)
97 format.setName(name)
100 return format
98 return format
101
99
102 def _copy_image(self, name):
100 def _copy_image(self, name):
103 """ Copies the ImageResource with 'name' to the clipboard.
101 """ Copies the ImageResource with 'name' to the clipboard.
104 """
102 """
105 image = self._get_image(name)
103 image = self._get_image(name)
106 QtGui.QApplication.clipboard().setImage(image)
104 QtGui.QApplication.clipboard().setImage(image)
107
105
108 def _get_image(self, name):
106 def _get_image(self, name):
109 """ Returns the QImage stored as the ImageResource with 'name'.
107 """ Returns the QImage stored as the ImageResource with 'name'.
110 """
108 """
111 document = self._control.document()
109 document = self._control.document()
112 variant = document.resource(QtGui.QTextDocument.ImageResource,
110 variant = document.resource(QtGui.QTextDocument.ImageResource,
113 QtCore.QUrl(name))
111 QtCore.QUrl(name))
114 return variant.toPyObject()
112 return variant.toPyObject()
115
113
116 def _save_image(self, name, format='PNG'):
114 def _save_image(self, name, format='PNG'):
117 """ Shows a save dialog for the ImageResource with 'name'.
115 """ Shows a save dialog for the ImageResource with 'name'.
118 """
116 """
119 dialog = QtGui.QFileDialog(self._control, 'Save Image')
117 dialog = QtGui.QFileDialog(self._control, 'Save Image')
120 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
118 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
121 dialog.setDefaultSuffix(format.lower())
119 dialog.setDefaultSuffix(format.lower())
122 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
120 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
123 if dialog.exec_():
121 if dialog.exec_():
124 filename = dialog.selectedFiles()[0]
122 filename = dialog.selectedFiles()[0]
125 image = self._get_image(name)
123 image = self._get_image(name)
126 image.save(filename, format)
124 image.save(filename, format)
@@ -1,617 +1,632 b''
1 """Base classes to manage the interaction with a running kernel.
1 """Base classes to manage the interaction with a running kernel.
2
2
3 Todo
3 Todo
4 ====
4 ====
5
5
6 * Create logger to handle debugging and console messages.
6 * Create logger to handle debugging and console messages.
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2008-2010 The IPython Development Team
10 # Copyright (C) 2008-2010 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 # Standard library imports.
20 # Standard library imports.
21 from Queue import Queue, Empty
21 from Queue import Queue, Empty
22 from subprocess import Popen
22 from subprocess import Popen
23 from threading import Thread
23 from threading import Thread
24 import time
24 import time
25
25
26 # System library imports.
26 # System library imports.
27 import zmq
27 import zmq
28 from zmq import POLLIN, POLLOUT, POLLERR
28 from zmq import POLLIN, POLLOUT, POLLERR
29 from zmq.eventloop import ioloop
29 from zmq.eventloop import ioloop
30
30
31 # Local imports.
31 # Local imports.
32 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
32 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
33 from session import Session
33 from session import Session
34
34
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36 # Constants and exceptions
36 # Constants and exceptions
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38
38
39 LOCALHOST = '127.0.0.1'
39 LOCALHOST = '127.0.0.1'
40
40
41 class InvalidPortNumber(Exception):
41 class InvalidPortNumber(Exception):
42 pass
42 pass
43
43
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45 # ZMQ Socket Channel classes
45 # ZMQ Socket Channel classes
46 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
47
47
48 class ZmqSocketChannel(Thread):
48 class ZmqSocketChannel(Thread):
49 """The base class for the channels that use ZMQ sockets.
49 """The base class for the channels that use ZMQ sockets.
50 """
50 """
51 context = None
51 context = None
52 session = None
52 session = None
53 socket = None
53 socket = None
54 ioloop = None
54 ioloop = None
55 iostate = None
55 iostate = None
56 _address = None
56 _address = None
57
57
58 def __init__(self, context, session, address):
58 def __init__(self, context, session, address):
59 """Create a channel
59 """Create a channel
60
60
61 Parameters
61 Parameters
62 ----------
62 ----------
63 context : :class:`zmq.Context`
63 context : :class:`zmq.Context`
64 The ZMQ context to use.
64 The ZMQ context to use.
65 session : :class:`session.Session`
65 session : :class:`session.Session`
66 The session to use.
66 The session to use.
67 address : tuple
67 address : tuple
68 Standard (ip, port) tuple that the kernel is listening on.
68 Standard (ip, port) tuple that the kernel is listening on.
69 """
69 """
70 super(ZmqSocketChannel, self).__init__()
70 super(ZmqSocketChannel, self).__init__()
71 self.daemon = True
71 self.daemon = True
72
72
73 self.context = context
73 self.context = context
74 self.session = session
74 self.session = session
75 if address[1] == 0:
75 if address[1] == 0:
76 message = 'The port number for a channel cannot be 0.'
76 message = 'The port number for a channel cannot be 0.'
77 raise InvalidPortNumber(message)
77 raise InvalidPortNumber(message)
78 self._address = address
78 self._address = address
79
79
80 def stop(self):
80 def stop(self):
81 """Stop the channel's activity.
81 """Stop the channel's activity.
82
82
83 This calls :method:`Thread.join` and returns when the thread
83 This calls :method:`Thread.join` and returns when the thread
84 terminates. :class:`RuntimeError` will be raised if
84 terminates. :class:`RuntimeError` will be raised if
85 :method:`self.start` is called again.
85 :method:`self.start` is called again.
86 """
86 """
87 self.join()
87 self.join()
88
88
89 @property
89 @property
90 def address(self):
90 def address(self):
91 """Get the channel's address as an (ip, port) tuple.
91 """Get the channel's address as an (ip, port) tuple.
92
92
93 By the default, the address is (localhost, 0), where 0 means a random
93 By the default, the address is (localhost, 0), where 0 means a random
94 port.
94 port.
95 """
95 """
96 return self._address
96 return self._address
97
97
98 def add_io_state(self, state):
98 def add_io_state(self, state):
99 """Add IO state to the eventloop.
99 """Add IO state to the eventloop.
100
100
101 Parameters
101 Parameters
102 ----------
102 ----------
103 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
103 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
104 The IO state flag to set.
104 The IO state flag to set.
105
105
106 This is thread safe as it uses the thread safe IOLoop.add_callback.
106 This is thread safe as it uses the thread safe IOLoop.add_callback.
107 """
107 """
108 def add_io_state_callback():
108 def add_io_state_callback():
109 if not self.iostate & state:
109 if not self.iostate & state:
110 self.iostate = self.iostate | state
110 self.iostate = self.iostate | state
111 self.ioloop.update_handler(self.socket, self.iostate)
111 self.ioloop.update_handler(self.socket, self.iostate)
112 self.ioloop.add_callback(add_io_state_callback)
112 self.ioloop.add_callback(add_io_state_callback)
113
113
114 def drop_io_state(self, state):
114 def drop_io_state(self, state):
115 """Drop IO state from the eventloop.
115 """Drop IO state from the eventloop.
116
116
117 Parameters
117 Parameters
118 ----------
118 ----------
119 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
119 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
120 The IO state flag to set.
120 The IO state flag to set.
121
121
122 This is thread safe as it uses the thread safe IOLoop.add_callback.
122 This is thread safe as it uses the thread safe IOLoop.add_callback.
123 """
123 """
124 def drop_io_state_callback():
124 def drop_io_state_callback():
125 if self.iostate & state:
125 if self.iostate & state:
126 self.iostate = self.iostate & (~state)
126 self.iostate = self.iostate & (~state)
127 self.ioloop.update_handler(self.socket, self.iostate)
127 self.ioloop.update_handler(self.socket, self.iostate)
128 self.ioloop.add_callback(drop_io_state_callback)
128 self.ioloop.add_callback(drop_io_state_callback)
129
129
130
130
131 class XReqSocketChannel(ZmqSocketChannel):
131 class XReqSocketChannel(ZmqSocketChannel):
132 """The XREQ channel for issues request/replies to the kernel.
132 """The XREQ channel for issues request/replies to the kernel.
133 """
133 """
134
134
135 command_queue = None
135 command_queue = None
136
136
137 def __init__(self, context, session, address):
137 def __init__(self, context, session, address):
138 self.command_queue = Queue()
138 self.command_queue = Queue()
139 super(XReqSocketChannel, self).__init__(context, session, address)
139 super(XReqSocketChannel, self).__init__(context, session, address)
140
140
141 def run(self):
141 def run(self):
142 """The thread's main activity. Call start() instead."""
142 """The thread's main activity. Call start() instead."""
143 self.socket = self.context.socket(zmq.XREQ)
143 self.socket = self.context.socket(zmq.XREQ)
144 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
144 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
145 self.socket.connect('tcp://%s:%i' % self.address)
145 self.socket.connect('tcp://%s:%i' % self.address)
146 self.ioloop = ioloop.IOLoop()
146 self.ioloop = ioloop.IOLoop()
147 self.iostate = POLLERR|POLLIN
147 self.iostate = POLLERR|POLLIN
148 self.ioloop.add_handler(self.socket, self._handle_events,
148 self.ioloop.add_handler(self.socket, self._handle_events,
149 self.iostate)
149 self.iostate)
150 self.ioloop.start()
150 self.ioloop.start()
151
151
152 def stop(self):
152 def stop(self):
153 self.ioloop.stop()
153 self.ioloop.stop()
154 super(XReqSocketChannel, self).stop()
154 super(XReqSocketChannel, self).stop()
155
155
156 def call_handlers(self, msg):
156 def call_handlers(self, msg):
157 """This method is called in the ioloop thread when a message arrives.
157 """This method is called in the ioloop thread when a message arrives.
158
158
159 Subclasses should override this method to handle incoming messages.
159 Subclasses should override this method to handle incoming messages.
160 It is important to remember that this method is called in the thread
160 It is important to remember that this method is called in the thread
161 so that some logic must be done to ensure that the application leve
161 so that some logic must be done to ensure that the application leve
162 handlers are called in the application thread.
162 handlers are called in the application thread.
163 """
163 """
164 raise NotImplementedError('call_handlers must be defined in a subclass.')
164 raise NotImplementedError('call_handlers must be defined in a subclass.')
165
165
166 def execute(self, code, silent=False):
166 def execute(self, code, silent=False):
167 """Execute code in the kernel.
167 """Execute code in the kernel.
168
168
169 Parameters
169 Parameters
170 ----------
170 ----------
171 code : str
171 code : str
172 A string of Python code.
172 A string of Python code.
173 silent : bool, optional (default False)
173 silent : bool, optional (default False)
174 If set, the kernel will execute the code as quietly possible.
174 If set, the kernel will execute the code as quietly possible.
175
175
176 Returns
176 Returns
177 -------
177 -------
178 The msg_id of the message sent.
178 The msg_id of the message sent.
179 """
179 """
180 # Create class for content/msg creation. Related to, but possibly
180 # Create class for content/msg creation. Related to, but possibly
181 # not in Session.
181 # not in Session.
182 content = dict(code=code, silent=silent)
182 content = dict(code=code, silent=silent)
183 msg = self.session.msg('execute_request', content)
183 msg = self.session.msg('execute_request', content)
184 self._queue_request(msg)
184 self._queue_request(msg)
185 return msg['header']['msg_id']
185 return msg['header']['msg_id']
186
186
187 def complete(self, text, line, cursor_pos, block=None):
187 def complete(self, text, line, cursor_pos, block=None):
188 """Tab complete text in the kernel's namespace.
188 """Tab complete text in the kernel's namespace.
189
189
190 Parameters
190 Parameters
191 ----------
191 ----------
192 text : str
192 text : str
193 The text to complete.
193 The text to complete.
194 line : str
194 line : str
195 The full line of text that is the surrounding context for the
195 The full line of text that is the surrounding context for the
196 text to complete.
196 text to complete.
197 cursor_pos : int
197 cursor_pos : int
198 The position of the cursor in the line where the completion was
198 The position of the cursor in the line where the completion was
199 requested.
199 requested.
200 block : str, optional
200 block : str, optional
201 The full block of code in which the completion is being requested.
201 The full block of code in which the completion is being requested.
202
202
203 Returns
203 Returns
204 -------
204 -------
205 The msg_id of the message sent.
205 The msg_id of the message sent.
206 """
206 """
207 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
207 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
208 msg = self.session.msg('complete_request', content)
208 msg = self.session.msg('complete_request', content)
209 self._queue_request(msg)
209 self._queue_request(msg)
210 return msg['header']['msg_id']
210 return msg['header']['msg_id']
211
211
212 def object_info(self, oname):
212 def object_info(self, oname):
213 """Get metadata information about an object.
213 """Get metadata information about an object.
214
214
215 Parameters
215 Parameters
216 ----------
216 ----------
217 oname : str
217 oname : str
218 A string specifying the object name.
218 A string specifying the object name.
219
219
220 Returns
220 Returns
221 -------
221 -------
222 The msg_id of the message sent.
222 The msg_id of the message sent.
223 """
223 """
224 content = dict(oname=oname)
224 content = dict(oname=oname)
225 msg = self.session.msg('object_info_request', content)
225 msg = self.session.msg('object_info_request', content)
226 self._queue_request(msg)
226 self._queue_request(msg)
227 return msg['header']['msg_id']
227 return msg['header']['msg_id']
228
228
229 def history(self, index=None, raw=False, output=True):
229 def history(self, index=None, raw=False, output=True):
230 """Get the history list.
230 """Get the history list.
231
231
232 Parameters
232 Parameters
233 ----------
233 ----------
234 index : n or (n1, n2) or None
234 index : n or (n1, n2) or None
235 If n, then the last entries. If a tuple, then all in
235 If n, then the last entries. If a tuple, then all in
236 range(n1, n2). If None, then all entries. Raises IndexError if
236 range(n1, n2). If None, then all entries. Raises IndexError if
237 the format of index is incorrect.
237 the format of index is incorrect.
238 raw : bool
238 raw : bool
239 If True, return the raw input.
239 If True, return the raw input.
240 output : bool
240 output : bool
241 If True, then return the output as well.
241 If True, then return the output as well.
242
242
243 Returns
243 Returns
244 -------
244 -------
245 The msg_id of the message sent.
245 The msg_id of the message sent.
246 """
246 """
247 content = dict(index=index, raw=raw, output=output)
247 content = dict(index=index, raw=raw, output=output)
248 msg = self.session.msg('history_request', content)
248 msg = self.session.msg('history_request', content)
249 self._queue_request(msg)
249 self._queue_request(msg)
250 return msg['header']['msg_id']
250 return msg['header']['msg_id']
251
251
252 def prompt(self):
252 def prompt(self):
253 """Requests a prompt number from the kernel.
253 """Requests a prompt number from the kernel.
254
254
255 Returns
255 Returns
256 -------
256 -------
257 The msg_id of the message sent.
257 The msg_id of the message sent.
258 """
258 """
259 msg = self.session.msg('prompt_request')
259 msg = self.session.msg('prompt_request')
260 self._queue_request(msg)
260 self._queue_request(msg)
261 return msg['header']['msg_id']
261 return msg['header']['msg_id']
262
262
263 def _handle_events(self, socket, events):
263 def _handle_events(self, socket, events):
264 if events & POLLERR:
264 if events & POLLERR:
265 self._handle_err()
265 self._handle_err()
266 if events & POLLOUT:
266 if events & POLLOUT:
267 self._handle_send()
267 self._handle_send()
268 if events & POLLIN:
268 if events & POLLIN:
269 self._handle_recv()
269 self._handle_recv()
270
270
271 def _handle_recv(self):
271 def _handle_recv(self):
272 msg = self.socket.recv_json()
272 msg = self.socket.recv_json()
273 self.call_handlers(msg)
273 self.call_handlers(msg)
274
274
275 def _handle_send(self):
275 def _handle_send(self):
276 try:
276 try:
277 msg = self.command_queue.get(False)
277 msg = self.command_queue.get(False)
278 except Empty:
278 except Empty:
279 pass
279 pass
280 else:
280 else:
281 self.socket.send_json(msg)
281 self.socket.send_json(msg)
282 if self.command_queue.empty():
282 if self.command_queue.empty():
283 self.drop_io_state(POLLOUT)
283 self.drop_io_state(POLLOUT)
284
284
285 def _handle_err(self):
285 def _handle_err(self):
286 # We don't want to let this go silently, so eventually we should log.
286 # We don't want to let this go silently, so eventually we should log.
287 raise zmq.ZMQError()
287 raise zmq.ZMQError()
288
288
289 def _queue_request(self, msg):
289 def _queue_request(self, msg):
290 self.command_queue.put(msg)
290 self.command_queue.put(msg)
291 self.add_io_state(POLLOUT)
291 self.add_io_state(POLLOUT)
292
292
293
293
294 class SubSocketChannel(ZmqSocketChannel):
294 class SubSocketChannel(ZmqSocketChannel):
295 """The SUB channel which listens for messages that the kernel publishes.
295 """The SUB channel which listens for messages that the kernel publishes.
296 """
296 """
297
297
298 def __init__(self, context, session, address):
298 def __init__(self, context, session, address):
299 super(SubSocketChannel, self).__init__(context, session, address)
299 super(SubSocketChannel, self).__init__(context, session, address)
300
300
301 def run(self):
301 def run(self):
302 """The thread's main activity. Call start() instead."""
302 """The thread's main activity. Call start() instead."""
303 self.socket = self.context.socket(zmq.SUB)
303 self.socket = self.context.socket(zmq.SUB)
304 self.socket.setsockopt(zmq.SUBSCRIBE,'')
304 self.socket.setsockopt(zmq.SUBSCRIBE,'')
305 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
305 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
306 self.socket.connect('tcp://%s:%i' % self.address)
306 self.socket.connect('tcp://%s:%i' % self.address)
307 self.ioloop = ioloop.IOLoop()
307 self.ioloop = ioloop.IOLoop()
308 self.iostate = POLLIN|POLLERR
308 self.iostate = POLLIN|POLLERR
309 self.ioloop.add_handler(self.socket, self._handle_events,
309 self.ioloop.add_handler(self.socket, self._handle_events,
310 self.iostate)
310 self.iostate)
311 self.ioloop.start()
311 self.ioloop.start()
312
312
313 def stop(self):
313 def stop(self):
314 self.ioloop.stop()
314 self.ioloop.stop()
315 super(SubSocketChannel, self).stop()
315 super(SubSocketChannel, self).stop()
316
316
317 def call_handlers(self, msg):
317 def call_handlers(self, msg):
318 """This method is called in the ioloop thread when a message arrives.
318 """This method is called in the ioloop thread when a message arrives.
319
319
320 Subclasses should override this method to handle incoming messages.
320 Subclasses should override this method to handle incoming messages.
321 It is important to remember that this method is called in the thread
321 It is important to remember that this method is called in the thread
322 so that some logic must be done to ensure that the application leve
322 so that some logic must be done to ensure that the application leve
323 handlers are called in the application thread.
323 handlers are called in the application thread.
324 """
324 """
325 raise NotImplementedError('call_handlers must be defined in a subclass.')
325 raise NotImplementedError('call_handlers must be defined in a subclass.')
326
326
327 def flush(self, timeout=1.0):
327 def flush(self, timeout=1.0):
328 """Immediately processes all pending messages on the SUB channel.
328 """Immediately processes all pending messages on the SUB channel.
329
329
330 Callers should use this method to ensure that :method:`call_handlers`
330 Callers should use this method to ensure that :method:`call_handlers`
331 has been called for all messages that have been received on the
331 has been called for all messages that have been received on the
332 0MQ SUB socket of this channel.
332 0MQ SUB socket of this channel.
333
333
334 This method is thread safe.
334 This method is thread safe.
335
335
336 Parameters
336 Parameters
337 ----------
337 ----------
338 timeout : float, optional
338 timeout : float, optional
339 The maximum amount of time to spend flushing, in seconds. The
339 The maximum amount of time to spend flushing, in seconds. The
340 default is one second.
340 default is one second.
341 """
341 """
342 # We do the IOLoop callback process twice to ensure that the IOLoop
342 # We do the IOLoop callback process twice to ensure that the IOLoop
343 # gets to perform at least one full poll.
343 # gets to perform at least one full poll.
344 stop_time = time.time() + timeout
344 stop_time = time.time() + timeout
345 for i in xrange(2):
345 for i in xrange(2):
346 self._flushed = False
346 self._flushed = False
347 self.ioloop.add_callback(self._flush)
347 self.ioloop.add_callback(self._flush)
348 while not self._flushed and time.time() < stop_time:
348 while not self._flushed and time.time() < stop_time:
349 time.sleep(0.01)
349 time.sleep(0.01)
350
350
351 def _handle_events(self, socket, events):
351 def _handle_events(self, socket, events):
352 # Turn on and off POLLOUT depending on if we have made a request
352 # Turn on and off POLLOUT depending on if we have made a request
353 if events & POLLERR:
353 if events & POLLERR:
354 self._handle_err()
354 self._handle_err()
355 if events & POLLIN:
355 if events & POLLIN:
356 self._handle_recv()
356 self._handle_recv()
357
357
358 def _handle_err(self):
358 def _handle_err(self):
359 # We don't want to let this go silently, so eventually we should log.
359 # We don't want to let this go silently, so eventually we should log.
360 raise zmq.ZMQError()
360 raise zmq.ZMQError()
361
361
362 def _handle_recv(self):
362 def _handle_recv(self):
363 # Get all of the messages we can
363 # Get all of the messages we can
364 while True:
364 while True:
365 try:
365 try:
366 msg = self.socket.recv_json(zmq.NOBLOCK)
366 msg = self.socket.recv_json(zmq.NOBLOCK)
367 except zmq.ZMQError:
367 except zmq.ZMQError:
368 # Check the errno?
368 # Check the errno?
369 # Will this trigger POLLERR?
369 # Will this trigger POLLERR?
370 break
370 break
371 else:
371 else:
372 self.call_handlers(msg)
372 self.call_handlers(msg)
373
373
374 def _flush(self):
374 def _flush(self):
375 """Callback for :method:`self.flush`."""
375 """Callback for :method:`self.flush`."""
376 self._flushed = True
376 self._flushed = True
377
377
378
378
379 class RepSocketChannel(ZmqSocketChannel):
379 class RepSocketChannel(ZmqSocketChannel):
380 """A reply channel to handle raw_input requests that the kernel makes."""
380 """A reply channel to handle raw_input requests that the kernel makes."""
381
381
382 msg_queue = None
382 msg_queue = None
383
383
384 def __init__(self, context, session, address):
384 def __init__(self, context, session, address):
385 self.msg_queue = Queue()
385 self.msg_queue = Queue()
386 super(RepSocketChannel, self).__init__(context, session, address)
386 super(RepSocketChannel, self).__init__(context, session, address)
387
387
388 def run(self):
388 def run(self):
389 """The thread's main activity. Call start() instead."""
389 """The thread's main activity. Call start() instead."""
390 self.socket = self.context.socket(zmq.XREQ)
390 self.socket = self.context.socket(zmq.XREQ)
391 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
391 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
392 self.socket.connect('tcp://%s:%i' % self.address)
392 self.socket.connect('tcp://%s:%i' % self.address)
393 self.ioloop = ioloop.IOLoop()
393 self.ioloop = ioloop.IOLoop()
394 self.iostate = POLLERR|POLLIN
394 self.iostate = POLLERR|POLLIN
395 self.ioloop.add_handler(self.socket, self._handle_events,
395 self.ioloop.add_handler(self.socket, self._handle_events,
396 self.iostate)
396 self.iostate)
397 self.ioloop.start()
397 self.ioloop.start()
398
398
399 def stop(self):
399 def stop(self):
400 self.ioloop.stop()
400 self.ioloop.stop()
401 super(RepSocketChannel, self).stop()
401 super(RepSocketChannel, self).stop()
402
402
403 def call_handlers(self, msg):
403 def call_handlers(self, msg):
404 """This method is called in the ioloop thread when a message arrives.
404 """This method is called in the ioloop thread when a message arrives.
405
405
406 Subclasses should override this method to handle incoming messages.
406 Subclasses should override this method to handle incoming messages.
407 It is important to remember that this method is called in the thread
407 It is important to remember that this method is called in the thread
408 so that some logic must be done to ensure that the application leve
408 so that some logic must be done to ensure that the application leve
409 handlers are called in the application thread.
409 handlers are called in the application thread.
410 """
410 """
411 raise NotImplementedError('call_handlers must be defined in a subclass.')
411 raise NotImplementedError('call_handlers must be defined in a subclass.')
412
412
413 def input(self, string):
413 def input(self, string):
414 """Send a string of raw input to the kernel."""
414 """Send a string of raw input to the kernel."""
415 content = dict(value=string)
415 content = dict(value=string)
416 msg = self.session.msg('input_reply', content)
416 msg = self.session.msg('input_reply', content)
417 self._queue_reply(msg)
417 self._queue_reply(msg)
418
418
419 def _handle_events(self, socket, events):
419 def _handle_events(self, socket, events):
420 if events & POLLERR:
420 if events & POLLERR:
421 self._handle_err()
421 self._handle_err()
422 if events & POLLOUT:
422 if events & POLLOUT:
423 self._handle_send()
423 self._handle_send()
424 if events & POLLIN:
424 if events & POLLIN:
425 self._handle_recv()
425 self._handle_recv()
426
426
427 def _handle_recv(self):
427 def _handle_recv(self):
428 msg = self.socket.recv_json()
428 msg = self.socket.recv_json()
429 self.call_handlers(msg)
429 self.call_handlers(msg)
430
430
431 def _handle_send(self):
431 def _handle_send(self):
432 try:
432 try:
433 msg = self.msg_queue.get(False)
433 msg = self.msg_queue.get(False)
434 except Empty:
434 except Empty:
435 pass
435 pass
436 else:
436 else:
437 self.socket.send_json(msg)
437 self.socket.send_json(msg)
438 if self.msg_queue.empty():
438 if self.msg_queue.empty():
439 self.drop_io_state(POLLOUT)
439 self.drop_io_state(POLLOUT)
440
440
441 def _handle_err(self):
441 def _handle_err(self):
442 # We don't want to let this go silently, so eventually we should log.
442 # We don't want to let this go silently, so eventually we should log.
443 raise zmq.ZMQError()
443 raise zmq.ZMQError()
444
444
445 def _queue_reply(self, msg):
445 def _queue_reply(self, msg):
446 self.msg_queue.put(msg)
446 self.msg_queue.put(msg)
447 self.add_io_state(POLLOUT)
447 self.add_io_state(POLLOUT)
448
448
449
449
450 #-----------------------------------------------------------------------------
450 #-----------------------------------------------------------------------------
451 # Main kernel manager class
451 # Main kernel manager class
452 #-----------------------------------------------------------------------------
452 #-----------------------------------------------------------------------------
453
453
454 class KernelManager(HasTraits):
454 class KernelManager(HasTraits):
455 """ Manages a kernel for a frontend.
455 """ Manages a kernel for a frontend.
456
456
457 The SUB channel is for the frontend to receive messages published by the
457 The SUB channel is for the frontend to receive messages published by the
458 kernel.
458 kernel.
459
459
460 The REQ channel is for the frontend to make requests of the kernel.
460 The REQ channel is for the frontend to make requests of the kernel.
461
461
462 The REP channel is for the kernel to request stdin (raw_input) from the
462 The REP channel is for the kernel to request stdin (raw_input) from the
463 frontend.
463 frontend.
464 """
464 """
465 # The PyZMQ Context to use for communication with the kernel.
465 # The PyZMQ Context to use for communication with the kernel.
466 context = Instance(zmq.Context,(),{})
466 context = Instance(zmq.Context,(),{})
467
467
468 # The Session to use for communication with the kernel.
468 # The Session to use for communication with the kernel.
469 session = Instance(Session,(),{})
469 session = Instance(Session,(),{})
470
470
471 # The kernel process with which the KernelManager is communicating.
471 # The kernel process with which the KernelManager is communicating.
472 kernel = Instance(Popen)
472 kernel = Instance(Popen)
473
473
474 # The addresses for the communication channels.
474 # The addresses for the communication channels.
475 xreq_address = TCPAddress((LOCALHOST, 0))
475 xreq_address = TCPAddress((LOCALHOST, 0))
476 sub_address = TCPAddress((LOCALHOST, 0))
476 sub_address = TCPAddress((LOCALHOST, 0))
477 rep_address = TCPAddress((LOCALHOST, 0))
477 rep_address = TCPAddress((LOCALHOST, 0))
478
478
479 # The classes to use for the various channels.
479 # The classes to use for the various channels.
480 xreq_channel_class = Type(XReqSocketChannel)
480 xreq_channel_class = Type(XReqSocketChannel)
481 sub_channel_class = Type(SubSocketChannel)
481 sub_channel_class = Type(SubSocketChannel)
482 rep_channel_class = Type(RepSocketChannel)
482 rep_channel_class = Type(RepSocketChannel)
483
483
484 # Protected traits.
484 # Protected traits.
485 _launch_args = Any
485 _xreq_channel = Any
486 _xreq_channel = Any
486 _sub_channel = Any
487 _sub_channel = Any
487 _rep_channel = Any
488 _rep_channel = Any
488
489
489 #--------------------------------------------------------------------------
490 #--------------------------------------------------------------------------
490 # Channel management methods:
491 # Channel management methods:
491 #--------------------------------------------------------------------------
492 #--------------------------------------------------------------------------
492
493
493 def start_channels(self):
494 def start_channels(self):
494 """Starts the channels for this kernel.
495 """Starts the channels for this kernel.
495
496
496 This will create the channels if they do not exist and then start
497 This will create the channels if they do not exist and then start
497 them. If port numbers of 0 are being used (random ports) then you
498 them. If port numbers of 0 are being used (random ports) then you
498 must first call :method:`start_kernel`. If the channels have been
499 must first call :method:`start_kernel`. If the channels have been
499 stopped and you call this, :class:`RuntimeError` will be raised.
500 stopped and you call this, :class:`RuntimeError` will be raised.
500 """
501 """
501 self.xreq_channel.start()
502 self.xreq_channel.start()
502 self.sub_channel.start()
503 self.sub_channel.start()
503 self.rep_channel.start()
504 self.rep_channel.start()
504
505
505 def stop_channels(self):
506 def stop_channels(self):
506 """Stops the channels for this kernel.
507 """Stops the channels for this kernel.
507
508
508 This stops the channels by joining their threads. If the channels
509 This stops the channels by joining their threads. If the channels
509 were not started, :class:`RuntimeError` will be raised.
510 were not started, :class:`RuntimeError` will be raised.
510 """
511 """
511 self.xreq_channel.stop()
512 self.xreq_channel.stop()
512 self.sub_channel.stop()
513 self.sub_channel.stop()
513 self.rep_channel.stop()
514 self.rep_channel.stop()
514
515
515 @property
516 @property
516 def channels_running(self):
517 def channels_running(self):
517 """Are all of the channels created and running?"""
518 """Are all of the channels created and running?"""
518 return self.xreq_channel.is_alive() \
519 return self.xreq_channel.is_alive() \
519 and self.sub_channel.is_alive() \
520 and self.sub_channel.is_alive() \
520 and self.rep_channel.is_alive()
521 and self.rep_channel.is_alive()
521
522
522 #--------------------------------------------------------------------------
523 #--------------------------------------------------------------------------
523 # Kernel process management methods:
524 # Kernel process management methods:
524 #--------------------------------------------------------------------------
525 #--------------------------------------------------------------------------
525
526
526 def start_kernel(self, ipython=True, **kw):
527 def start_kernel(self, **kw):
527 """Starts a kernel process and configures the manager to use it.
528 """Starts a kernel process and configures the manager to use it.
528
529
529 If random ports (port=0) are being used, this method must be called
530 If random ports (port=0) are being used, this method must be called
530 before the channels are created.
531 before the channels are created.
531
532
532 Parameters:
533 Parameters:
533 -----------
534 -----------
534 ipython : bool, optional (default True)
535 ipython : bool, optional (default True)
535 Whether to use an IPython kernel instead of a plain Python kernel.
536 Whether to use an IPython kernel instead of a plain Python kernel.
536 """
537 """
537 xreq, sub, rep = self.xreq_address, self.sub_address, self.rep_address
538 xreq, sub, rep = self.xreq_address, self.sub_address, self.rep_address
538 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST:
539 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST:
539 raise RuntimeError("Can only launch a kernel on localhost."
540 raise RuntimeError("Can only launch a kernel on localhost."
540 "Make sure that the '*_address' attributes are "
541 "Make sure that the '*_address' attributes are "
541 "configured properly.")
542 "configured properly.")
542
543
543 if ipython:
544 self._launch_args = kw.copy()
545 if kw.pop('ipython', True):
544 from ipkernel import launch_kernel as launch
546 from ipkernel import launch_kernel as launch
545 else:
547 else:
546 from pykernel import launch_kernel as launch
548 from pykernel import launch_kernel as launch
547 self.kernel, xrep, pub, req = launch(xrep_port=xreq[1], pub_port=sub[1],
549 self.kernel, xrep, pub, req = launch(xrep_port=xreq[1], pub_port=sub[1],
548 req_port=rep[1], **kw)
550 req_port=rep[1], **kw)
549 self.xreq_address = (LOCALHOST, xrep)
551 self.xreq_address = (LOCALHOST, xrep)
550 self.sub_address = (LOCALHOST, pub)
552 self.sub_address = (LOCALHOST, pub)
551 self.rep_address = (LOCALHOST, req)
553 self.rep_address = (LOCALHOST, req)
552
554
555 def restart_kernel(self):
556 """Restarts a kernel with the same arguments that were used to launch
557 it. If the old kernel was launched with random ports, the same ports
558 will be used for the new kernel.
559 """
560 if self._launch_args is None:
561 raise RuntimeError("Cannot restart the kernel. "
562 "No previous call to 'start_kernel'.")
563 else:
564 if self.has_kernel:
565 self.kill_kernel()
566 self.start_kernel(*self._launch_args)
567
553 @property
568 @property
554 def has_kernel(self):
569 def has_kernel(self):
555 """Returns whether a kernel process has been specified for the kernel
570 """Returns whether a kernel process has been specified for the kernel
556 manager.
571 manager.
557 """
572 """
558 return self.kernel is not None
573 return self.kernel is not None
559
574
560 def kill_kernel(self):
575 def kill_kernel(self):
561 """ Kill the running kernel. """
576 """ Kill the running kernel. """
562 if self.kernel is not None:
577 if self.kernel is not None:
563 self.kernel.kill()
578 self.kernel.kill()
564 self.kernel = None
579 self.kernel = None
565 else:
580 else:
566 raise RuntimeError("Cannot kill kernel. No kernel is running!")
581 raise RuntimeError("Cannot kill kernel. No kernel is running!")
567
582
568 def signal_kernel(self, signum):
583 def signal_kernel(self, signum):
569 """ Sends a signal to the kernel. """
584 """ Sends a signal to the kernel. """
570 if self.kernel is not None:
585 if self.kernel is not None:
571 self.kernel.send_signal(signum)
586 self.kernel.send_signal(signum)
572 else:
587 else:
573 raise RuntimeError("Cannot signal kernel. No kernel is running!")
588 raise RuntimeError("Cannot signal kernel. No kernel is running!")
574
589
575 @property
590 @property
576 def is_alive(self):
591 def is_alive(self):
577 """Is the kernel process still running?"""
592 """Is the kernel process still running?"""
578 if self.kernel is not None:
593 if self.kernel is not None:
579 if self.kernel.poll() is None:
594 if self.kernel.poll() is None:
580 return True
595 return True
581 else:
596 else:
582 return False
597 return False
583 else:
598 else:
584 # We didn't start the kernel with this KernelManager so we don't
599 # We didn't start the kernel with this KernelManager so we don't
585 # know if it is running. We should use a heartbeat for this case.
600 # know if it is running. We should use a heartbeat for this case.
586 return True
601 return True
587
602
588 #--------------------------------------------------------------------------
603 #--------------------------------------------------------------------------
589 # Channels used for communication with the kernel:
604 # Channels used for communication with the kernel:
590 #--------------------------------------------------------------------------
605 #--------------------------------------------------------------------------
591
606
592 @property
607 @property
593 def xreq_channel(self):
608 def xreq_channel(self):
594 """Get the REQ socket channel object to make requests of the kernel."""
609 """Get the REQ socket channel object to make requests of the kernel."""
595 if self._xreq_channel is None:
610 if self._xreq_channel is None:
596 self._xreq_channel = self.xreq_channel_class(self.context,
611 self._xreq_channel = self.xreq_channel_class(self.context,
597 self.session,
612 self.session,
598 self.xreq_address)
613 self.xreq_address)
599 return self._xreq_channel
614 return self._xreq_channel
600
615
601 @property
616 @property
602 def sub_channel(self):
617 def sub_channel(self):
603 """Get the SUB socket channel object."""
618 """Get the SUB socket channel object."""
604 if self._sub_channel is None:
619 if self._sub_channel is None:
605 self._sub_channel = self.sub_channel_class(self.context,
620 self._sub_channel = self.sub_channel_class(self.context,
606 self.session,
621 self.session,
607 self.sub_address)
622 self.sub_address)
608 return self._sub_channel
623 return self._sub_channel
609
624
610 @property
625 @property
611 def rep_channel(self):
626 def rep_channel(self):
612 """Get the REP socket channel object to handle stdin (raw_input)."""
627 """Get the REP socket channel object to handle stdin (raw_input)."""
613 if self._rep_channel is None:
628 if self._rep_channel is None:
614 self._rep_channel = self.rep_channel_class(self.context,
629 self._rep_channel = self.rep_channel_class(self.context,
615 self.session,
630 self.session,
616 self.rep_address)
631 self.rep_address)
617 return self._rep_channel
632 return self._rep_channel
General Comments 0
You need to be logged in to leave comments. Login now