##// END OF EJS Templates
* Refactored ConsoleWidget execution API for greater flexibility and clarity....
epatters -
Show More
@@ -1,766 +1,817 b''
1 # Standard library imports
1 # Standard library imports
2 import re
2 import re
3 import sys
3 import sys
4
4
5 # System library imports
5 # System library imports
6 from PyQt4 import QtCore, QtGui
6 from PyQt4 import QtCore, QtGui
7
7
8 # Local imports
8 # Local imports
9 from completion_widget import CompletionWidget
9 from completion_widget import CompletionWidget
10
10
11
11
12 class AnsiCodeProcessor(object):
12 class AnsiCodeProcessor(object):
13 """ Translates ANSI escape codes into readable attributes.
13 """ Translates ANSI escape codes into readable attributes.
14 """
14 """
15
15
16 def __init__(self):
16 def __init__(self):
17 self.ansi_colors = ( # Normal, Bright/Light
17 self.ansi_colors = ( # Normal, Bright/Light
18 ('#000000', '#7f7f7f'), # 0: black
18 ('#000000', '#7f7f7f'), # 0: black
19 ('#cd0000', '#ff0000'), # 1: red
19 ('#cd0000', '#ff0000'), # 1: red
20 ('#00cd00', '#00ff00'), # 2: green
20 ('#00cd00', '#00ff00'), # 2: green
21 ('#cdcd00', '#ffff00'), # 3: yellow
21 ('#cdcd00', '#ffff00'), # 3: yellow
22 ('#0000ee', '#0000ff'), # 4: blue
22 ('#0000ee', '#0000ff'), # 4: blue
23 ('#cd00cd', '#ff00ff'), # 5: magenta
23 ('#cd00cd', '#ff00ff'), # 5: magenta
24 ('#00cdcd', '#00ffff'), # 6: cyan
24 ('#00cdcd', '#00ffff'), # 6: cyan
25 ('#e5e5e5', '#ffffff')) # 7: white
25 ('#e5e5e5', '#ffffff')) # 7: white
26 self.reset()
26 self.reset()
27
27
28 def set_code(self, code):
28 def set_code(self, code):
29 """ Set attributes based on code.
29 """ Set attributes based on code.
30 """
30 """
31 if code == 0:
31 if code == 0:
32 self.reset()
32 self.reset()
33 elif code == 1:
33 elif code == 1:
34 self.intensity = 1
34 self.intensity = 1
35 self.bold = True
35 self.bold = True
36 elif code == 3:
36 elif code == 3:
37 self.italic = True
37 self.italic = True
38 elif code == 4:
38 elif code == 4:
39 self.underline = True
39 self.underline = True
40 elif code == 22:
40 elif code == 22:
41 self.intensity = 0
41 self.intensity = 0
42 self.bold = False
42 self.bold = False
43 elif code == 23:
43 elif code == 23:
44 self.italic = False
44 self.italic = False
45 elif code == 24:
45 elif code == 24:
46 self.underline = False
46 self.underline = False
47 elif code >= 30 and code <= 37:
47 elif code >= 30 and code <= 37:
48 self.foreground_color = code - 30
48 self.foreground_color = code - 30
49 elif code == 39:
49 elif code == 39:
50 self.foreground_color = None
50 self.foreground_color = None
51 elif code >= 40 and code <= 47:
51 elif code >= 40 and code <= 47:
52 self.background_color = code - 40
52 self.background_color = code - 40
53 elif code == 49:
53 elif code == 49:
54 self.background_color = None
54 self.background_color = None
55
55
56 def reset(self):
56 def reset(self):
57 """ Reset attributs to their default values.
57 """ Reset attributs to their default values.
58 """
58 """
59 self.intensity = 0
59 self.intensity = 0
60 self.italic = False
60 self.italic = False
61 self.bold = False
61 self.bold = False
62 self.underline = False
62 self.underline = False
63 self.foreground_color = None
63 self.foreground_color = None
64 self.background_color = None
64 self.background_color = None
65
65
66
66
67 class QtAnsiCodeProcessor(AnsiCodeProcessor):
67 class QtAnsiCodeProcessor(AnsiCodeProcessor):
68 """ Translates ANSI escape codes into QTextCharFormats.
68 """ Translates ANSI escape codes into QTextCharFormats.
69 """
69 """
70
70
71 def get_format(self):
71 def get_format(self):
72 """ Returns a QTextCharFormat that encodes the current style attributes.
72 """ Returns a QTextCharFormat that encodes the current style attributes.
73 """
73 """
74 format = QtGui.QTextCharFormat()
74 format = QtGui.QTextCharFormat()
75
75
76 # Set foreground color
76 # Set foreground color
77 if self.foreground_color is not None:
77 if self.foreground_color is not None:
78 color = self.ansi_colors[self.foreground_color][self.intensity]
78 color = self.ansi_colors[self.foreground_color][self.intensity]
79 format.setForeground(QtGui.QColor(color))
79 format.setForeground(QtGui.QColor(color))
80
80
81 # Set background color
81 # Set background color
82 if self.background_color is not None:
82 if self.background_color is not None:
83 color = self.ansi_colors[self.background_color][self.intensity]
83 color = self.ansi_colors[self.background_color][self.intensity]
84 format.setBackground(QtGui.QColor(color))
84 format.setBackground(QtGui.QColor(color))
85
85
86 # Set font weight/style options
86 # Set font weight/style options
87 if self.bold:
87 if self.bold:
88 format.setFontWeight(QtGui.QFont.Bold)
88 format.setFontWeight(QtGui.QFont.Bold)
89 else:
89 else:
90 format.setFontWeight(QtGui.QFont.Normal)
90 format.setFontWeight(QtGui.QFont.Normal)
91 format.setFontItalic(self.italic)
91 format.setFontItalic(self.italic)
92 format.setFontUnderline(self.underline)
92 format.setFontUnderline(self.underline)
93
93
94 return format
94 return format
95
95
96
96
97 class ConsoleWidget(QtGui.QPlainTextEdit):
97 class ConsoleWidget(QtGui.QPlainTextEdit):
98 """ Base class for console-type widgets. This class is mainly concerned with
98 """ Base class for console-type widgets. This class is mainly concerned with
99 dealing with the prompt, keeping the cursor inside the editing line, and
99 dealing with the prompt, keeping the cursor inside the editing line, and
100 handling ANSI escape sequences.
100 handling ANSI escape sequences.
101 """
101 """
102
102
103 # Whether to process ANSI escape codes.
103 # Whether to process ANSI escape codes.
104 ansi_codes = True
104 ansi_codes = True
105
105
106 # The maximum number of lines of text before truncation.
106 # The maximum number of lines of text before truncation.
107 buffer_size = 500
107 buffer_size = 500
108
108
109 # Whether to use a CompletionWidget or plain text output for tab completion.
109 # Whether to use a CompletionWidget or plain text output for tab completion.
110 gui_completion = True
110 gui_completion = True
111
111
112 # Whether to override ShortcutEvents for the keybindings defined by this
112 # Whether to override ShortcutEvents for the keybindings defined by this
113 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
113 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
114 # priority (when it has focus) over, e.g., window-level menu shortcuts.
114 # priority (when it has focus) over, e.g., window-level menu shortcuts.
115 override_shortcuts = False
115 override_shortcuts = False
116
116
117 # Protected class variables.
117 # Protected class variables.
118 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
118 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
119 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
119 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
120 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
120 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
121 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
121 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
122 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
122 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
123 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
123 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
124 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
124 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
125 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
125 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
126 _shortcuts = set(_ctrl_down_remap.keys() +
126 _shortcuts = set(_ctrl_down_remap.keys() +
127 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
127 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
128
128
129 #---------------------------------------------------------------------------
129 #---------------------------------------------------------------------------
130 # 'QObject' interface
130 # 'QObject' interface
131 #---------------------------------------------------------------------------
131 #---------------------------------------------------------------------------
132
132
133 def __init__(self, parent=None):
133 def __init__(self, parent=None):
134 QtGui.QPlainTextEdit.__init__(self, parent)
134 QtGui.QPlainTextEdit.__init__(self, parent)
135
135
136 # Initialize protected variables.
136 # Initialize protected variables.
137 self._ansi_processor = QtAnsiCodeProcessor()
137 self._ansi_processor = QtAnsiCodeProcessor()
138 self._completion_widget = CompletionWidget(self)
138 self._completion_widget = CompletionWidget(self)
139 self._continuation_prompt = '> '
139 self._continuation_prompt = '> '
140 self._executing = False
140 self._executing = False
141 self._prompt = ''
141 self._prompt = ''
142 self._prompt_pos = 0
142 self._prompt_pos = 0
143 self._reading = False
143 self._reading = False
144
144
145 # Set a monospaced font.
145 # Set a monospaced font.
146 self.reset_font()
146 self.reset_font()
147
147
148 # Define a custom context menu.
148 # Define a custom context menu.
149 self._context_menu = QtGui.QMenu(self)
149 self._context_menu = QtGui.QMenu(self)
150
150
151 copy_action = QtGui.QAction('Copy', self)
151 copy_action = QtGui.QAction('Copy', self)
152 copy_action.triggered.connect(self.copy)
152 copy_action.triggered.connect(self.copy)
153 self.copyAvailable.connect(copy_action.setEnabled)
153 self.copyAvailable.connect(copy_action.setEnabled)
154 self._context_menu.addAction(copy_action)
154 self._context_menu.addAction(copy_action)
155
155
156 self._paste_action = QtGui.QAction('Paste', self)
156 self._paste_action = QtGui.QAction('Paste', self)
157 self._paste_action.triggered.connect(self.paste)
157 self._paste_action.triggered.connect(self.paste)
158 self._context_menu.addAction(self._paste_action)
158 self._context_menu.addAction(self._paste_action)
159 self._context_menu.addSeparator()
159 self._context_menu.addSeparator()
160
160
161 select_all_action = QtGui.QAction('Select All', self)
161 select_all_action = QtGui.QAction('Select All', self)
162 select_all_action.triggered.connect(self.selectAll)
162 select_all_action.triggered.connect(self.selectAll)
163 self._context_menu.addAction(select_all_action)
163 self._context_menu.addAction(select_all_action)
164
164
165 def event(self, event):
165 def event(self, event):
166 """ Reimplemented to override shortcuts, if necessary.
166 """ Reimplemented to override shortcuts, if necessary.
167 """
167 """
168 # On Mac OS, it is always unnecessary to override shortcuts, hence the
168 # On Mac OS, it is always unnecessary to override shortcuts, hence the
169 # check below. Users should just use the Control key instead of the
169 # check below. Users should just use the Control key instead of the
170 # Command key.
170 # Command key.
171 if self.override_shortcuts and \
171 if self.override_shortcuts and \
172 sys.platform != 'darwin' and \
172 sys.platform != 'darwin' and \
173 event.type() == QtCore.QEvent.ShortcutOverride and \
173 event.type() == QtCore.QEvent.ShortcutOverride and \
174 self._control_down(event.modifiers()) and \
174 self._control_down(event.modifiers()) and \
175 event.key() in self._shortcuts:
175 event.key() in self._shortcuts:
176 event.accept()
176 event.accept()
177 return True
177 return True
178 else:
178 else:
179 return QtGui.QPlainTextEdit.event(self, event)
179 return QtGui.QPlainTextEdit.event(self, event)
180
180
181 #---------------------------------------------------------------------------
181 #---------------------------------------------------------------------------
182 # 'QWidget' interface
182 # 'QWidget' interface
183 #---------------------------------------------------------------------------
183 #---------------------------------------------------------------------------
184
184
185 def contextMenuEvent(self, event):
185 def contextMenuEvent(self, event):
186 """ Reimplemented to create a menu without destructive actions like
186 """ Reimplemented to create a menu without destructive actions like
187 'Cut' and 'Delete'.
187 'Cut' and 'Delete'.
188 """
188 """
189 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
189 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
190 self._paste_action.setEnabled(not clipboard_empty)
190 self._paste_action.setEnabled(not clipboard_empty)
191
191
192 self._context_menu.exec_(event.globalPos())
192 self._context_menu.exec_(event.globalPos())
193
193
194 def keyPressEvent(self, event):
194 def keyPressEvent(self, event):
195 """ Reimplemented to create a console-like interface.
195 """ Reimplemented to create a console-like interface.
196 """
196 """
197 intercepted = False
197 intercepted = False
198 cursor = self.textCursor()
198 cursor = self.textCursor()
199 position = cursor.position()
199 position = cursor.position()
200 key = event.key()
200 key = event.key()
201 ctrl_down = self._control_down(event.modifiers())
201 ctrl_down = self._control_down(event.modifiers())
202 alt_down = event.modifiers() & QtCore.Qt.AltModifier
202 alt_down = event.modifiers() & QtCore.Qt.AltModifier
203 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
203 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
204
204
205 # Even though we have reimplemented 'paste', the C++ level slot is still
205 # Even though we have reimplemented 'paste', the C++ level slot is still
206 # called by Qt. So we intercept the key press here.
206 # called by Qt. So we intercept the key press here.
207 if event.matches(QtGui.QKeySequence.Paste):
207 if event.matches(QtGui.QKeySequence.Paste):
208 self.paste()
208 self.paste()
209 intercepted = True
209 intercepted = True
210
210
211 elif ctrl_down:
211 elif ctrl_down:
212 if key in self._ctrl_down_remap:
212 if key in self._ctrl_down_remap:
213 ctrl_down = False
213 ctrl_down = False
214 key = self._ctrl_down_remap[key]
214 key = self._ctrl_down_remap[key]
215 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
215 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
216 QtCore.Qt.NoModifier)
216 QtCore.Qt.NoModifier)
217
217
218 elif key == QtCore.Qt.Key_K:
218 elif key == QtCore.Qt.Key_K:
219 if self._in_buffer(position):
219 if self._in_buffer(position):
220 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
220 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
221 QtGui.QTextCursor.KeepAnchor)
221 QtGui.QTextCursor.KeepAnchor)
222 cursor.removeSelectedText()
222 cursor.removeSelectedText()
223 intercepted = True
223 intercepted = True
224
224
225 elif key == QtCore.Qt.Key_X:
225 elif key == QtCore.Qt.Key_X:
226 intercepted = True
226 intercepted = True
227
227
228 elif key == QtCore.Qt.Key_Y:
228 elif key == QtCore.Qt.Key_Y:
229 self.paste()
229 self.paste()
230 intercepted = True
230 intercepted = True
231
231
232 elif alt_down:
232 elif alt_down:
233 if key == QtCore.Qt.Key_B:
233 if key == QtCore.Qt.Key_B:
234 self.setTextCursor(self._get_word_start_cursor(position))
234 self.setTextCursor(self._get_word_start_cursor(position))
235 intercepted = True
235 intercepted = True
236
236
237 elif key == QtCore.Qt.Key_F:
237 elif key == QtCore.Qt.Key_F:
238 self.setTextCursor(self._get_word_end_cursor(position))
238 self.setTextCursor(self._get_word_end_cursor(position))
239 intercepted = True
239 intercepted = True
240
240
241 elif key == QtCore.Qt.Key_Backspace:
241 elif key == QtCore.Qt.Key_Backspace:
242 cursor = self._get_word_start_cursor(position)
242 cursor = self._get_word_start_cursor(position)
243 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
243 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
244 cursor.removeSelectedText()
244 cursor.removeSelectedText()
245 intercepted = True
245 intercepted = True
246
246
247 elif key == QtCore.Qt.Key_D:
247 elif key == QtCore.Qt.Key_D:
248 cursor = self._get_word_end_cursor(position)
248 cursor = self._get_word_end_cursor(position)
249 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
249 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
250 cursor.removeSelectedText()
250 cursor.removeSelectedText()
251 intercepted = True
251 intercepted = True
252
252
253 if self._completion_widget.isVisible():
253 if self._completion_widget.isVisible():
254 self._completion_widget.keyPressEvent(event)
254 self._completion_widget.keyPressEvent(event)
255 intercepted = event.isAccepted()
255 intercepted = event.isAccepted()
256
256
257 else:
257 else:
258 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
258 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
259 if self._reading:
259 if self._reading:
260 self._reading = False
260 self._reading = False
261 elif not self._executing:
261 elif not self._executing:
262 self.execute(interactive=True)
262 self.execute(interactive=True)
263 intercepted = True
263 intercepted = True
264
264
265 elif key == QtCore.Qt.Key_Up:
265 elif key == QtCore.Qt.Key_Up:
266 if self._reading or not self._up_pressed():
266 if self._reading or not self._up_pressed():
267 intercepted = True
267 intercepted = True
268 else:
268 else:
269 prompt_line = self._get_prompt_cursor().blockNumber()
269 prompt_line = self._get_prompt_cursor().blockNumber()
270 intercepted = cursor.blockNumber() <= prompt_line
270 intercepted = cursor.blockNumber() <= prompt_line
271
271
272 elif key == QtCore.Qt.Key_Down:
272 elif key == QtCore.Qt.Key_Down:
273 if self._reading or not self._down_pressed():
273 if self._reading or not self._down_pressed():
274 intercepted = True
274 intercepted = True
275 else:
275 else:
276 end_line = self._get_end_cursor().blockNumber()
276 end_line = self._get_end_cursor().blockNumber()
277 intercepted = cursor.blockNumber() == end_line
277 intercepted = cursor.blockNumber() == end_line
278
278
279 elif key == QtCore.Qt.Key_Tab:
279 elif key == QtCore.Qt.Key_Tab:
280 if self._reading:
280 if self._reading:
281 intercepted = False
281 intercepted = False
282 else:
282 else:
283 intercepted = not self._tab_pressed()
283 intercepted = not self._tab_pressed()
284
284
285 elif key == QtCore.Qt.Key_Left:
285 elif key == QtCore.Qt.Key_Left:
286 intercepted = not self._in_buffer(position - 1)
286 intercepted = not self._in_buffer(position - 1)
287
287
288 elif key == QtCore.Qt.Key_Home:
288 elif key == QtCore.Qt.Key_Home:
289 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
289 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
290 start_pos = cursor.position()
290 start_pos = cursor.position()
291 start_line = cursor.blockNumber()
291 start_line = cursor.blockNumber()
292 if start_line == self._get_prompt_cursor().blockNumber():
292 if start_line == self._get_prompt_cursor().blockNumber():
293 start_pos += len(self._prompt)
293 start_pos += len(self._prompt)
294 else:
294 else:
295 start_pos += len(self._continuation_prompt)
295 start_pos += len(self._continuation_prompt)
296 if shift_down and self._in_buffer(position):
296 if shift_down and self._in_buffer(position):
297 self._set_selection(position, start_pos)
297 self._set_selection(position, start_pos)
298 else:
298 else:
299 self._set_position(start_pos)
299 self._set_position(start_pos)
300 intercepted = True
300 intercepted = True
301
301
302 elif key == QtCore.Qt.Key_Backspace and not alt_down:
302 elif key == QtCore.Qt.Key_Backspace and not alt_down:
303
303
304 # Line deletion (remove continuation prompt)
304 # Line deletion (remove continuation prompt)
305 len_prompt = len(self._continuation_prompt)
305 len_prompt = len(self._continuation_prompt)
306 if cursor.columnNumber() == len_prompt and \
306 if cursor.columnNumber() == len_prompt and \
307 position != self._prompt_pos:
307 position != self._prompt_pos:
308 cursor.setPosition(position - len_prompt,
308 cursor.setPosition(position - len_prompt,
309 QtGui.QTextCursor.KeepAnchor)
309 QtGui.QTextCursor.KeepAnchor)
310 cursor.removeSelectedText()
310 cursor.removeSelectedText()
311
311
312 # Regular backwards deletion
312 # Regular backwards deletion
313 else:
313 else:
314 anchor = cursor.anchor()
314 anchor = cursor.anchor()
315 if anchor == position:
315 if anchor == position:
316 intercepted = not self._in_buffer(position - 1)
316 intercepted = not self._in_buffer(position - 1)
317 else:
317 else:
318 intercepted = not self._in_buffer(min(anchor, position))
318 intercepted = not self._in_buffer(min(anchor, position))
319
319
320 elif key == QtCore.Qt.Key_Delete:
320 elif key == QtCore.Qt.Key_Delete:
321 anchor = cursor.anchor()
321 anchor = cursor.anchor()
322 intercepted = not self._in_buffer(min(anchor, position))
322 intercepted = not self._in_buffer(min(anchor, position))
323
323
324 # Don't move cursor if control is down to allow copy-paste using
324 # Don't move cursor if control is down to allow copy-paste using
325 # the keyboard in any part of the buffer.
325 # the keyboard in any part of the buffer.
326 if not ctrl_down:
326 if not ctrl_down:
327 self._keep_cursor_in_buffer()
327 self._keep_cursor_in_buffer()
328
328
329 if not intercepted:
329 if not intercepted:
330 QtGui.QPlainTextEdit.keyPressEvent(self, event)
330 QtGui.QPlainTextEdit.keyPressEvent(self, event)
331
331
332 #--------------------------------------------------------------------------
332 #--------------------------------------------------------------------------
333 # 'QPlainTextEdit' interface
333 # 'QPlainTextEdit' interface
334 #--------------------------------------------------------------------------
334 #--------------------------------------------------------------------------
335
335
336 def appendPlainText(self, text):
336 def appendPlainText(self, text):
337 """ Reimplemented to not append text as a new paragraph, which doesn't
337 """ Reimplemented to not append text as a new paragraph, which doesn't
338 make sense for a console widget. Also, if enabled, handle ANSI
338 make sense for a console widget. Also, if enabled, handle ANSI
339 codes.
339 codes.
340 """
340 """
341 cursor = self.textCursor()
341 cursor = self.textCursor()
342 cursor.movePosition(QtGui.QTextCursor.End)
342 cursor.movePosition(QtGui.QTextCursor.End)
343
343
344 if self.ansi_codes:
344 if self.ansi_codes:
345 format = QtGui.QTextCharFormat()
345 format = QtGui.QTextCharFormat()
346 previous_end = 0
346 previous_end = 0
347 for match in self._ansi_pattern.finditer(text):
347 for match in self._ansi_pattern.finditer(text):
348 cursor.insertText(text[previous_end:match.start()], format)
348 cursor.insertText(text[previous_end:match.start()], format)
349 previous_end = match.end()
349 previous_end = match.end()
350 for code in match.group(1).split(';'):
350 for code in match.group(1).split(';'):
351 self._ansi_processor.set_code(int(code))
351 self._ansi_processor.set_code(int(code))
352 format = self._ansi_processor.get_format()
352 format = self._ansi_processor.get_format()
353 cursor.insertText(text[previous_end:], format)
353 cursor.insertText(text[previous_end:], format)
354 else:
354 else:
355 cursor.insertText(text)
355 cursor.insertText(text)
356
356
357 def clear(self, keep_input=False):
357 def clear(self, keep_input=False):
358 """ Reimplemented to write a new prompt. If 'keep_input' is set,
358 """ Reimplemented to write a new prompt. If 'keep_input' is set,
359 restores the old input buffer when the new prompt is written.
359 restores the old input buffer when the new prompt is written.
360 """
360 """
361 super(ConsoleWidget, self).clear()
361 super(ConsoleWidget, self).clear()
362
362
363 if keep_input:
363 if keep_input:
364 input_buffer = self.input_buffer
364 input_buffer = self.input_buffer
365 self._show_prompt()
365 self._show_prompt()
366 if keep_input:
366 if keep_input:
367 self.input_buffer = input_buffer
367 self.input_buffer = input_buffer
368
368
369 def paste(self):
369 def paste(self):
370 """ Reimplemented to ensure that text is pasted in the editing region.
370 """ Reimplemented to ensure that text is pasted in the editing region.
371 """
371 """
372 self._keep_cursor_in_buffer()
372 self._keep_cursor_in_buffer()
373 QtGui.QPlainTextEdit.paste(self)
373 QtGui.QPlainTextEdit.paste(self)
374
374
375 def print_(self, printer):
375 def print_(self, printer):
376 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
376 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
377 slot has the wrong signature.
377 slot has the wrong signature.
378 """
378 """
379 QtGui.QPlainTextEdit.print_(self, printer)
379 QtGui.QPlainTextEdit.print_(self, printer)
380
380
381 #---------------------------------------------------------------------------
381 #---------------------------------------------------------------------------
382 # 'ConsoleWidget' public interface
382 # 'ConsoleWidget' public interface
383 #---------------------------------------------------------------------------
383 #---------------------------------------------------------------------------
384
384
385 def execute(self, interactive=False):
385 def execute(self, source=None, hidden=False, interactive=False):
386 """ Execute the text in the input buffer. Returns whether the input
386 """ Executes source or the input buffer, possibly prompting for more
387 buffer was completely processed and a new prompt created.
387 input.
388 """
388
389 self.appendPlainText('\n')
389 Parameters:
390 self._executing_input_buffer = self.input_buffer
390 -----------
391 self._executing = True
391 source : str, optional
392 self._prompt_finished()
392
393 return self._execute(interactive=interactive)
393 The source to execute. If not specified, the input buffer will be
394 used. If specified and 'hidden' is False, the input buffer will be
395 replaced with the source before execution.
396
397 hidden : bool, optional (default False)
398
399 If set, no output will be shown and the prompt will not be modified.
400 In other words, it will be completely invisible to the user that
401 an execution has occurred.
402
403 interactive : bool, optional (default False)
404
405 Whether the console is to treat the source as having been manually
406 entered by the user. The effect of this parameter depends on the
407 subclass implementation.
408
409 Returns:
410 --------
411 A boolean indicating whether the source was executed.
412 """
413 if not hidden:
414 if source is not None:
415 self.input_buffer = source
416
417 self.appendPlainText('\n')
418 self._executing_input_buffer = self.input_buffer
419 self._executing = True
420 self._prompt_finished()
421
422 real_source = self.input_buffer if source is None else source
423 complete = self._is_complete(real_source, interactive)
424 if complete:
425 if not hidden:
426 # The maximum block count is only in effect during execution.
427 # This ensures that _prompt_pos does not become invalid due to
428 # text truncation.
429 self.setMaximumBlockCount(self.buffer_size)
430 self._execute(real_source, hidden)
431 elif hidden:
432 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
433 else:
434 self._show_continuation_prompt()
435
436 return complete
394
437
395 def _get_input_buffer(self):
438 def _get_input_buffer(self):
396 """ The text that the user has entered entered at the current prompt.
439 """ The text that the user has entered entered at the current prompt.
397 """
440 """
398 # If we're executing, the input buffer may not even exist anymore due to
441 # If we're executing, the input buffer may not even exist anymore due to
399 # the limit imposed by 'buffer_size'. Therefore, we store it.
442 # the limit imposed by 'buffer_size'. Therefore, we store it.
400 if self._executing:
443 if self._executing:
401 return self._executing_input_buffer
444 return self._executing_input_buffer
402
445
403 cursor = self._get_end_cursor()
446 cursor = self._get_end_cursor()
404 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
447 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
405
448
406 # Use QTextDocumentFragment intermediate object because it strips
449 # Use QTextDocumentFragment intermediate object because it strips
407 # out the Unicode line break characters that Qt insists on inserting.
450 # out the Unicode line break characters that Qt insists on inserting.
408 input_buffer = str(cursor.selection().toPlainText())
451 input_buffer = str(cursor.selection().toPlainText())
409
452
410 # Strip out continuation prompts.
453 # Strip out continuation prompts.
411 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
454 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
412
455
413 def _set_input_buffer(self, string):
456 def _set_input_buffer(self, string):
414 """ Replaces the text in the input buffer with 'string'.
457 """ Replaces the text in the input buffer with 'string'.
415 """
458 """
416 # Add continuation prompts where necessary.
459 # Add continuation prompts where necessary.
417 lines = string.splitlines()
460 lines = string.splitlines()
418 for i in xrange(1, len(lines)):
461 for i in xrange(1, len(lines)):
419 lines[i] = self._continuation_prompt + lines[i]
462 lines[i] = self._continuation_prompt + lines[i]
420 string = '\n'.join(lines)
463 string = '\n'.join(lines)
421
464
422 # Replace buffer with new text.
465 # Replace buffer with new text.
423 cursor = self._get_end_cursor()
466 cursor = self._get_end_cursor()
424 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
467 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
425 cursor.insertText(string)
468 cursor.insertText(string)
426 self.moveCursor(QtGui.QTextCursor.End)
469 self.moveCursor(QtGui.QTextCursor.End)
427
470
428 input_buffer = property(_get_input_buffer, _set_input_buffer)
471 input_buffer = property(_get_input_buffer, _set_input_buffer)
429
472
430 def _get_input_buffer_cursor_line(self):
473 def _get_input_buffer_cursor_line(self):
431 """ The text in the line of the input buffer in which the user's cursor
474 """ The text in the line of the input buffer in which the user's cursor
432 rests. Returns a string if there is such a line; otherwise, None.
475 rests. Returns a string if there is such a line; otherwise, None.
433 """
476 """
434 if self._executing:
477 if self._executing:
435 return None
478 return None
436 cursor = self.textCursor()
479 cursor = self.textCursor()
437 if cursor.position() >= self._prompt_pos:
480 if cursor.position() >= self._prompt_pos:
438 text = str(cursor.block().text())
481 text = str(cursor.block().text())
439 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
482 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
440 return text[len(self._prompt):]
483 return text[len(self._prompt):]
441 else:
484 else:
442 return text[len(self._continuation_prompt):]
485 return text[len(self._continuation_prompt):]
443 else:
486 else:
444 return None
487 return None
445
488
446 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
489 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
447
490
448 def _get_font(self):
491 def _get_font(self):
449 """ The base font being used by the ConsoleWidget.
492 """ The base font being used by the ConsoleWidget.
450 """
493 """
451 return self.document().defaultFont()
494 return self.document().defaultFont()
452
495
453 def _set_font(self, font):
496 def _set_font(self, font):
454 """ Sets the base font for the ConsoleWidget to the specified QFont.
497 """ Sets the base font for the ConsoleWidget to the specified QFont.
455 """
498 """
456 self._completion_widget.setFont(font)
499 self._completion_widget.setFont(font)
457 self.document().setDefaultFont(font)
500 self.document().setDefaultFont(font)
458
501
459 font = property(_get_font, _set_font)
502 font = property(_get_font, _set_font)
460
503
461 def reset_font(self):
504 def reset_font(self):
462 """ Sets the font to the default fixed-width font for this platform.
505 """ Sets the font to the default fixed-width font for this platform.
463 """
506 """
464 if sys.platform == 'win32':
507 if sys.platform == 'win32':
465 name = 'Courier'
508 name = 'Courier'
466 elif sys.platform == 'darwin':
509 elif sys.platform == 'darwin':
467 name = 'Monaco'
510 name = 'Monaco'
468 else:
511 else:
469 name = 'Monospace'
512 name = 'Monospace'
470 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
513 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
471 font.setStyleHint(QtGui.QFont.TypeWriter)
514 font.setStyleHint(QtGui.QFont.TypeWriter)
472 self._set_font(font)
515 self._set_font(font)
473
516
474 #---------------------------------------------------------------------------
517 #---------------------------------------------------------------------------
475 # 'ConsoleWidget' abstract interface
518 # 'ConsoleWidget' abstract interface
476 #---------------------------------------------------------------------------
519 #---------------------------------------------------------------------------
477
520
478 def _execute(self, interactive):
521 def _is_complete(self, source, interactive):
479 """ Called to execute the input buffer. When triggered by an the enter
522 """ Returns whether 'source' can be executed. When triggered by an
480 key press, 'interactive' is True; otherwise, it is False. Returns
523 Enter/Return key press, 'interactive' is True; otherwise, it is
481 whether the input buffer was completely processed and a new prompt
524 False.
482 created.
525 """
526 raise NotImplementedError
527
528 def _execute(self, source, hidden):
529 """ Execute 'source'. If 'hidden', do not show any output.
483 """
530 """
484 raise NotImplementedError
531 raise NotImplementedError
485
532
486 def _prompt_started_hook(self):
533 def _prompt_started_hook(self):
487 """ Called immediately after a new prompt is displayed.
534 """ Called immediately after a new prompt is displayed.
488 """
535 """
489 pass
536 pass
490
537
491 def _prompt_finished_hook(self):
538 def _prompt_finished_hook(self):
492 """ Called immediately after a prompt is finished, i.e. when some input
539 """ Called immediately after a prompt is finished, i.e. when some input
493 will be processed and a new prompt displayed.
540 will be processed and a new prompt displayed.
494 """
541 """
495 pass
542 pass
496
543
497 def _up_pressed(self):
544 def _up_pressed(self):
498 """ Called when the up key is pressed. Returns whether to continue
545 """ Called when the up key is pressed. Returns whether to continue
499 processing the event.
546 processing the event.
500 """
547 """
501 return True
548 return True
502
549
503 def _down_pressed(self):
550 def _down_pressed(self):
504 """ Called when the down key is pressed. Returns whether to continue
551 """ Called when the down key is pressed. Returns whether to continue
505 processing the event.
552 processing the event.
506 """
553 """
507 return True
554 return True
508
555
509 def _tab_pressed(self):
556 def _tab_pressed(self):
510 """ Called when the tab key is pressed. Returns whether to continue
557 """ Called when the tab key is pressed. Returns whether to continue
511 processing the event.
558 processing the event.
512 """
559 """
513 return False
560 return False
514
561
515 #--------------------------------------------------------------------------
562 #--------------------------------------------------------------------------
516 # 'ConsoleWidget' protected interface
563 # 'ConsoleWidget' protected interface
517 #--------------------------------------------------------------------------
564 #--------------------------------------------------------------------------
518
565
519 def _control_down(self, modifiers):
566 def _control_down(self, modifiers):
520 """ Given a KeyboardModifiers flags object, return whether the Control
567 """ Given a KeyboardModifiers flags object, return whether the Control
521 key is down (on Mac OS, treat the Command key as a synonym for
568 key is down (on Mac OS, treat the Command key as a synonym for
522 Control).
569 Control).
523 """
570 """
524 down = bool(modifiers & QtCore.Qt.ControlModifier)
571 down = bool(modifiers & QtCore.Qt.ControlModifier)
525
572
526 # Note: on Mac OS, ControlModifier corresponds to the Command key while
573 # Note: on Mac OS, ControlModifier corresponds to the Command key while
527 # MetaModifier corresponds to the Control key.
574 # MetaModifier corresponds to the Control key.
528 if sys.platform == 'darwin':
575 if sys.platform == 'darwin':
529 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
576 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
530
577
531 return down
578 return down
532
579
533 def _complete_with_items(self, cursor, items):
580 def _complete_with_items(self, cursor, items):
534 """ Performs completion with 'items' at the specified cursor location.
581 """ Performs completion with 'items' at the specified cursor location.
535 """
582 """
536 if len(items) == 1:
583 if len(items) == 1:
537 cursor.setPosition(self.textCursor().position(),
584 cursor.setPosition(self.textCursor().position(),
538 QtGui.QTextCursor.KeepAnchor)
585 QtGui.QTextCursor.KeepAnchor)
539 cursor.insertText(items[0])
586 cursor.insertText(items[0])
540 elif len(items) > 1:
587 elif len(items) > 1:
541 if self.gui_completion:
588 if self.gui_completion:
542 self._completion_widget.show_items(cursor, items)
589 self._completion_widget.show_items(cursor, items)
543 else:
590 else:
544 text = '\n'.join(items) + '\n'
591 text = '\n'.join(items) + '\n'
545 self._write_text_keeping_prompt(text)
592 self._write_text_keeping_prompt(text)
546
593
547 def _get_end_cursor(self):
594 def _get_end_cursor(self):
548 """ Convenience method that returns a cursor for the last character.
595 """ Convenience method that returns a cursor for the last character.
549 """
596 """
550 cursor = self.textCursor()
597 cursor = self.textCursor()
551 cursor.movePosition(QtGui.QTextCursor.End)
598 cursor.movePosition(QtGui.QTextCursor.End)
552 return cursor
599 return cursor
553
600
554 def _get_prompt_cursor(self):
601 def _get_prompt_cursor(self):
555 """ Convenience method that returns a cursor for the prompt position.
602 """ Convenience method that returns a cursor for the prompt position.
556 """
603 """
557 cursor = self.textCursor()
604 cursor = self.textCursor()
558 cursor.setPosition(self._prompt_pos)
605 cursor.setPosition(self._prompt_pos)
559 return cursor
606 return cursor
560
607
561 def _get_selection_cursor(self, start, end):
608 def _get_selection_cursor(self, start, end):
562 """ Convenience method that returns a cursor with text selected between
609 """ Convenience method that returns a cursor with text selected between
563 the positions 'start' and 'end'.
610 the positions 'start' and 'end'.
564 """
611 """
565 cursor = self.textCursor()
612 cursor = self.textCursor()
566 cursor.setPosition(start)
613 cursor.setPosition(start)
567 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
614 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
568 return cursor
615 return cursor
569
616
570 def _get_word_start_cursor(self, position):
617 def _get_word_start_cursor(self, position):
571 """ Find the start of the word to the left the given position. If a
618 """ Find the start of the word to the left the given position. If a
572 sequence of non-word characters precedes the first word, skip over
619 sequence of non-word characters precedes the first word, skip over
573 them. (This emulates the behavior of bash, emacs, etc.)
620 them. (This emulates the behavior of bash, emacs, etc.)
574 """
621 """
575 document = self.document()
622 document = self.document()
576 position -= 1
623 position -= 1
577 while self._in_buffer(position) and \
624 while self._in_buffer(position) and \
578 not document.characterAt(position).isLetterOrNumber():
625 not document.characterAt(position).isLetterOrNumber():
579 position -= 1
626 position -= 1
580 while self._in_buffer(position) and \
627 while self._in_buffer(position) and \
581 document.characterAt(position).isLetterOrNumber():
628 document.characterAt(position).isLetterOrNumber():
582 position -= 1
629 position -= 1
583 cursor = self.textCursor()
630 cursor = self.textCursor()
584 cursor.setPosition(position + 1)
631 cursor.setPosition(position + 1)
585 return cursor
632 return cursor
586
633
587 def _get_word_end_cursor(self, position):
634 def _get_word_end_cursor(self, position):
588 """ Find the end of the word to the right the given position. If a
635 """ Find the end of the word to the right the given position. If a
589 sequence of non-word characters precedes the first word, skip over
636 sequence of non-word characters precedes the first word, skip over
590 them. (This emulates the behavior of bash, emacs, etc.)
637 them. (This emulates the behavior of bash, emacs, etc.)
591 """
638 """
592 document = self.document()
639 document = self.document()
593 end = self._get_end_cursor().position()
640 end = self._get_end_cursor().position()
594 while position < end and \
641 while position < end and \
595 not document.characterAt(position).isLetterOrNumber():
642 not document.characterAt(position).isLetterOrNumber():
596 position += 1
643 position += 1
597 while position < end and \
644 while position < end and \
598 document.characterAt(position).isLetterOrNumber():
645 document.characterAt(position).isLetterOrNumber():
599 position += 1
646 position += 1
600 cursor = self.textCursor()
647 cursor = self.textCursor()
601 cursor.setPosition(position)
648 cursor.setPosition(position)
602 return cursor
649 return cursor
603
650
604 def _prompt_started(self):
651 def _prompt_started(self):
605 """ Called immediately after a new prompt is displayed.
652 """ Called immediately after a new prompt is displayed.
606 """
653 """
607 # Temporarily disable the maximum block count to permit undo/redo.
654 # Temporarily disable the maximum block count to permit undo/redo and
655 # to ensure that the prompt position does not change due to truncation.
608 self.setMaximumBlockCount(0)
656 self.setMaximumBlockCount(0)
609 self.setUndoRedoEnabled(True)
657 self.setUndoRedoEnabled(True)
610
658
611 self.setReadOnly(False)
659 self.setReadOnly(False)
612 self.moveCursor(QtGui.QTextCursor.End)
660 self.moveCursor(QtGui.QTextCursor.End)
613 self.centerCursor()
661 self.centerCursor()
614
662
615 self._executing = False
663 self._executing = False
616 self._prompt_started_hook()
664 self._prompt_started_hook()
617
665
618 def _prompt_finished(self):
666 def _prompt_finished(self):
619 """ Called immediately after a prompt is finished, i.e. when some input
667 """ Called immediately after a prompt is finished, i.e. when some input
620 will be processed and a new prompt displayed.
668 will be processed and a new prompt displayed.
621 """
669 """
622 # This has the (desired) side effect of disabling the undo/redo history.
670 self.setUndoRedoEnabled(False)
623 self.setMaximumBlockCount(self.buffer_size)
624
625 self.setReadOnly(True)
671 self.setReadOnly(True)
626 self._prompt_finished_hook()
672 self._prompt_finished_hook()
627
673
628 def _set_position(self, position):
674 def _set_position(self, position):
629 """ Convenience method to set the position of the cursor.
675 """ Convenience method to set the position of the cursor.
630 """
676 """
631 cursor = self.textCursor()
677 cursor = self.textCursor()
632 cursor.setPosition(position)
678 cursor.setPosition(position)
633 self.setTextCursor(cursor)
679 self.setTextCursor(cursor)
634
680
635 def _set_selection(self, start, end):
681 def _set_selection(self, start, end):
636 """ Convenience method to set the current selected text.
682 """ Convenience method to set the current selected text.
637 """
683 """
638 self.setTextCursor(self._get_selection_cursor(start, end))
684 self.setTextCursor(self._get_selection_cursor(start, end))
639
685
640 def _show_prompt(self, prompt=None):
686 def _show_prompt(self, prompt=None):
641 """ Writes a new prompt at the end of the buffer. If 'prompt' is not
687 """ Writes a new prompt at the end of the buffer. If 'prompt' is not
642 specified, uses the previous prompt.
688 specified, uses the previous prompt.
643 """
689 """
644 if prompt is not None:
690 if prompt is not None:
645 self._prompt = prompt
691 self._prompt = prompt
646 self.appendPlainText('\n' + self._prompt)
692 self.appendPlainText('\n' + self._prompt)
647 self._prompt_pos = self._get_end_cursor().position()
693 self._prompt_pos = self._get_end_cursor().position()
648 self._prompt_started()
694 self._prompt_started()
649
695
650 def _show_continuation_prompt(self):
696 def _show_continuation_prompt(self):
651 """ Writes a new continuation prompt at the end of the buffer.
697 """ Writes a new continuation prompt at the end of the buffer.
652 """
698 """
653 self.appendPlainText(self._continuation_prompt)
699 self.appendPlainText(self._continuation_prompt)
654 self._prompt_started()
700 self._prompt_started()
655
701
656 def _write_text_keeping_prompt(self, text):
702 def _write_text_keeping_prompt(self, text):
657 """ Writes 'text' after the current prompt, then restores the old prompt
703 """ Writes 'text' after the current prompt, then restores the old prompt
658 with its old input buffer.
704 with its old input buffer.
659 """
705 """
660 input_buffer = self.input_buffer
706 input_buffer = self.input_buffer
661 self.appendPlainText('\n')
707 self.appendPlainText('\n')
662 self._prompt_finished()
708 self._prompt_finished()
663
709
664 self.appendPlainText(text)
710 self.appendPlainText(text)
665 self._show_prompt()
711 self._show_prompt()
666 self.input_buffer = input_buffer
712 self.input_buffer = input_buffer
667
713
668 def _in_buffer(self, position):
714 def _in_buffer(self, position):
669 """ Returns whether the given position is inside the editing region.
715 """ Returns whether the given position is inside the editing region.
670 """
716 """
671 return position >= self._prompt_pos
717 return position >= self._prompt_pos
672
718
673 def _keep_cursor_in_buffer(self):
719 def _keep_cursor_in_buffer(self):
674 """ Ensures that the cursor is inside the editing region. Returns
720 """ Ensures that the cursor is inside the editing region. Returns
675 whether the cursor was moved.
721 whether the cursor was moved.
676 """
722 """
677 cursor = self.textCursor()
723 cursor = self.textCursor()
678 if cursor.position() < self._prompt_pos:
724 if cursor.position() < self._prompt_pos:
679 cursor.movePosition(QtGui.QTextCursor.End)
725 cursor.movePosition(QtGui.QTextCursor.End)
680 self.setTextCursor(cursor)
726 self.setTextCursor(cursor)
681 return True
727 return True
682 else:
728 else:
683 return False
729 return False
684
730
685
731
686 class HistoryConsoleWidget(ConsoleWidget):
732 class HistoryConsoleWidget(ConsoleWidget):
687 """ A ConsoleWidget that keeps a history of the commands that have been
733 """ A ConsoleWidget that keeps a history of the commands that have been
688 executed.
734 executed.
689 """
735 """
690
736
691 #---------------------------------------------------------------------------
737 #---------------------------------------------------------------------------
692 # 'QObject' interface
738 # 'QObject' interface
693 #---------------------------------------------------------------------------
739 #---------------------------------------------------------------------------
694
740
695 def __init__(self, parent=None):
741 def __init__(self, parent=None):
696 super(HistoryConsoleWidget, self).__init__(parent)
742 super(HistoryConsoleWidget, self).__init__(parent)
697
743
698 self._history = []
744 self._history = []
699 self._history_index = 0
745 self._history_index = 0
700
746
701 #---------------------------------------------------------------------------
747 #---------------------------------------------------------------------------
702 # 'ConsoleWidget' public interface
748 # 'ConsoleWidget' public interface
703 #---------------------------------------------------------------------------
749 #---------------------------------------------------------------------------
704
750
705 def execute(self, interactive=False):
751 def execute(self, source=None, hidden=False, interactive=False):
706 """ Reimplemented to the store history.
752 """ Reimplemented to the store history.
707 """
753 """
708 stripped = self.input_buffer.rstrip()
754 if source is None and not hidden:
709 executed = super(HistoryConsoleWidget, self).execute(interactive)
755 history = self.input_buffer.rstrip()
710 if executed:
756
711 self._history.append(stripped)
757 executed = super(HistoryConsoleWidget, self).execute(
758 source, hidden, interactive)
759
760 if executed and not hidden:
761 self._history.append(history)
712 self._history_index = len(self._history)
762 self._history_index = len(self._history)
763
713 return executed
764 return executed
714
765
715 #---------------------------------------------------------------------------
766 #---------------------------------------------------------------------------
716 # 'ConsoleWidget' abstract interface
767 # 'ConsoleWidget' abstract interface
717 #---------------------------------------------------------------------------
768 #---------------------------------------------------------------------------
718
769
719 def _up_pressed(self):
770 def _up_pressed(self):
720 """ Called when the up key is pressed. Returns whether to continue
771 """ Called when the up key is pressed. Returns whether to continue
721 processing the event.
772 processing the event.
722 """
773 """
723 prompt_cursor = self._get_prompt_cursor()
774 prompt_cursor = self._get_prompt_cursor()
724 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
775 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
725 self.history_previous()
776 self.history_previous()
726
777
727 # Go to the first line of prompt for seemless history scrolling.
778 # Go to the first line of prompt for seemless history scrolling.
728 cursor = self._get_prompt_cursor()
779 cursor = self._get_prompt_cursor()
729 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
780 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
730 self.setTextCursor(cursor)
781 self.setTextCursor(cursor)
731
782
732 return False
783 return False
733 return True
784 return True
734
785
735 def _down_pressed(self):
786 def _down_pressed(self):
736 """ Called when the down key is pressed. Returns whether to continue
787 """ Called when the down key is pressed. Returns whether to continue
737 processing the event.
788 processing the event.
738 """
789 """
739 end_cursor = self._get_end_cursor()
790 end_cursor = self._get_end_cursor()
740 if self.textCursor().blockNumber() == end_cursor.blockNumber():
791 if self.textCursor().blockNumber() == end_cursor.blockNumber():
741 self.history_next()
792 self.history_next()
742 return False
793 return False
743 return True
794 return True
744
795
745 #---------------------------------------------------------------------------
796 #---------------------------------------------------------------------------
746 # 'HistoryConsoleWidget' interface
797 # 'HistoryConsoleWidget' interface
747 #---------------------------------------------------------------------------
798 #---------------------------------------------------------------------------
748
799
749 def history_previous(self):
800 def history_previous(self):
750 """ If possible, set the input buffer to the previous item in the
801 """ If possible, set the input buffer to the previous item in the
751 history.
802 history.
752 """
803 """
753 if self._history_index > 0:
804 if self._history_index > 0:
754 self._history_index -= 1
805 self._history_index -= 1
755 self.input_buffer = self._history[self._history_index]
806 self.input_buffer = self._history[self._history_index]
756
807
757 def history_next(self):
808 def history_next(self):
758 """ Set the input buffer to the next item in the history, or a blank
809 """ Set the input buffer to the next item in the history, or a blank
759 line if there is no subsequent item.
810 line if there is no subsequent item.
760 """
811 """
761 if self._history_index < len(self._history):
812 if self._history_index < len(self._history):
762 self._history_index += 1
813 self._history_index += 1
763 if self._history_index < len(self._history):
814 if self._history_index < len(self._history):
764 self.input_buffer = self._history[self._history_index]
815 self.input_buffer = self._history[self._history_index]
765 else:
816 else:
766 self.input_buffer = ''
817 self.input_buffer = ''
@@ -1,326 +1,330 b''
1 # Standard library imports
1 # Standard library imports
2 import signal
2 import signal
3
3
4 # System library imports
4 # System library imports
5 from pygments.lexers import PythonLexer
5 from pygments.lexers import PythonLexer
6 from PyQt4 import QtCore, QtGui
6 from PyQt4 import QtCore, QtGui
7 import zmq
7 import zmq
8
8
9 # Local imports
9 # Local imports
10 from IPython.core.inputsplitter import InputSplitter
10 from IPython.core.inputsplitter import InputSplitter
11 from call_tip_widget import CallTipWidget
11 from call_tip_widget import CallTipWidget
12 from completion_lexer import CompletionLexer
12 from completion_lexer import CompletionLexer
13 from console_widget import HistoryConsoleWidget
13 from console_widget import HistoryConsoleWidget
14 from pygments_highlighter import PygmentsHighlighter
14 from pygments_highlighter import PygmentsHighlighter
15
15
16
16
17 class FrontendHighlighter(PygmentsHighlighter):
17 class FrontendHighlighter(PygmentsHighlighter):
18 """ A Python PygmentsHighlighter that can be turned on and off and which
18 """ A Python PygmentsHighlighter that can be turned on and off and which
19 knows about continuation prompts.
19 knows about continuation prompts.
20 """
20 """
21
21
22 def __init__(self, frontend):
22 def __init__(self, frontend):
23 PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
23 PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
24 self._current_offset = 0
24 self._current_offset = 0
25 self._frontend = frontend
25 self._frontend = frontend
26 self.highlighting_on = False
26 self.highlighting_on = False
27
27
28 def highlightBlock(self, qstring):
28 def highlightBlock(self, qstring):
29 """ Highlight a block of text. Reimplemented to highlight selectively.
29 """ Highlight a block of text. Reimplemented to highlight selectively.
30 """
30 """
31 if self.highlighting_on:
31 if self.highlighting_on:
32 for prompt in (self._frontend._continuation_prompt,
32 for prompt in (self._frontend._continuation_prompt,
33 self._frontend._prompt):
33 self._frontend._prompt):
34 if qstring.startsWith(prompt):
34 if qstring.startsWith(prompt):
35 qstring.remove(0, len(prompt))
35 qstring.remove(0, len(prompt))
36 self._current_offset = len(prompt)
36 self._current_offset = len(prompt)
37 break
37 break
38 PygmentsHighlighter.highlightBlock(self, qstring)
38 PygmentsHighlighter.highlightBlock(self, qstring)
39
39
40 def setFormat(self, start, count, format):
40 def setFormat(self, start, count, format):
41 """ Reimplemented to avoid highlighting continuation prompts.
41 """ Reimplemented to avoid highlighting continuation prompts.
42 """
42 """
43 start += self._current_offset
43 start += self._current_offset
44 PygmentsHighlighter.setFormat(self, start, count, format)
44 PygmentsHighlighter.setFormat(self, start, count, format)
45
45
46
46
47 class FrontendWidget(HistoryConsoleWidget):
47 class FrontendWidget(HistoryConsoleWidget):
48 """ A Qt frontend for a generic Python kernel.
48 """ A Qt frontend for a generic Python kernel.
49 """
49 """
50
50
51 # Emitted when an 'execute_reply' is received from the kernel.
51 # Emitted when an 'execute_reply' is received from the kernel.
52 executed = QtCore.pyqtSignal(object)
52 executed = QtCore.pyqtSignal(object)
53
53
54 #---------------------------------------------------------------------------
54 #---------------------------------------------------------------------------
55 # 'QObject' interface
55 # 'QObject' interface
56 #---------------------------------------------------------------------------
56 #---------------------------------------------------------------------------
57
57
58 def __init__(self, parent=None):
58 def __init__(self, parent=None):
59 super(FrontendWidget, self).__init__(parent)
59 super(FrontendWidget, self).__init__(parent)
60
60
61 # ConsoleWidget protected variables.
61 # ConsoleWidget protected variables.
62 self._continuation_prompt = '... '
62 self._continuation_prompt = '... '
63 self._prompt = '>>> '
63 self._prompt = '>>> '
64
64
65 # FrontendWidget protected variables.
65 # FrontendWidget protected variables.
66 self._call_tip_widget = CallTipWidget(self)
66 self._call_tip_widget = CallTipWidget(self)
67 self._completion_lexer = CompletionLexer(PythonLexer())
67 self._completion_lexer = CompletionLexer(PythonLexer())
68 self._hidden = True
68 self._hidden = True
69 self._highlighter = FrontendHighlighter(self)
69 self._highlighter = FrontendHighlighter(self)
70 self._input_splitter = InputSplitter(input_mode='replace')
70 self._input_splitter = InputSplitter(input_mode='replace')
71 self._kernel_manager = None
71 self._kernel_manager = None
72
72
73 self.document().contentsChange.connect(self._document_contents_change)
73 self.document().contentsChange.connect(self._document_contents_change)
74
74
75 #---------------------------------------------------------------------------
75 #---------------------------------------------------------------------------
76 # 'QWidget' interface
76 # 'QWidget' interface
77 #---------------------------------------------------------------------------
77 #---------------------------------------------------------------------------
78
78
79 def focusOutEvent(self, event):
79 def focusOutEvent(self, event):
80 """ Reimplemented to hide calltips.
80 """ Reimplemented to hide calltips.
81 """
81 """
82 self._call_tip_widget.hide()
82 self._call_tip_widget.hide()
83 super(FrontendWidget, self).focusOutEvent(event)
83 super(FrontendWidget, self).focusOutEvent(event)
84
84
85 def keyPressEvent(self, event):
85 def keyPressEvent(self, event):
86 """ Reimplemented to allow calltips to process events and to send
86 """ Reimplemented to allow calltips to process events and to send
87 signals to the kernel.
87 signals to the kernel.
88 """
88 """
89 if self._executing and event.key() == QtCore.Qt.Key_C and \
89 if self._executing and event.key() == QtCore.Qt.Key_C and \
90 self._control_down(event.modifiers()):
90 self._control_down(event.modifiers()):
91 self._interrupt_kernel()
91 self._interrupt_kernel()
92 else:
92 else:
93 self._call_tip_widget.keyPressEvent(event)
93 if self._call_tip_widget.isVisible():
94 self._call_tip_widget.keyPressEvent(event)
94 super(FrontendWidget, self).keyPressEvent(event)
95 super(FrontendWidget, self).keyPressEvent(event)
95
96
96 #---------------------------------------------------------------------------
97 #---------------------------------------------------------------------------
97 # 'ConsoleWidget' abstract interface
98 # 'ConsoleWidget' abstract interface
98 #---------------------------------------------------------------------------
99 #---------------------------------------------------------------------------
99
100
100 def _execute(self, interactive):
101 def _is_complete(self, source, interactive):
101 """ Called to execute the input buffer. When triggered by an the enter
102 """ Returns whether 'source' can be completely processed and a new
102 key press, 'interactive' is True; otherwise, it is False. Returns
103 prompt created. When triggered by an Enter/Return key press,
103 whether the input buffer was completely processed and a new prompt
104 'interactive' is True; otherwise, it is False.
104 created.
105 """
105 """
106 return self.execute_source(self.input_buffer, interactive=interactive)
106 complete = self._input_splitter.push(source)
107 if interactive:
108 complete = not self._input_splitter.push_accepts_more()
109 return complete
110
111 def _execute(self, source, hidden):
112 """ Execute 'source'. If 'hidden', do not show any output.
113 """
114 self.kernel_manager.xreq_channel.execute(source)
115 self._hidden = hidden
107
116
108 def _prompt_started_hook(self):
117 def _prompt_started_hook(self):
109 """ Called immediately after a new prompt is displayed.
118 """ Called immediately after a new prompt is displayed.
110 """
119 """
111 self._highlighter.highlighting_on = True
120 self._highlighter.highlighting_on = True
112
121
122 # Auto-indent if this is a continuation prompt.
123 if self._get_prompt_cursor().blockNumber() != \
124 self._get_end_cursor().blockNumber():
125 self.appendPlainText(' ' * self._input_splitter.indent_spaces)
126
113 def _prompt_finished_hook(self):
127 def _prompt_finished_hook(self):
114 """ Called immediately after a prompt is finished, i.e. when some input
128 """ Called immediately after a prompt is finished, i.e. when some input
115 will be processed and a new prompt displayed.
129 will be processed and a new prompt displayed.
116 """
130 """
117 self._highlighter.highlighting_on = False
131 self._highlighter.highlighting_on = False
118
132
119 def _tab_pressed(self):
133 def _tab_pressed(self):
120 """ Called when the tab key is pressed. Returns whether to continue
134 """ Called when the tab key is pressed. Returns whether to continue
121 processing the event.
135 processing the event.
122 """
136 """
123 self._keep_cursor_in_buffer()
137 self._keep_cursor_in_buffer()
124 cursor = self.textCursor()
138 cursor = self.textCursor()
125 if not self._complete():
139 if not self._complete():
126 cursor.insertText(' ')
140 cursor.insertText(' ')
127 return False
141 return False
128
142
129 #---------------------------------------------------------------------------
143 #---------------------------------------------------------------------------
130 # 'FrontendWidget' interface
144 # 'FrontendWidget' interface
131 #---------------------------------------------------------------------------
145 #---------------------------------------------------------------------------
132
146
133 def execute_source(self, source, hidden=False, interactive=False):
134 """ Execute a string containing Python code. If 'hidden', no output is
135 shown. Returns whether the source executed (i.e., returns True only
136 if no more input is necessary).
137 """
138 self._input_splitter.push(source)
139 executed = not self._input_splitter.push_accepts_more()
140 if executed:
141 self.kernel_manager.xreq_channel.execute(source)
142 self._hidden = hidden
143 else:
144 self._show_continuation_prompt()
145 self.appendPlainText(' ' * self._input_splitter.indent_spaces)
146 return executed
147
148 def execute_file(self, path, hidden=False):
147 def execute_file(self, path, hidden=False):
149 """ Attempts to execute file with 'path'. If 'hidden', no output is
148 """ Attempts to execute file with 'path'. If 'hidden', no output is
150 shown.
149 shown.
151 """
150 """
152 self.execute_source('run %s' % path, hidden=hidden)
151 self.execute('execfile("%s")' % path, hidden=hidden)
153
152
154 def _get_kernel_manager(self):
153 def _get_kernel_manager(self):
155 """ Returns the current kernel manager.
154 """ Returns the current kernel manager.
156 """
155 """
157 return self._kernel_manager
156 return self._kernel_manager
158
157
159 def _set_kernel_manager(self, kernel_manager):
158 def _set_kernel_manager(self, kernel_manager):
160 """ Disconnect from the current kernel manager (if any) and set a new
159 """ Disconnect from the current kernel manager (if any) and set a new
161 kernel manager.
160 kernel manager.
162 """
161 """
163 # Disconnect the old kernel manager, if necessary.
162 # Disconnect the old kernel manager, if necessary.
164 if self._kernel_manager is not None:
163 if self._kernel_manager is not None:
165 self._kernel_manager.started_listening.disconnect(
164 self._kernel_manager.started_listening.disconnect(
166 self._started_listening)
165 self._started_listening)
167 self._kernel_manager.stopped_listening.disconnect(
166 self._kernel_manager.stopped_listening.disconnect(
168 self._stopped_listening)
167 self._stopped_listening)
169
168
170 # Disconnect the old kernel manager's channels.
169 # Disconnect the old kernel manager's channels.
171 sub = self._kernel_manager.sub_channel
170 sub = self._kernel_manager.sub_channel
172 xreq = self._kernel_manager.xreq_channel
171 xreq = self._kernel_manager.xreq_channel
173 sub.message_received.disconnect(self._handle_sub)
172 sub.message_received.disconnect(self._handle_sub)
174 xreq.execute_reply.disconnect(self._handle_execute_reply)
173 xreq.execute_reply.disconnect(self._handle_execute_reply)
175 xreq.complete_reply.disconnect(self._handle_complete_reply)
174 xreq.complete_reply.disconnect(self._handle_complete_reply)
176 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
175 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
177
176
178 # Handle the case where the old kernel manager is still listening.
177 # Handle the case where the old kernel manager is still listening.
179 if self._kernel_manager.is_listening:
178 if self._kernel_manager.is_listening:
180 self._stopped_listening()
179 self._stopped_listening()
181
180
182 # Set the new kernel manager.
181 # Set the new kernel manager.
183 self._kernel_manager = kernel_manager
182 self._kernel_manager = kernel_manager
184 if kernel_manager is None:
183 if kernel_manager is None:
185 return
184 return
186
185
187 # Connect the new kernel manager.
186 # Connect the new kernel manager.
188 kernel_manager.started_listening.connect(self._started_listening)
187 kernel_manager.started_listening.connect(self._started_listening)
189 kernel_manager.stopped_listening.connect(self._stopped_listening)
188 kernel_manager.stopped_listening.connect(self._stopped_listening)
190
189
191 # Connect the new kernel manager's channels.
190 # Connect the new kernel manager's channels.
192 sub = kernel_manager.sub_channel
191 sub = kernel_manager.sub_channel
193 xreq = kernel_manager.xreq_channel
192 xreq = kernel_manager.xreq_channel
194 sub.message_received.connect(self._handle_sub)
193 sub.message_received.connect(self._handle_sub)
195 xreq.execute_reply.connect(self._handle_execute_reply)
194 xreq.execute_reply.connect(self._handle_execute_reply)
196 xreq.complete_reply.connect(self._handle_complete_reply)
195 xreq.complete_reply.connect(self._handle_complete_reply)
197 xreq.object_info_reply.connect(self._handle_object_info_reply)
196 xreq.object_info_reply.connect(self._handle_object_info_reply)
198
197
199 # Handle the case where the kernel manager started listening before
198 # Handle the case where the kernel manager started listening before
200 # we connected.
199 # we connected.
201 if kernel_manager.is_listening:
200 if kernel_manager.is_listening:
202 self._started_listening()
201 self._started_listening()
203
202
204 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
203 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
205
204
206 #---------------------------------------------------------------------------
205 #---------------------------------------------------------------------------
207 # 'FrontendWidget' protected interface
206 # 'FrontendWidget' protected interface
208 #---------------------------------------------------------------------------
207 #---------------------------------------------------------------------------
209
208
210 def _call_tip(self):
209 def _call_tip(self):
211 """ Shows a call tip, if appropriate, at the current cursor location.
210 """ Shows a call tip, if appropriate, at the current cursor location.
212 """
211 """
213 # Decide if it makes sense to show a call tip
212 # Decide if it makes sense to show a call tip
214 cursor = self.textCursor()
213 cursor = self.textCursor()
215 cursor.movePosition(QtGui.QTextCursor.Left)
214 cursor.movePosition(QtGui.QTextCursor.Left)
216 document = self.document()
215 document = self.document()
217 if document.characterAt(cursor.position()).toAscii() != '(':
216 if document.characterAt(cursor.position()).toAscii() != '(':
218 return False
217 return False
219 context = self._get_context(cursor)
218 context = self._get_context(cursor)
220 if not context:
219 if not context:
221 return False
220 return False
222
221
223 # Send the metadata request to the kernel
222 # Send the metadata request to the kernel
224 name = '.'.join(context)
223 name = '.'.join(context)
225 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
224 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
226 self._calltip_pos = self.textCursor().position()
225 self._calltip_pos = self.textCursor().position()
227 return True
226 return True
228
227
229 def _complete(self):
228 def _complete(self):
230 """ Performs completion at the current cursor location.
229 """ Performs completion at the current cursor location.
231 """
230 """
232 # Decide if it makes sense to do completion
231 # Decide if it makes sense to do completion
233 context = self._get_context()
232 context = self._get_context()
234 if not context:
233 if not context:
235 return False
234 return False
236
235
237 # Send the completion request to the kernel
236 # Send the completion request to the kernel
238 text = '.'.join(context)
237 text = '.'.join(context)
239 self._complete_id = self.kernel_manager.xreq_channel.complete(
238 self._complete_id = self.kernel_manager.xreq_channel.complete(
240 text, self.input_buffer_cursor_line, self.input_buffer)
239 text, self.input_buffer_cursor_line, self.input_buffer)
241 self._complete_pos = self.textCursor().position()
240 self._complete_pos = self.textCursor().position()
242 return True
241 return True
243
242
244 def _get_context(self, cursor=None):
243 def _get_context(self, cursor=None):
245 """ Gets the context at the current cursor location.
244 """ Gets the context at the current cursor location.
246 """
245 """
247 if cursor is None:
246 if cursor is None:
248 cursor = self.textCursor()
247 cursor = self.textCursor()
249 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
248 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
250 QtGui.QTextCursor.KeepAnchor)
249 QtGui.QTextCursor.KeepAnchor)
251 text = unicode(cursor.selectedText())
250 text = unicode(cursor.selectedText())
252 return self._completion_lexer.get_context(text)
251 return self._completion_lexer.get_context(text)
253
252
254 def _interrupt_kernel(self):
253 def _interrupt_kernel(self):
255 """ Attempts to the interrupt the kernel.
254 """ Attempts to the interrupt the kernel.
256 """
255 """
257 if self.kernel_manager.has_kernel:
256 if self.kernel_manager.has_kernel:
258 self.kernel_manager.signal_kernel(signal.SIGINT)
257 self.kernel_manager.signal_kernel(signal.SIGINT)
259 else:
258 else:
260 self.appendPlainText('Kernel process is either remote or '
259 self.appendPlainText('Kernel process is either remote or '
261 'unspecified. Cannot interrupt.\n')
260 'unspecified. Cannot interrupt.\n')
262
261
263 #------ Signal handlers ----------------------------------------------------
262 #------ Signal handlers ----------------------------------------------------
264
263
265 def _document_contents_change(self, position, removed, added):
264 def _document_contents_change(self, position, removed, added):
266 """ Called whenever the document's content changes. Display a calltip
265 """ Called whenever the document's content changes. Display a calltip
267 if appropriate.
266 if appropriate.
268 """
267 """
269 # Calculate where the cursor should be *after* the change:
268 # Calculate where the cursor should be *after* the change:
270 position += added
269 position += added
271
270
272 document = self.document()
271 document = self.document()
273 if position == self.textCursor().position():
272 if position == self.textCursor().position():
274 self._call_tip()
273 self._call_tip()
275
274
276 def _handle_sub(self, omsg):
275 def _handle_sub(self, omsg):
277 if not self._hidden:
276 if self._hidden:
278 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
277 return
279 if handler is not None:
278 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
280 handler(omsg)
279 if handler is not None:
280 handler(omsg)
281
281
282 def _handle_pyout(self, omsg):
282 def _handle_pyout(self, omsg):
283 session = omsg['parent_header']['session']
283 session = omsg['parent_header']['session']
284 if session == self.kernel_manager.session.session:
284 if session == self.kernel_manager.session.session:
285 self.appendPlainText(omsg['content']['data'] + '\n')
285 self.appendPlainText(omsg['content']['data'] + '\n')
286
286
287 def _handle_stream(self, omsg):
287 def _handle_stream(self, omsg):
288 self.appendPlainText(omsg['content']['data'])
288 self.appendPlainText(omsg['content']['data'])
289 self.moveCursor(QtGui.QTextCursor.End)
289
290
290 def _handle_execute_reply(self, rep):
291 def _handle_execute_reply(self, rep):
292 if self._hidden:
293 return
294
291 # Make sure that all output from the SUB channel has been processed
295 # Make sure that all output from the SUB channel has been processed
292 # before writing a new prompt.
296 # before writing a new prompt.
293 self.kernel_manager.sub_channel.flush()
297 self.kernel_manager.sub_channel.flush()
294
298
295 content = rep['content']
299 content = rep['content']
296 status = content['status']
300 status = content['status']
297 if status == 'error':
301 if status == 'error':
298 self.appendPlainText(content['traceback'][-1])
302 self.appendPlainText(content['traceback'][-1])
299 elif status == 'aborted':
303 elif status == 'aborted':
300 text = "ERROR: ABORTED\n"
304 text = "ERROR: ABORTED\n"
301 self.appendPlainText(text)
305 self.appendPlainText(text)
302 self._hidden = True
306 self._hidden = True
303 self._show_prompt()
307 self._show_prompt()
304 self.executed.emit(rep)
308 self.executed.emit(rep)
305
309
306 def _handle_complete_reply(self, rep):
310 def _handle_complete_reply(self, rep):
307 cursor = self.textCursor()
311 cursor = self.textCursor()
308 if rep['parent_header']['msg_id'] == self._complete_id and \
312 if rep['parent_header']['msg_id'] == self._complete_id and \
309 cursor.position() == self._complete_pos:
313 cursor.position() == self._complete_pos:
310 text = '.'.join(self._get_context())
314 text = '.'.join(self._get_context())
311 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
315 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
312 self._complete_with_items(cursor, rep['content']['matches'])
316 self._complete_with_items(cursor, rep['content']['matches'])
313
317
314 def _handle_object_info_reply(self, rep):
318 def _handle_object_info_reply(self, rep):
315 cursor = self.textCursor()
319 cursor = self.textCursor()
316 if rep['parent_header']['msg_id'] == self._calltip_id and \
320 if rep['parent_header']['msg_id'] == self._calltip_id and \
317 cursor.position() == self._calltip_pos:
321 cursor.position() == self._calltip_pos:
318 doc = rep['content']['docstring']
322 doc = rep['content']['docstring']
319 if doc:
323 if doc:
320 self._call_tip_widget.show_docstring(doc)
324 self._call_tip_widget.show_docstring(doc)
321
325
322 def _started_listening(self):
326 def _started_listening(self):
323 self.clear()
327 self.clear()
324
328
325 def _stopped_listening(self):
329 def _stopped_listening(self):
326 pass
330 pass
@@ -1,100 +1,107 b''
1 # System library imports
1 # System library imports
2 from PyQt4 import QtCore, QtGui
2 from PyQt4 import QtCore, QtGui
3
3
4 # Local imports
4 # Local imports
5 from frontend_widget import FrontendWidget
5 from frontend_widget import FrontendWidget
6
6
7
7
8 class IPythonWidget(FrontendWidget):
8 class IPythonWidget(FrontendWidget):
9 """ A FrontendWidget for an IPython kernel.
9 """ A FrontendWidget for an IPython kernel.
10 """
10 """
11
11
12 #---------------------------------------------------------------------------
12 #---------------------------------------------------------------------------
13 # 'QObject' interface
13 # 'QObject' interface
14 #---------------------------------------------------------------------------
14 #---------------------------------------------------------------------------
15
15
16 def __init__(self, parent=None):
16 def __init__(self, parent=None):
17 super(IPythonWidget, self).__init__(parent)
17 super(IPythonWidget, self).__init__(parent)
18
18
19 self._magic_overrides = {}
19 self._magic_overrides = {}
20
20
21 #---------------------------------------------------------------------------
21 #---------------------------------------------------------------------------
22 # 'FrontendWidget' interface
22 # 'ConsoleWidget' abstract interface
23 #---------------------------------------------------------------------------
23 #---------------------------------------------------------------------------
24
24
25 def execute_source(self, source, hidden=False, interactive=False):
25 def _execute(self, source, hidden):
26 """ Reimplemented to override magic commands.
26 """ Reimplemented to override magic commands.
27 """
27 """
28 magic_source = source.strip()
28 magic_source = source.strip()
29 if magic_source.startswith('%'):
29 if magic_source.startswith('%'):
30 magic_source = magic_source[1:]
30 magic_source = magic_source[1:]
31 magic, sep, arguments = magic_source.partition(' ')
31 magic, sep, arguments = magic_source.partition(' ')
32 if not magic:
32 if not magic:
33 magic = magic_source
33 magic = magic_source
34
34
35 callback = self._magic_overrides.get(magic)
35 callback = self._magic_overrides.get(magic)
36 if callback:
36 if callback:
37 output = callback(arguments)
37 output = callback(arguments)
38 if output:
38 if output:
39 self.appendPlainText(output)
39 self.appendPlainText(output)
40 self._show_prompt('>>> ')
40 self._show_prompt()
41 return True
42 else:
41 else:
43 return super(IPythonWidget, self).execute_source(source, hidden,
42 super(IPythonWidget, self)._execute(source, hidden)
44 interactive)
43
44 #---------------------------------------------------------------------------
45 # 'FrontendWidget' interface
46 #---------------------------------------------------------------------------
47
48 def execute_file(self, path, hidden=False):
49 """ Reimplemented to use the 'run' magic.
50 """
51 self.execute('run %s' % path, hidden=hidden)
45
52
46 #---------------------------------------------------------------------------
53 #---------------------------------------------------------------------------
47 # 'IPythonWidget' interface
54 # 'IPythonWidget' interface
48 #---------------------------------------------------------------------------
55 #---------------------------------------------------------------------------
49
56
50 def set_magic_override(self, magic, callback):
57 def set_magic_override(self, magic, callback):
51 """ Overrides an IPython magic command. This magic will be intercepted
58 """ Overrides an IPython magic command. This magic will be intercepted
52 by the frontend rather than passed on to the kernel and 'callback'
59 by the frontend rather than passed on to the kernel and 'callback'
53 will be called with a single argument: a string of argument(s) for
60 will be called with a single argument: a string of argument(s) for
54 the magic. The callback can (optionally) return text to print to the
61 the magic. The callback can (optionally) return text to print to the
55 console.
62 console.
56 """
63 """
57 self._magic_overrides[magic] = callback
64 self._magic_overrides[magic] = callback
58
65
59 def remove_magic_override(self, magic):
66 def remove_magic_override(self, magic):
60 """ Removes the override for the specified magic, if there is one.
67 """ Removes the override for the specified magic, if there is one.
61 """
68 """
62 try:
69 try:
63 del self._magic_overrides[magic]
70 del self._magic_overrides[magic]
64 except KeyError:
71 except KeyError:
65 pass
72 pass
66
73
67
74
68 if __name__ == '__main__':
75 if __name__ == '__main__':
69 import signal
76 import signal
70 from IPython.frontend.qt.kernelmanager import QtKernelManager
77 from IPython.frontend.qt.kernelmanager import QtKernelManager
71
78
72 # Create a KernelManager.
79 # Create a KernelManager.
73 kernel_manager = QtKernelManager()
80 kernel_manager = QtKernelManager()
74 kernel_manager.start_kernel()
81 kernel_manager.start_kernel()
75 kernel_manager.start_listening()
82 kernel_manager.start_listening()
76
83
77 # Don't let Qt or ZMQ swallow KeyboardInterupts.
84 # Don't let Qt or ZMQ swallow KeyboardInterupts.
78 # FIXME: Gah, ZMQ swallows even custom signal handlers. So for now we leave
85 # FIXME: Gah, ZMQ swallows even custom signal handlers. So for now we leave
79 # behind a kernel process when Ctrl-C is pressed.
86 # behind a kernel process when Ctrl-C is pressed.
80 #def sigint_hook(signum, frame):
87 #def sigint_hook(signum, frame):
81 # QtGui.qApp.quit()
88 # QtGui.qApp.quit()
82 #signal.signal(signal.SIGINT, sigint_hook)
89 #signal.signal(signal.SIGINT, sigint_hook)
83 signal.signal(signal.SIGINT, signal.SIG_DFL)
90 signal.signal(signal.SIGINT, signal.SIG_DFL)
84
91
85 # Create the application, making sure to clean up nicely when we exit.
92 # Create the application, making sure to clean up nicely when we exit.
86 app = QtGui.QApplication([])
93 app = QtGui.QApplication([])
87 def quit_hook():
94 def quit_hook():
88 kernel_manager.stop_listening()
95 kernel_manager.stop_listening()
89 kernel_manager.kill_kernel()
96 kernel_manager.kill_kernel()
90 app.aboutToQuit.connect(quit_hook)
97 app.aboutToQuit.connect(quit_hook)
91
98
92 # Launch the application.
99 # Launch the application.
93 widget = IPythonWidget()
100 widget = IPythonWidget()
94 widget.kernel_manager = kernel_manager
101 widget.kernel_manager = kernel_manager
95 widget.setWindowTitle('Python')
102 widget.setWindowTitle('Python')
96 widget.resize(640, 480)
103 widget.resize(640, 480)
97 widget.show()
104 widget.show()
98 app.exec_()
105 app.exec_()
99
106
100
107
General Comments 0
You need to be logged in to leave comments. Login now