##// END OF EJS Templates
* IPythonWidget now has IPython-style prompts that are futher stylabla via CSS...
epatters -
Show More
@@ -1,891 +1,973 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. Some variables contain useful state
137 # information for subclasses; they should be considered read-only.
137 self._ansi_processor = QtAnsiCodeProcessor()
138 self._ansi_processor = QtAnsiCodeProcessor()
138 self._completion_widget = CompletionWidget(self)
139 self._completion_widget = CompletionWidget(self)
139 self._continuation_prompt = '> '
140 self._continuation_prompt = '> '
141 self._continuation_prompt_html = None
140 self._executing = False
142 self._executing = False
141 self._prompt = ''
143 self._prompt = ''
144 self._prompt_html = None
142 self._prompt_pos = 0
145 self._prompt_pos = 0
143 self._reading = False
146 self._reading = False
144 self._reading_callback = None
147 self._reading_callback = None
145
148
146 # Set a monospaced font.
149 # Set a monospaced font.
147 self.reset_font()
150 self.reset_font()
148
151
149 # Define a custom context menu.
152 # Define a custom context menu.
150 self._context_menu = QtGui.QMenu(self)
153 self._context_menu = QtGui.QMenu(self)
151
154
152 copy_action = QtGui.QAction('Copy', self)
155 copy_action = QtGui.QAction('Copy', self)
153 copy_action.triggered.connect(self.copy)
156 copy_action.triggered.connect(self.copy)
154 self.copyAvailable.connect(copy_action.setEnabled)
157 self.copyAvailable.connect(copy_action.setEnabled)
155 self._context_menu.addAction(copy_action)
158 self._context_menu.addAction(copy_action)
156
159
157 self._paste_action = QtGui.QAction('Paste', self)
160 self._paste_action = QtGui.QAction('Paste', self)
158 self._paste_action.triggered.connect(self.paste)
161 self._paste_action.triggered.connect(self.paste)
159 self._context_menu.addAction(self._paste_action)
162 self._context_menu.addAction(self._paste_action)
160 self._context_menu.addSeparator()
163 self._context_menu.addSeparator()
161
164
162 select_all_action = QtGui.QAction('Select All', self)
165 select_all_action = QtGui.QAction('Select All', self)
163 select_all_action.triggered.connect(self.selectAll)
166 select_all_action.triggered.connect(self.selectAll)
164 self._context_menu.addAction(select_all_action)
167 self._context_menu.addAction(select_all_action)
165
168
166 def event(self, event):
169 def event(self, event):
167 """ Reimplemented to override shortcuts, if necessary.
170 """ Reimplemented to override shortcuts, if necessary.
168 """
171 """
169 # On Mac OS, it is always unnecessary to override shortcuts, hence the
172 # On Mac OS, it is always unnecessary to override shortcuts, hence the
170 # check below. Users should just use the Control key instead of the
173 # check below. Users should just use the Control key instead of the
171 # Command key.
174 # Command key.
172 if self.override_shortcuts and \
175 if self.override_shortcuts and \
173 sys.platform != 'darwin' and \
176 sys.platform != 'darwin' and \
174 event.type() == QtCore.QEvent.ShortcutOverride and \
177 event.type() == QtCore.QEvent.ShortcutOverride and \
175 self._control_down(event.modifiers()) and \
178 self._control_down(event.modifiers()) and \
176 event.key() in self._shortcuts:
179 event.key() in self._shortcuts:
177 event.accept()
180 event.accept()
178 return True
181 return True
179 else:
182 else:
180 return QtGui.QPlainTextEdit.event(self, event)
183 return QtGui.QPlainTextEdit.event(self, event)
181
184
182 #---------------------------------------------------------------------------
185 #---------------------------------------------------------------------------
183 # 'QWidget' interface
186 # 'QWidget' interface
184 #---------------------------------------------------------------------------
187 #---------------------------------------------------------------------------
185
188
186 def contextMenuEvent(self, event):
189 def contextMenuEvent(self, event):
187 """ Reimplemented to create a menu without destructive actions like
190 """ Reimplemented to create a menu without destructive actions like
188 'Cut' and 'Delete'.
191 'Cut' and 'Delete'.
189 """
192 """
190 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
193 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
191 self._paste_action.setEnabled(not clipboard_empty)
194 self._paste_action.setEnabled(not clipboard_empty)
192
195
193 self._context_menu.exec_(event.globalPos())
196 self._context_menu.exec_(event.globalPos())
194
197
195 def dragMoveEvent(self, event):
198 def dragMoveEvent(self, event):
196 """ Reimplemented to disable dropping text.
199 """ Reimplemented to disable dropping text.
197 """
200 """
198 event.ignore()
201 event.ignore()
199
202
200 def keyPressEvent(self, event):
203 def keyPressEvent(self, event):
201 """ Reimplemented to create a console-like interface.
204 """ Reimplemented to create a console-like interface.
202 """
205 """
203 intercepted = False
206 intercepted = False
204 cursor = self.textCursor()
207 cursor = self.textCursor()
205 position = cursor.position()
208 position = cursor.position()
206 key = event.key()
209 key = event.key()
207 ctrl_down = self._control_down(event.modifiers())
210 ctrl_down = self._control_down(event.modifiers())
208 alt_down = event.modifiers() & QtCore.Qt.AltModifier
211 alt_down = event.modifiers() & QtCore.Qt.AltModifier
209 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
212 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
210
213
211 # Even though we have reimplemented 'paste', the C++ level slot is still
214 # Even though we have reimplemented 'paste', the C++ level slot is still
212 # called by Qt. So we intercept the key press here.
215 # called by Qt. So we intercept the key press here.
213 if event.matches(QtGui.QKeySequence.Paste):
216 if event.matches(QtGui.QKeySequence.Paste):
214 self.paste()
217 self.paste()
215 intercepted = True
218 intercepted = True
216
219
217 elif ctrl_down:
220 elif ctrl_down:
218 if key in self._ctrl_down_remap:
221 if key in self._ctrl_down_remap:
219 ctrl_down = False
222 ctrl_down = False
220 key = self._ctrl_down_remap[key]
223 key = self._ctrl_down_remap[key]
221 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
224 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
222 QtCore.Qt.NoModifier)
225 QtCore.Qt.NoModifier)
223
226
224 elif key == QtCore.Qt.Key_K:
227 elif key == QtCore.Qt.Key_K:
225 if self._in_buffer(position):
228 if self._in_buffer(position):
226 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
229 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
227 QtGui.QTextCursor.KeepAnchor)
230 QtGui.QTextCursor.KeepAnchor)
228 cursor.removeSelectedText()
231 cursor.removeSelectedText()
229 intercepted = True
232 intercepted = True
230
233
231 elif key == QtCore.Qt.Key_X:
234 elif key == QtCore.Qt.Key_X:
232 intercepted = True
235 intercepted = True
233
236
234 elif key == QtCore.Qt.Key_Y:
237 elif key == QtCore.Qt.Key_Y:
235 self.paste()
238 self.paste()
236 intercepted = True
239 intercepted = True
237
240
238 elif alt_down:
241 elif alt_down:
239 if key == QtCore.Qt.Key_B:
242 if key == QtCore.Qt.Key_B:
240 self.setTextCursor(self._get_word_start_cursor(position))
243 self.setTextCursor(self._get_word_start_cursor(position))
241 intercepted = True
244 intercepted = True
242
245
243 elif key == QtCore.Qt.Key_F:
246 elif key == QtCore.Qt.Key_F:
244 self.setTextCursor(self._get_word_end_cursor(position))
247 self.setTextCursor(self._get_word_end_cursor(position))
245 intercepted = True
248 intercepted = True
246
249
247 elif key == QtCore.Qt.Key_Backspace:
250 elif key == QtCore.Qt.Key_Backspace:
248 cursor = self._get_word_start_cursor(position)
251 cursor = self._get_word_start_cursor(position)
249 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
252 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
250 cursor.removeSelectedText()
253 cursor.removeSelectedText()
251 intercepted = True
254 intercepted = True
252
255
253 elif key == QtCore.Qt.Key_D:
256 elif key == QtCore.Qt.Key_D:
254 cursor = self._get_word_end_cursor(position)
257 cursor = self._get_word_end_cursor(position)
255 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
258 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
256 cursor.removeSelectedText()
259 cursor.removeSelectedText()
257 intercepted = True
260 intercepted = True
258
261
259 if self._completion_widget.isVisible():
262 if self._completion_widget.isVisible():
260 self._completion_widget.keyPressEvent(event)
263 self._completion_widget.keyPressEvent(event)
261 intercepted = event.isAccepted()
264 intercepted = event.isAccepted()
262
265
263 else:
266 else:
264 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
267 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
265 if self._reading:
268 if self._reading:
266 self.appendPlainText('\n')
269 self.appendPlainText('\n')
267 self._reading = False
270 self._reading = False
268 if self._reading_callback:
271 if self._reading_callback:
269 self._reading_callback()
272 self._reading_callback()
270 elif not self._executing:
273 elif not self._executing:
271 self.execute(interactive=True)
274 self.execute(interactive=True)
272 intercepted = True
275 intercepted = True
273
276
274 elif key == QtCore.Qt.Key_Up:
277 elif key == QtCore.Qt.Key_Up:
275 if self._reading or not self._up_pressed():
278 if self._reading or not self._up_pressed():
276 intercepted = True
279 intercepted = True
277 else:
280 else:
278 prompt_line = self._get_prompt_cursor().blockNumber()
281 prompt_line = self._get_prompt_cursor().blockNumber()
279 intercepted = cursor.blockNumber() <= prompt_line
282 intercepted = cursor.blockNumber() <= prompt_line
280
283
281 elif key == QtCore.Qt.Key_Down:
284 elif key == QtCore.Qt.Key_Down:
282 if self._reading or not self._down_pressed():
285 if self._reading or not self._down_pressed():
283 intercepted = True
286 intercepted = True
284 else:
287 else:
285 end_line = self._get_end_cursor().blockNumber()
288 end_line = self._get_end_cursor().blockNumber()
286 intercepted = cursor.blockNumber() == end_line
289 intercepted = cursor.blockNumber() == end_line
287
290
288 elif key == QtCore.Qt.Key_Tab:
291 elif key == QtCore.Qt.Key_Tab:
289 if self._reading:
292 if self._reading:
290 intercepted = False
293 intercepted = False
291 else:
294 else:
292 intercepted = not self._tab_pressed()
295 intercepted = not self._tab_pressed()
293
296
294 elif key == QtCore.Qt.Key_Left:
297 elif key == QtCore.Qt.Key_Left:
295 intercepted = not self._in_buffer(position - 1)
298 intercepted = not self._in_buffer(position - 1)
296
299
297 elif key == QtCore.Qt.Key_Home:
300 elif key == QtCore.Qt.Key_Home:
298 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
301 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
299 start_pos = cursor.position()
302 start_pos = cursor.position()
300 start_line = cursor.blockNumber()
303 start_line = cursor.blockNumber()
301 if start_line == self._get_prompt_cursor().blockNumber():
304 if start_line == self._get_prompt_cursor().blockNumber():
302 start_pos += len(self._prompt)
305 start_pos += len(self._prompt)
303 else:
306 else:
304 start_pos += len(self._continuation_prompt)
307 start_pos += len(self._continuation_prompt)
305 if shift_down and self._in_buffer(position):
308 if shift_down and self._in_buffer(position):
306 self._set_selection(position, start_pos)
309 self._set_selection(position, start_pos)
307 else:
310 else:
308 self._set_position(start_pos)
311 self._set_position(start_pos)
309 intercepted = True
312 intercepted = True
310
313
311 elif key == QtCore.Qt.Key_Backspace and not alt_down:
314 elif key == QtCore.Qt.Key_Backspace and not alt_down:
312
315
313 # Line deletion (remove continuation prompt)
316 # Line deletion (remove continuation prompt)
314 len_prompt = len(self._continuation_prompt)
317 len_prompt = len(self._continuation_prompt)
315 if not self._reading and \
318 if not self._reading and \
316 cursor.columnNumber() == len_prompt and \
319 cursor.columnNumber() == len_prompt and \
317 position != self._prompt_pos:
320 position != self._prompt_pos:
318 cursor.setPosition(position - len_prompt,
321 cursor.setPosition(position - len_prompt,
319 QtGui.QTextCursor.KeepAnchor)
322 QtGui.QTextCursor.KeepAnchor)
320 cursor.removeSelectedText()
323 cursor.removeSelectedText()
321
324
322 # Regular backwards deletion
325 # Regular backwards deletion
323 else:
326 else:
324 anchor = cursor.anchor()
327 anchor = cursor.anchor()
325 if anchor == position:
328 if anchor == position:
326 intercepted = not self._in_buffer(position - 1)
329 intercepted = not self._in_buffer(position - 1)
327 else:
330 else:
328 intercepted = not self._in_buffer(min(anchor, position))
331 intercepted = not self._in_buffer(min(anchor, position))
329
332
330 elif key == QtCore.Qt.Key_Delete:
333 elif key == QtCore.Qt.Key_Delete:
331 anchor = cursor.anchor()
334 anchor = cursor.anchor()
332 intercepted = not self._in_buffer(min(anchor, position))
335 intercepted = not self._in_buffer(min(anchor, position))
333
336
334 # Don't move cursor if control is down to allow copy-paste using
337 # Don't move cursor if control is down to allow copy-paste using
335 # the keyboard in any part of the buffer.
338 # the keyboard in any part of the buffer.
336 if not ctrl_down:
339 if not ctrl_down:
337 self._keep_cursor_in_buffer()
340 self._keep_cursor_in_buffer()
338
341
339 if not intercepted:
342 if not intercepted:
340 QtGui.QPlainTextEdit.keyPressEvent(self, event)
343 QtGui.QPlainTextEdit.keyPressEvent(self, event)
341
344
342 #--------------------------------------------------------------------------
345 #--------------------------------------------------------------------------
343 # 'QPlainTextEdit' interface
346 # 'QPlainTextEdit' interface
344 #--------------------------------------------------------------------------
347 #--------------------------------------------------------------------------
345
348
349 def appendHtml(self, html):
350 """ Reimplemented to not append HTML as a new paragraph, which doesn't
351 make sense for a console widget.
352 """
353 cursor = self._get_end_cursor()
354 cursor.insertHtml(html)
355
356 # After appending HTML, the text document "remembers" the current
357 # formatting, which means that subsequent calls to 'appendPlainText'
358 # will be formatted similarly, a behavior that we do not want. To
359 # prevent this, we make sure that the last character has no formatting.
360 cursor.movePosition(QtGui.QTextCursor.Left,
361 QtGui.QTextCursor.KeepAnchor)
362 if cursor.selection().toPlainText().trimmed().isEmpty():
363 # If the last character is whitespace, it doesn't matter how it's
364 # formatted, so just clear the formatting.
365 cursor.setCharFormat(QtGui.QTextCharFormat())
366 else:
367 # Otherwise, add an unformatted space.
368 cursor.movePosition(QtGui.QTextCursor.Right)
369 cursor.insertText(' ', QtGui.QTextCharFormat())
370
346 def appendPlainText(self, text):
371 def appendPlainText(self, text):
347 """ Reimplemented to not append text as a new paragraph, which doesn't
372 """ Reimplemented to not append text as a new paragraph, which doesn't
348 make sense for a console widget. Also, if enabled, handle ANSI
373 make sense for a console widget. Also, if enabled, handle ANSI
349 codes.
374 codes.
350 """
375 """
351 cursor = self.textCursor()
376 cursor = self._get_end_cursor()
352 cursor.movePosition(QtGui.QTextCursor.End)
353
354 if self.ansi_codes:
377 if self.ansi_codes:
355 format = QtGui.QTextCharFormat()
378 format = QtGui.QTextCharFormat()
356 previous_end = 0
379 previous_end = 0
357 for match in self._ansi_pattern.finditer(text):
380 for match in self._ansi_pattern.finditer(text):
358 cursor.insertText(text[previous_end:match.start()], format)
381 cursor.insertText(text[previous_end:match.start()], format)
359 previous_end = match.end()
382 previous_end = match.end()
360 for code in match.group(1).split(';'):
383 for code in match.group(1).split(';'):
361 self._ansi_processor.set_code(int(code))
384 self._ansi_processor.set_code(int(code))
362 format = self._ansi_processor.get_format()
385 format = self._ansi_processor.get_format()
363 cursor.insertText(text[previous_end:], format)
386 cursor.insertText(text[previous_end:], format)
364 else:
387 else:
365 cursor.insertText(text)
388 cursor.insertText(text)
366
389
367 def clear(self, keep_input=False):
390 def clear(self, keep_input=False):
368 """ Reimplemented to cancel reading and write a new prompt. If
391 """ Reimplemented to write a new prompt. If 'keep_input' is set,
369 'keep_input' is set, restores the old input buffer when the new
392 restores the old input buffer when the new prompt is written.
370 prompt is written.
371 """
393 """
372 QtGui.QPlainTextEdit.clear(self)
394 QtGui.QPlainTextEdit.clear(self)
373 input_buffer = ''
395 if keep_input:
374 if self._reading:
375 self._reading = False
376 elif keep_input:
377 input_buffer = self.input_buffer
396 input_buffer = self.input_buffer
378 self._show_prompt()
397 self._show_prompt()
379 self.input_buffer = input_buffer
398 if keep_input:
399 self.input_buffer = input_buffer
380
400
381 def paste(self):
401 def paste(self):
382 """ Reimplemented to ensure that text is pasted in the editing region.
402 """ Reimplemented to ensure that text is pasted in the editing region.
383 """
403 """
384 self._keep_cursor_in_buffer()
404 self._keep_cursor_in_buffer()
385 QtGui.QPlainTextEdit.paste(self)
405 QtGui.QPlainTextEdit.paste(self)
386
406
387 def print_(self, printer):
407 def print_(self, printer):
388 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
408 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
389 slot has the wrong signature.
409 slot has the wrong signature.
390 """
410 """
391 QtGui.QPlainTextEdit.print_(self, printer)
411 QtGui.QPlainTextEdit.print_(self, printer)
392
412
393 #---------------------------------------------------------------------------
413 #---------------------------------------------------------------------------
394 # 'ConsoleWidget' public interface
414 # 'ConsoleWidget' public interface
395 #---------------------------------------------------------------------------
415 #---------------------------------------------------------------------------
396
416
397 def execute(self, source=None, hidden=False, interactive=False):
417 def execute(self, source=None, hidden=False, interactive=False):
398 """ Executes source or the input buffer, possibly prompting for more
418 """ Executes source or the input buffer, possibly prompting for more
399 input.
419 input.
400
420
401 Parameters:
421 Parameters:
402 -----------
422 -----------
403 source : str, optional
423 source : str, optional
404
424
405 The source to execute. If not specified, the input buffer will be
425 The source to execute. If not specified, the input buffer will be
406 used. If specified and 'hidden' is False, the input buffer will be
426 used. If specified and 'hidden' is False, the input buffer will be
407 replaced with the source before execution.
427 replaced with the source before execution.
408
428
409 hidden : bool, optional (default False)
429 hidden : bool, optional (default False)
410
430
411 If set, no output will be shown and the prompt will not be modified.
431 If set, no output will be shown and the prompt will not be modified.
412 In other words, it will be completely invisible to the user that
432 In other words, it will be completely invisible to the user that
413 an execution has occurred.
433 an execution has occurred.
414
434
415 interactive : bool, optional (default False)
435 interactive : bool, optional (default False)
416
436
417 Whether the console is to treat the source as having been manually
437 Whether the console is to treat the source as having been manually
418 entered by the user. The effect of this parameter depends on the
438 entered by the user. The effect of this parameter depends on the
419 subclass implementation.
439 subclass implementation.
420
440
421 Raises:
441 Raises:
422 -------
442 -------
423 RuntimeError
443 RuntimeError
424 If incomplete input is given and 'hidden' is True. In this case,
444 If incomplete input is given and 'hidden' is True. In this case,
425 it not possible to prompt for more input.
445 it not possible to prompt for more input.
426
446
427 Returns:
447 Returns:
428 --------
448 --------
429 A boolean indicating whether the source was executed.
449 A boolean indicating whether the source was executed.
430 """
450 """
431 if not hidden:
451 if not hidden:
432 if source is not None:
452 if source is not None:
433 self.input_buffer = source
453 self.input_buffer = source
434
454
435 self.appendPlainText('\n')
455 self.appendPlainText('\n')
436 self._executing_input_buffer = self.input_buffer
456 self._executing_input_buffer = self.input_buffer
437 self._executing = True
457 self._executing = True
438 self._prompt_finished()
458 self._prompt_finished()
439
459
440 real_source = self.input_buffer if source is None else source
460 real_source = self.input_buffer if source is None else source
441 complete = self._is_complete(real_source, interactive)
461 complete = self._is_complete(real_source, interactive)
442 if complete:
462 if complete:
443 if not hidden:
463 if not hidden:
444 # The maximum block count is only in effect during execution.
464 # The maximum block count is only in effect during execution.
445 # This ensures that _prompt_pos does not become invalid due to
465 # This ensures that _prompt_pos does not become invalid due to
446 # text truncation.
466 # text truncation.
447 self.setMaximumBlockCount(self.buffer_size)
467 self.setMaximumBlockCount(self.buffer_size)
448 self._execute(real_source, hidden)
468 self._execute(real_source, hidden)
449 elif hidden:
469 elif hidden:
450 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
470 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
451 else:
471 else:
452 self._show_continuation_prompt()
472 self._show_continuation_prompt()
453
473
454 return complete
474 return complete
455
475
456 def _get_input_buffer(self):
476 def _get_input_buffer(self):
457 """ The text that the user has entered entered at the current prompt.
477 """ The text that the user has entered entered at the current prompt.
458 """
478 """
459 # If we're executing, the input buffer may not even exist anymore due to
479 # If we're executing, the input buffer may not even exist anymore due to
460 # the limit imposed by 'buffer_size'. Therefore, we store it.
480 # the limit imposed by 'buffer_size'. Therefore, we store it.
461 if self._executing:
481 if self._executing:
462 return self._executing_input_buffer
482 return self._executing_input_buffer
463
483
464 cursor = self._get_end_cursor()
484 cursor = self._get_end_cursor()
465 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
485 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
466
467 # Use QTextDocumentFragment intermediate object because it strips
468 # out the Unicode line break characters that Qt insists on inserting.
469 input_buffer = str(cursor.selection().toPlainText())
486 input_buffer = str(cursor.selection().toPlainText())
470
487
471 # Strip out continuation prompts.
488 # Strip out continuation prompts.
472 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
489 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
473
490
474 def _set_input_buffer(self, string):
491 def _set_input_buffer(self, string):
475 """ Replaces the text in the input buffer with 'string'.
492 """ Replaces the text in the input buffer with 'string'.
476 """
493 """
477 # Add continuation prompts where necessary.
494 # Add continuation prompts where necessary.
478 lines = string.splitlines()
495 lines = string.splitlines()
479 for i in xrange(1, len(lines)):
496 for i in xrange(1, len(lines)):
480 lines[i] = self._continuation_prompt + lines[i]
497 lines[i] = self._continuation_prompt + lines[i]
481 string = '\n'.join(lines)
498 string = '\n'.join(lines)
482
499
483 # Replace buffer with new text.
500 # Replace buffer with new text.
484 cursor = self._get_end_cursor()
501 cursor = self._get_end_cursor()
485 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
502 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
486 cursor.insertText(string)
503 cursor.insertText(string)
487 self.moveCursor(QtGui.QTextCursor.End)
504 self.moveCursor(QtGui.QTextCursor.End)
488
505
489 input_buffer = property(_get_input_buffer, _set_input_buffer)
506 input_buffer = property(_get_input_buffer, _set_input_buffer)
490
507
491 def _get_input_buffer_cursor_line(self):
508 def _get_input_buffer_cursor_line(self):
492 """ The text in the line of the input buffer in which the user's cursor
509 """ The text in the line of the input buffer in which the user's cursor
493 rests. Returns a string if there is such a line; otherwise, None.
510 rests. Returns a string if there is such a line; otherwise, None.
494 """
511 """
495 if self._executing:
512 if self._executing:
496 return None
513 return None
497 cursor = self.textCursor()
514 cursor = self.textCursor()
498 if cursor.position() >= self._prompt_pos:
515 if cursor.position() >= self._prompt_pos:
499 text = str(cursor.block().text())
516 text = self._get_block_plain_text(cursor.block())
500 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
517 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
501 return text[len(self._prompt):]
518 return text[len(self._prompt):]
502 else:
519 else:
503 return text[len(self._continuation_prompt):]
520 return text[len(self._continuation_prompt):]
504 else:
521 else:
505 return None
522 return None
506
523
507 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
524 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
508
525
509 def _get_font(self):
526 def _get_font(self):
510 """ The base font being used by the ConsoleWidget.
527 """ The base font being used by the ConsoleWidget.
511 """
528 """
512 return self.document().defaultFont()
529 return self.document().defaultFont()
513
530
514 def _set_font(self, font):
531 def _set_font(self, font):
515 """ Sets the base font for the ConsoleWidget to the specified QFont.
532 """ Sets the base font for the ConsoleWidget to the specified QFont.
516 """
533 """
517 self._completion_widget.setFont(font)
534 self._completion_widget.setFont(font)
518 self.document().setDefaultFont(font)
535 self.document().setDefaultFont(font)
519
536
520 font = property(_get_font, _set_font)
537 font = property(_get_font, _set_font)
521
538
522 def reset_font(self):
539 def reset_font(self):
523 """ Sets the font to the default fixed-width font for this platform.
540 """ Sets the font to the default fixed-width font for this platform.
524 """
541 """
525 if sys.platform == 'win32':
542 if sys.platform == 'win32':
526 name = 'Courier'
543 name = 'Courier'
527 elif sys.platform == 'darwin':
544 elif sys.platform == 'darwin':
528 name = 'Monaco'
545 name = 'Monaco'
529 else:
546 else:
530 name = 'Monospace'
547 name = 'Monospace'
531 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
548 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
532 font.setStyleHint(QtGui.QFont.TypeWriter)
549 font.setStyleHint(QtGui.QFont.TypeWriter)
533 self._set_font(font)
550 self._set_font(font)
534
551
535 #---------------------------------------------------------------------------
552 #---------------------------------------------------------------------------
536 # 'ConsoleWidget' abstract interface
553 # 'ConsoleWidget' abstract interface
537 #---------------------------------------------------------------------------
554 #---------------------------------------------------------------------------
538
555
539 def _is_complete(self, source, interactive):
556 def _is_complete(self, source, interactive):
540 """ Returns whether 'source' can be executed. When triggered by an
557 """ Returns whether 'source' can be executed. When triggered by an
541 Enter/Return key press, 'interactive' is True; otherwise, it is
558 Enter/Return key press, 'interactive' is True; otherwise, it is
542 False.
559 False.
543 """
560 """
544 raise NotImplementedError
561 raise NotImplementedError
545
562
546 def _execute(self, source, hidden):
563 def _execute(self, source, hidden):
547 """ Execute 'source'. If 'hidden', do not show any output.
564 """ Execute 'source'. If 'hidden', do not show any output.
548 """
565 """
549 raise NotImplementedError
566 raise NotImplementedError
550
567
551 def _prompt_started_hook(self):
568 def _prompt_started_hook(self):
552 """ Called immediately after a new prompt is displayed.
569 """ Called immediately after a new prompt is displayed.
553 """
570 """
554 pass
571 pass
555
572
556 def _prompt_finished_hook(self):
573 def _prompt_finished_hook(self):
557 """ Called immediately after a prompt is finished, i.e. when some input
574 """ Called immediately after a prompt is finished, i.e. when some input
558 will be processed and a new prompt displayed.
575 will be processed and a new prompt displayed.
559 """
576 """
560 pass
577 pass
561
578
562 def _up_pressed(self):
579 def _up_pressed(self):
563 """ Called when the up key is pressed. Returns whether to continue
580 """ Called when the up key is pressed. Returns whether to continue
564 processing the event.
581 processing the event.
565 """
582 """
566 return True
583 return True
567
584
568 def _down_pressed(self):
585 def _down_pressed(self):
569 """ Called when the down key is pressed. Returns whether to continue
586 """ Called when the down key is pressed. Returns whether to continue
570 processing the event.
587 processing the event.
571 """
588 """
572 return True
589 return True
573
590
574 def _tab_pressed(self):
591 def _tab_pressed(self):
575 """ Called when the tab key is pressed. Returns whether to continue
592 """ Called when the tab key is pressed. Returns whether to continue
576 processing the event.
593 processing the event.
577 """
594 """
578 return False
595 return False
579
596
580 #--------------------------------------------------------------------------
597 #--------------------------------------------------------------------------
581 # 'ConsoleWidget' protected interface
598 # 'ConsoleWidget' protected interface
582 #--------------------------------------------------------------------------
599 #--------------------------------------------------------------------------
583
600
601 def _append_html_fetching_plain_text(self, html):
602 """ Appends 'html', then returns the plain text version of it.
603 """
604 anchor = self._get_end_cursor().position()
605 self.appendHtml(html)
606 cursor = self._get_end_cursor()
607 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
608 return str(cursor.selection().toPlainText())
609
610 def _append_plain_text_keeping_prompt(self, text):
611 """ Writes 'text' after the current prompt, then restores the old prompt
612 with its old input buffer.
613 """
614 input_buffer = self.input_buffer
615 self.appendPlainText('\n')
616 self._prompt_finished()
617
618 self.appendPlainText(text)
619 self._show_prompt()
620 self.input_buffer = input_buffer
621
584 def _control_down(self, modifiers):
622 def _control_down(self, modifiers):
585 """ Given a KeyboardModifiers flags object, return whether the Control
623 """ Given a KeyboardModifiers flags object, return whether the Control
586 key is down (on Mac OS, treat the Command key as a synonym for
624 key is down (on Mac OS, treat the Command key as a synonym for
587 Control).
625 Control).
588 """
626 """
589 down = bool(modifiers & QtCore.Qt.ControlModifier)
627 down = bool(modifiers & QtCore.Qt.ControlModifier)
590
628
591 # Note: on Mac OS, ControlModifier corresponds to the Command key while
629 # Note: on Mac OS, ControlModifier corresponds to the Command key while
592 # MetaModifier corresponds to the Control key.
630 # MetaModifier corresponds to the Control key.
593 if sys.platform == 'darwin':
631 if sys.platform == 'darwin':
594 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
632 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
595
633
596 return down
634 return down
597
635
598 def _complete_with_items(self, cursor, items):
636 def _complete_with_items(self, cursor, items):
599 """ Performs completion with 'items' at the specified cursor location.
637 """ Performs completion with 'items' at the specified cursor location.
600 """
638 """
601 if len(items) == 1:
639 if len(items) == 1:
602 cursor.setPosition(self.textCursor().position(),
640 cursor.setPosition(self.textCursor().position(),
603 QtGui.QTextCursor.KeepAnchor)
641 QtGui.QTextCursor.KeepAnchor)
604 cursor.insertText(items[0])
642 cursor.insertText(items[0])
605 elif len(items) > 1:
643 elif len(items) > 1:
606 if self.gui_completion:
644 if self.gui_completion:
607 self._completion_widget.show_items(cursor, items)
645 self._completion_widget.show_items(cursor, items)
608 else:
646 else:
609 text = '\n'.join(items) + '\n'
647 text = '\n'.join(items) + '\n'
610 self._write_text_keeping_prompt(text)
648 self._append_plain_text_keeping_prompt(text)
649
650 def _get_block_plain_text(self, block):
651 """ Given a QTextBlock, return its unformatted text.
652 """
653 cursor = QtGui.QTextCursor(block)
654 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
655 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
656 QtGui.QTextCursor.KeepAnchor)
657 return str(cursor.selection().toPlainText())
611
658
612 def _get_end_cursor(self):
659 def _get_end_cursor(self):
613 """ Convenience method that returns a cursor for the last character.
660 """ Convenience method that returns a cursor for the last character.
614 """
661 """
615 cursor = self.textCursor()
662 cursor = self.textCursor()
616 cursor.movePosition(QtGui.QTextCursor.End)
663 cursor.movePosition(QtGui.QTextCursor.End)
617 return cursor
664 return cursor
618
665
619 def _get_prompt_cursor(self):
666 def _get_prompt_cursor(self):
620 """ Convenience method that returns a cursor for the prompt position.
667 """ Convenience method that returns a cursor for the prompt position.
621 """
668 """
622 cursor = self.textCursor()
669 cursor = self.textCursor()
623 cursor.setPosition(self._prompt_pos)
670 cursor.setPosition(self._prompt_pos)
624 return cursor
671 return cursor
625
672
626 def _get_selection_cursor(self, start, end):
673 def _get_selection_cursor(self, start, end):
627 """ Convenience method that returns a cursor with text selected between
674 """ Convenience method that returns a cursor with text selected between
628 the positions 'start' and 'end'.
675 the positions 'start' and 'end'.
629 """
676 """
630 cursor = self.textCursor()
677 cursor = self.textCursor()
631 cursor.setPosition(start)
678 cursor.setPosition(start)
632 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
679 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
633 return cursor
680 return cursor
634
681
635 def _get_word_start_cursor(self, position):
682 def _get_word_start_cursor(self, position):
636 """ Find the start of the word to the left the given position. If a
683 """ Find the start of the word to the left the given position. If a
637 sequence of non-word characters precedes the first word, skip over
684 sequence of non-word characters precedes the first word, skip over
638 them. (This emulates the behavior of bash, emacs, etc.)
685 them. (This emulates the behavior of bash, emacs, etc.)
639 """
686 """
640 document = self.document()
687 document = self.document()
641 position -= 1
688 position -= 1
642 while self._in_buffer(position) and \
689 while self._in_buffer(position) and \
643 not document.characterAt(position).isLetterOrNumber():
690 not document.characterAt(position).isLetterOrNumber():
644 position -= 1
691 position -= 1
645 while self._in_buffer(position) and \
692 while self._in_buffer(position) and \
646 document.characterAt(position).isLetterOrNumber():
693 document.characterAt(position).isLetterOrNumber():
647 position -= 1
694 position -= 1
648 cursor = self.textCursor()
695 cursor = self.textCursor()
649 cursor.setPosition(position + 1)
696 cursor.setPosition(position + 1)
650 return cursor
697 return cursor
651
698
652 def _get_word_end_cursor(self, position):
699 def _get_word_end_cursor(self, position):
653 """ Find the end of the word to the right the given position. If a
700 """ Find the end of the word to the right the given position. If a
654 sequence of non-word characters precedes the first word, skip over
701 sequence of non-word characters precedes the first word, skip over
655 them. (This emulates the behavior of bash, emacs, etc.)
702 them. (This emulates the behavior of bash, emacs, etc.)
656 """
703 """
657 document = self.document()
704 document = self.document()
658 end = self._get_end_cursor().position()
705 end = self._get_end_cursor().position()
659 while position < end and \
706 while position < end and \
660 not document.characterAt(position).isLetterOrNumber():
707 not document.characterAt(position).isLetterOrNumber():
661 position += 1
708 position += 1
662 while position < end and \
709 while position < end and \
663 document.characterAt(position).isLetterOrNumber():
710 document.characterAt(position).isLetterOrNumber():
664 position += 1
711 position += 1
665 cursor = self.textCursor()
712 cursor = self.textCursor()
666 cursor.setPosition(position)
713 cursor.setPosition(position)
667 return cursor
714 return cursor
668
715
669 def _prompt_started(self):
716 def _prompt_started(self):
670 """ Called immediately after a new prompt is displayed.
717 """ Called immediately after a new prompt is displayed.
671 """
718 """
672 # Temporarily disable the maximum block count to permit undo/redo and
719 # Temporarily disable the maximum block count to permit undo/redo and
673 # to ensure that the prompt position does not change due to truncation.
720 # to ensure that the prompt position does not change due to truncation.
674 self.setMaximumBlockCount(0)
721 self.setMaximumBlockCount(0)
675 self.setUndoRedoEnabled(True)
722 self.setUndoRedoEnabled(True)
676
723
677 self.setReadOnly(False)
724 self.setReadOnly(False)
678 self.moveCursor(QtGui.QTextCursor.End)
725 self.moveCursor(QtGui.QTextCursor.End)
679 self.centerCursor()
726 self.centerCursor()
680
727
681 self._executing = False
728 self._executing = False
682 self._prompt_started_hook()
729 self._prompt_started_hook()
683
730
684 def _prompt_finished(self):
731 def _prompt_finished(self):
685 """ Called immediately after a prompt is finished, i.e. when some input
732 """ Called immediately after a prompt is finished, i.e. when some input
686 will be processed and a new prompt displayed.
733 will be processed and a new prompt displayed.
687 """
734 """
688 self.setUndoRedoEnabled(False)
735 self.setUndoRedoEnabled(False)
689 self.setReadOnly(True)
736 self.setReadOnly(True)
690 self._prompt_finished_hook()
737 self._prompt_finished_hook()
691
738
692 def _readline(self, prompt='', callback=None):
739 def _readline(self, prompt='', callback=None):
693 """ Reads one line of input from the user.
740 """ Reads one line of input from the user.
694
741
695 Parameters
742 Parameters
696 ----------
743 ----------
697 prompt : str, optional
744 prompt : str, optional
698 The prompt to print before reading the line.
745 The prompt to print before reading the line.
699
746
700 callback : callable, optional
747 callback : callable, optional
701 A callback to execute with the read line. If not specified, input is
748 A callback to execute with the read line. If not specified, input is
702 read *synchronously* and this method does not return until it has
749 read *synchronously* and this method does not return until it has
703 been read.
750 been read.
704
751
705 Returns
752 Returns
706 -------
753 -------
707 If a callback is specified, returns nothing. Otherwise, returns the
754 If a callback is specified, returns nothing. Otherwise, returns the
708 input string with the trailing newline stripped.
755 input string with the trailing newline stripped.
709 """
756 """
710 if self._reading:
757 if self._reading:
711 raise RuntimeError('Cannot read a line. Widget is already reading.')
758 raise RuntimeError('Cannot read a line. Widget is already reading.')
712
759
713 if not callback and not self.isVisible():
760 if not callback and not self.isVisible():
714 # If the user cannot see the widget, this function cannot return.
761 # If the user cannot see the widget, this function cannot return.
715 raise RuntimeError('Cannot synchronously read a line if the widget'
762 raise RuntimeError('Cannot synchronously read a line if the widget'
716 'is not visible!')
763 'is not visible!')
717
764
718 self._reading = True
765 self._reading = True
719 self._show_prompt(prompt, newline=False)
766 self._show_prompt(prompt, newline=False)
720
767
721 if callback is None:
768 if callback is None:
722 self._reading_callback = None
769 self._reading_callback = None
723 while self._reading:
770 while self._reading:
724 QtCore.QCoreApplication.processEvents()
771 QtCore.QCoreApplication.processEvents()
725 return self.input_buffer.rstrip('\n')
772 return self.input_buffer.rstrip('\n')
726
773
727 else:
774 else:
728 self._reading_callback = lambda: \
775 self._reading_callback = lambda: \
729 callback(self.input_buffer.rstrip('\n'))
776 callback(self.input_buffer.rstrip('\n'))
730
777
778 def _reset(self):
779 """ Clears the console and resets internal state variables.
780 """
781 QtGui.QPlainTextEdit.clear(self)
782 self._executing = self._reading = False
783
784 def _set_continuation_prompt(self, prompt, html=False):
785 """ Sets the continuation prompt.
786
787 Parameters
788 ----------
789 prompt : str
790 The prompt to show when more input is needed.
791
792 html : bool, optional (default False)
793 If set, the prompt will be inserted as formatted HTML. Otherwise,
794 the prompt will be treated as plain text, though ANSI color codes
795 will be handled.
796 """
797 if html:
798 self._continuation_prompt_html = prompt
799 else:
800 self._continuation_prompt = prompt
801 self._continuation_prompt_html = None
802
731 def _set_position(self, position):
803 def _set_position(self, position):
732 """ Convenience method to set the position of the cursor.
804 """ Convenience method to set the position of the cursor.
733 """
805 """
734 cursor = self.textCursor()
806 cursor = self.textCursor()
735 cursor.setPosition(position)
807 cursor.setPosition(position)
736 self.setTextCursor(cursor)
808 self.setTextCursor(cursor)
737
809
738 def _set_selection(self, start, end):
810 def _set_selection(self, start, end):
739 """ Convenience method to set the current selected text.
811 """ Convenience method to set the current selected text.
740 """
812 """
741 self.setTextCursor(self._get_selection_cursor(start, end))
813 self.setTextCursor(self._get_selection_cursor(start, end))
742
814
743 def _show_prompt(self, prompt=None, newline=True):
815 def _show_prompt(self, prompt=None, html=False, newline=True):
744 """ Writes a new prompt at the end of the buffer.
816 """ Writes a new prompt at the end of the buffer.
745
817
746 Parameters
818 Parameters
747 ----------
819 ----------
748 prompt : str, optional
820 prompt : str, optional
749 The prompt to show. If not specified, the previous prompt is used.
821 The prompt to show. If not specified, the previous prompt is used.
750
822
823 html : bool, optional (default False)
824 Only relevant when a prompt is specified. If set, the prompt will
825 be inserted as formatted HTML. Otherwise, the prompt will be treated
826 as plain text, though ANSI color codes will be handled.
827
751 newline : bool, optional (default True)
828 newline : bool, optional (default True)
752 If set, a new line will be written before showing the prompt if
829 If set, a new line will be written before showing the prompt if
753 there is not already a newline at the end of the buffer.
830 there is not already a newline at the end of the buffer.
754 """
831 """
832 # Insert a preliminary newline, if necessary.
755 if newline:
833 if newline:
756 cursor = self._get_end_cursor()
834 cursor = self._get_end_cursor()
757 if cursor.position() > 0:
835 if cursor.position() > 0:
758 cursor.movePosition(QtGui.QTextCursor.Left,
836 cursor.movePosition(QtGui.QTextCursor.Left,
759 QtGui.QTextCursor.KeepAnchor)
837 QtGui.QTextCursor.KeepAnchor)
760 if str(cursor.selection().toPlainText()) != '\n':
838 if str(cursor.selection().toPlainText()) != '\n':
761 self.appendPlainText('\n')
839 self.appendPlainText('\n')
762
840
763 if prompt is not None:
841 # Write the prompt.
764 self._prompt = prompt
842 if prompt is None:
765 self.appendPlainText(self._prompt)
843 if self._prompt_html is None:
844 self.appendPlainText(self._prompt)
845 else:
846 self.appendHtml(self._prompt_html)
847 else:
848 if html:
849 self._prompt = self._append_html_fetching_plain_text(prompt)
850 self._prompt_html = prompt
851 else:
852 self.appendPlainText(prompt)
853 self._prompt = prompt
854 self._prompt_html = None
766
855
767 self._prompt_pos = self._get_end_cursor().position()
856 self._prompt_pos = self._get_end_cursor().position()
768 self._prompt_started()
857 self._prompt_started()
769
858
770 def _show_continuation_prompt(self):
859 def _show_continuation_prompt(self):
771 """ Writes a new continuation prompt at the end of the buffer.
860 """ Writes a new continuation prompt at the end of the buffer.
772 """
861 """
773 self.appendPlainText(self._continuation_prompt)
862 if self._continuation_prompt_html is None:
774 self._prompt_started()
863 self.appendPlainText(self._continuation_prompt)
775
864 else:
776 def _write_text_keeping_prompt(self, text):
865 self._continuation_prompt = self._append_html_fetching_plain_text(
777 """ Writes 'text' after the current prompt, then restores the old prompt
866 self._continuation_prompt_html)
778 with its old input buffer.
779 """
780 input_buffer = self.input_buffer
781 self.appendPlainText('\n')
782 self._prompt_finished()
783
867
784 self.appendPlainText(text)
868 self._prompt_started()
785 self._show_prompt()
786 self.input_buffer = input_buffer
787
869
788 def _in_buffer(self, position):
870 def _in_buffer(self, position):
789 """ Returns whether the given position is inside the editing region.
871 """ Returns whether the given position is inside the editing region.
790 """
872 """
791 return position >= self._prompt_pos
873 return position >= self._prompt_pos
792
874
793 def _keep_cursor_in_buffer(self):
875 def _keep_cursor_in_buffer(self):
794 """ Ensures that the cursor is inside the editing region. Returns
876 """ Ensures that the cursor is inside the editing region. Returns
795 whether the cursor was moved.
877 whether the cursor was moved.
796 """
878 """
797 cursor = self.textCursor()
879 cursor = self.textCursor()
798 if cursor.position() < self._prompt_pos:
880 if cursor.position() < self._prompt_pos:
799 cursor.movePosition(QtGui.QTextCursor.End)
881 cursor.movePosition(QtGui.QTextCursor.End)
800 self.setTextCursor(cursor)
882 self.setTextCursor(cursor)
801 return True
883 return True
802 else:
884 else:
803 return False
885 return False
804
886
805
887
806 class HistoryConsoleWidget(ConsoleWidget):
888 class HistoryConsoleWidget(ConsoleWidget):
807 """ A ConsoleWidget that keeps a history of the commands that have been
889 """ A ConsoleWidget that keeps a history of the commands that have been
808 executed.
890 executed.
809 """
891 """
810
892
811 #---------------------------------------------------------------------------
893 #---------------------------------------------------------------------------
812 # 'QObject' interface
894 # 'QObject' interface
813 #---------------------------------------------------------------------------
895 #---------------------------------------------------------------------------
814
896
815 def __init__(self, parent=None):
897 def __init__(self, parent=None):
816 super(HistoryConsoleWidget, self).__init__(parent)
898 super(HistoryConsoleWidget, self).__init__(parent)
817
899
818 self._history = []
900 self._history = []
819 self._history_index = 0
901 self._history_index = 0
820
902
821 #---------------------------------------------------------------------------
903 #---------------------------------------------------------------------------
822 # 'ConsoleWidget' public interface
904 # 'ConsoleWidget' public interface
823 #---------------------------------------------------------------------------
905 #---------------------------------------------------------------------------
824
906
825 def execute(self, source=None, hidden=False, interactive=False):
907 def execute(self, source=None, hidden=False, interactive=False):
826 """ Reimplemented to the store history.
908 """ Reimplemented to the store history.
827 """
909 """
828 if not hidden:
910 if not hidden:
829 history = self.input_buffer if source is None else source
911 history = self.input_buffer if source is None else source
830
912
831 executed = super(HistoryConsoleWidget, self).execute(
913 executed = super(HistoryConsoleWidget, self).execute(
832 source, hidden, interactive)
914 source, hidden, interactive)
833
915
834 if executed and not hidden:
916 if executed and not hidden:
835 self._history.append(history.rstrip())
917 self._history.append(history.rstrip())
836 self._history_index = len(self._history)
918 self._history_index = len(self._history)
837
919
838 return executed
920 return executed
839
921
840 #---------------------------------------------------------------------------
922 #---------------------------------------------------------------------------
841 # 'ConsoleWidget' abstract interface
923 # 'ConsoleWidget' abstract interface
842 #---------------------------------------------------------------------------
924 #---------------------------------------------------------------------------
843
925
844 def _up_pressed(self):
926 def _up_pressed(self):
845 """ Called when the up key is pressed. Returns whether to continue
927 """ Called when the up key is pressed. Returns whether to continue
846 processing the event.
928 processing the event.
847 """
929 """
848 prompt_cursor = self._get_prompt_cursor()
930 prompt_cursor = self._get_prompt_cursor()
849 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
931 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
850 self.history_previous()
932 self.history_previous()
851
933
852 # Go to the first line of prompt for seemless history scrolling.
934 # Go to the first line of prompt for seemless history scrolling.
853 cursor = self._get_prompt_cursor()
935 cursor = self._get_prompt_cursor()
854 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
936 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
855 self.setTextCursor(cursor)
937 self.setTextCursor(cursor)
856
938
857 return False
939 return False
858 return True
940 return True
859
941
860 def _down_pressed(self):
942 def _down_pressed(self):
861 """ Called when the down key is pressed. Returns whether to continue
943 """ Called when the down key is pressed. Returns whether to continue
862 processing the event.
944 processing the event.
863 """
945 """
864 end_cursor = self._get_end_cursor()
946 end_cursor = self._get_end_cursor()
865 if self.textCursor().blockNumber() == end_cursor.blockNumber():
947 if self.textCursor().blockNumber() == end_cursor.blockNumber():
866 self.history_next()
948 self.history_next()
867 return False
949 return False
868 return True
950 return True
869
951
870 #---------------------------------------------------------------------------
952 #---------------------------------------------------------------------------
871 # 'HistoryConsoleWidget' interface
953 # 'HistoryConsoleWidget' interface
872 #---------------------------------------------------------------------------
954 #---------------------------------------------------------------------------
873
955
874 def history_previous(self):
956 def history_previous(self):
875 """ If possible, set the input buffer to the previous item in the
957 """ If possible, set the input buffer to the previous item in the
876 history.
958 history.
877 """
959 """
878 if self._history_index > 0:
960 if self._history_index > 0:
879 self._history_index -= 1
961 self._history_index -= 1
880 self.input_buffer = self._history[self._history_index]
962 self.input_buffer = self._history[self._history_index]
881
963
882 def history_next(self):
964 def history_next(self):
883 """ Set the input buffer to the next item in the history, or a blank
965 """ Set the input buffer to the next item in the history, or a blank
884 line if there is no subsequent item.
966 line if there is no subsequent item.
885 """
967 """
886 if self._history_index < len(self._history):
968 if self._history_index < len(self._history):
887 self._history_index += 1
969 self._history_index += 1
888 if self._history_index < len(self._history):
970 if self._history_index < len(self._history):
889 self.input_buffer = self._history[self._history_index]
971 self.input_buffer = self._history[self._history_index]
890 else:
972 else:
891 self.input_buffer = ''
973 self.input_buffer = ''
@@ -1,371 +1,377 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 call_tip_widget import CallTipWidget
12 from call_tip_widget import CallTipWidget
13 from completion_lexer import CompletionLexer
13 from completion_lexer import CompletionLexer
14 from console_widget import HistoryConsoleWidget
14 from console_widget import HistoryConsoleWidget
15 from pygments_highlighter import PygmentsHighlighter
15 from pygments_highlighter import PygmentsHighlighter
16
16
17
17
18 class FrontendHighlighter(PygmentsHighlighter):
18 class FrontendHighlighter(PygmentsHighlighter):
19 """ A Python PygmentsHighlighter that can be turned on and off and which
19 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 knows about continuation prompts.
20 prompts.
21 """
21 """
22
22
23 def __init__(self, frontend):
23 def __init__(self, frontend):
24 PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
24 super(FrontendHighlighter, self).__init__(frontend.document())
25 self._current_offset = 0
25 self._current_offset = 0
26 self._frontend = frontend
26 self._frontend = frontend
27 self.highlighting_on = False
27 self.highlighting_on = False
28
28
29 def highlightBlock(self, qstring):
29 def highlightBlock(self, qstring):
30 """ Highlight a block of text. Reimplemented to highlight selectively.
30 """ Highlight a block of text. Reimplemented to highlight selectively.
31 """
31 """
32 if self.highlighting_on:
32 if not self.highlighting_on:
33 for prompt in (self._frontend._continuation_prompt,
33 return
34 self._frontend._prompt):
34
35 if qstring.startsWith(prompt):
35 # The input to this function is unicode string that may contain
36 qstring.remove(0, len(prompt))
36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 self._current_offset = len(prompt)
37 # the string as plain text so we can compare it.
38 break
38 current_block = self.currentBlock()
39 PygmentsHighlighter.highlightBlock(self, qstring)
39 string = self._frontend._get_block_plain_text(current_block)
40
41 # Decide whether to check for the regular or continuation prompt.
42 if current_block.contains(self._frontend._prompt_pos):
43 prompt = self._frontend._prompt
44 else:
45 prompt = self._frontend._continuation_prompt
46
47 # Don't highlight the part of the string that contains the prompt.
48 if string.startswith(prompt):
49 self._current_offset = len(prompt)
50 qstring.remove(0, len(prompt))
51 else:
52 self._current_offset = 0
53
54 PygmentsHighlighter.highlightBlock(self, qstring)
40
55
41 def setFormat(self, start, count, format):
56 def setFormat(self, start, count, format):
42 """ Reimplemented to avoid highlighting continuation prompts.
57 """ Reimplemented to highlight selectively.
43 """
58 """
44 start += self._current_offset
59 start += self._current_offset
45 PygmentsHighlighter.setFormat(self, start, count, format)
60 PygmentsHighlighter.setFormat(self, start, count, format)
46
61
47
62
48 class FrontendWidget(HistoryConsoleWidget):
63 class FrontendWidget(HistoryConsoleWidget):
49 """ A Qt frontend for a generic Python kernel.
64 """ A Qt frontend for a generic Python kernel.
50 """
65 """
51
66
52 # Emitted when an 'execute_reply' is received from the kernel.
67 # Emitted when an 'execute_reply' is received from the kernel.
53 executed = QtCore.pyqtSignal(object)
68 executed = QtCore.pyqtSignal(object)
54
69
55 #---------------------------------------------------------------------------
70 #---------------------------------------------------------------------------
56 # 'QObject' interface
71 # 'QObject' interface
57 #---------------------------------------------------------------------------
72 #---------------------------------------------------------------------------
58
73
59 def __init__(self, parent=None):
74 def __init__(self, parent=None):
60 super(FrontendWidget, self).__init__(parent)
75 super(FrontendWidget, self).__init__(parent)
61
76
62 # ConsoleWidget protected variables.
63 self._continuation_prompt = '... '
64
65 # FrontendWidget protected variables.
77 # FrontendWidget protected variables.
66 self._call_tip_widget = CallTipWidget(self)
78 self._call_tip_widget = CallTipWidget(self)
67 self._completion_lexer = CompletionLexer(PythonLexer())
79 self._completion_lexer = CompletionLexer(PythonLexer())
68 self._hidden = True
80 self._hidden = True
69 self._highlighter = FrontendHighlighter(self)
81 self._highlighter = FrontendHighlighter(self)
70 self._input_splitter = InputSplitter(input_mode='replace')
82 self._input_splitter = InputSplitter(input_mode='replace')
71 self._kernel_manager = None
83 self._kernel_manager = None
72
84
85 # Configure the ConsoleWidget.
86 self._set_continuation_prompt('... ')
87
73 self.document().contentsChange.connect(self._document_contents_change)
88 self.document().contentsChange.connect(self._document_contents_change)
74
89
75 #---------------------------------------------------------------------------
90 #---------------------------------------------------------------------------
76 # 'QWidget' interface
91 # 'QWidget' interface
77 #---------------------------------------------------------------------------
92 #---------------------------------------------------------------------------
78
93
79 def focusOutEvent(self, event):
94 def focusOutEvent(self, event):
80 """ Reimplemented to hide calltips.
95 """ Reimplemented to hide calltips.
81 """
96 """
82 self._call_tip_widget.hide()
97 self._call_tip_widget.hide()
83 super(FrontendWidget, self).focusOutEvent(event)
98 super(FrontendWidget, self).focusOutEvent(event)
84
99
85 def keyPressEvent(self, event):
100 def keyPressEvent(self, event):
86 """ Reimplemented to allow calltips to process events and to send
101 """ Reimplemented to allow calltips to process events and to send
87 signals to the kernel.
102 signals to the kernel.
88 """
103 """
89 if self._executing and event.key() == QtCore.Qt.Key_C and \
104 if self._executing and event.key() == QtCore.Qt.Key_C and \
90 self._control_down(event.modifiers()):
105 self._control_down(event.modifiers()):
91 self._interrupt_kernel()
106 self._interrupt_kernel()
92 else:
107 else:
93 if self._call_tip_widget.isVisible():
108 if self._call_tip_widget.isVisible():
94 self._call_tip_widget.keyPressEvent(event)
109 self._call_tip_widget.keyPressEvent(event)
95 super(FrontendWidget, self).keyPressEvent(event)
110 super(FrontendWidget, self).keyPressEvent(event)
96
111
97 #---------------------------------------------------------------------------
112 #---------------------------------------------------------------------------
98 # 'ConsoleWidget' abstract interface
113 # 'ConsoleWidget' abstract interface
99 #---------------------------------------------------------------------------
114 #---------------------------------------------------------------------------
100
115
101 def _is_complete(self, source, interactive):
116 def _is_complete(self, source, interactive):
102 """ Returns whether 'source' can be completely processed and a new
117 """ Returns whether 'source' can be completely processed and a new
103 prompt created. When triggered by an Enter/Return key press,
118 prompt created. When triggered by an Enter/Return key press,
104 'interactive' is True; otherwise, it is False.
119 'interactive' is True; otherwise, it is False.
105 """
120 """
106 complete = self._input_splitter.push(source)
121 complete = self._input_splitter.push(source)
107 if interactive:
122 if interactive:
108 complete = not self._input_splitter.push_accepts_more()
123 complete = not self._input_splitter.push_accepts_more()
109 return complete
124 return complete
110
125
111 def _execute(self, source, hidden):
126 def _execute(self, source, hidden):
112 """ Execute 'source'. If 'hidden', do not show any output.
127 """ Execute 'source'. If 'hidden', do not show any output.
113 """
128 """
114 self.kernel_manager.xreq_channel.execute(source)
129 self.kernel_manager.xreq_channel.execute(source)
115 self._hidden = hidden
130 self._hidden = hidden
116
131
117 def _prompt_started_hook(self):
132 def _prompt_started_hook(self):
118 """ Called immediately after a new prompt is displayed.
133 """ Called immediately after a new prompt is displayed.
119 """
134 """
120 if not self._reading:
135 if not self._reading:
121 self._highlighter.highlighting_on = True
136 self._highlighter.highlighting_on = True
122
137
123 # Auto-indent if this is a continuation prompt.
138 # Auto-indent if this is a continuation prompt.
124 if self._get_prompt_cursor().blockNumber() != \
139 if self._get_prompt_cursor().blockNumber() != \
125 self._get_end_cursor().blockNumber():
140 self._get_end_cursor().blockNumber():
126 self.appendPlainText(' ' * self._input_splitter.indent_spaces)
141 self.appendPlainText(' ' * self._input_splitter.indent_spaces)
127
142
128 def _prompt_finished_hook(self):
143 def _prompt_finished_hook(self):
129 """ 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
130 will be processed and a new prompt displayed.
145 will be processed and a new prompt displayed.
131 """
146 """
132 if not self._reading:
147 if not self._reading:
133 self._highlighter.highlighting_on = False
148 self._highlighter.highlighting_on = False
134
149
135 def _tab_pressed(self):
150 def _tab_pressed(self):
136 """ Called when the tab key is pressed. Returns whether to continue
151 """ Called when the tab key is pressed. Returns whether to continue
137 processing the event.
152 processing the event.
138 """
153 """
139 self._keep_cursor_in_buffer()
154 self._keep_cursor_in_buffer()
140 cursor = self.textCursor()
155 cursor = self.textCursor()
141 if not self._complete():
156 if not self._complete():
142 cursor.insertText(' ')
157 cursor.insertText(' ')
143 return False
158 return False
144
159
145 #---------------------------------------------------------------------------
160 #---------------------------------------------------------------------------
146 # 'ConsoleWidget' protected interface
147 #---------------------------------------------------------------------------
148
149 def _show_prompt(self, prompt=None, newline=True):
150 """ Reimplemented to set a default prompt.
151 """
152 if prompt is None:
153 prompt = '>>> '
154 super(FrontendWidget, self)._show_prompt(prompt, newline)
155
156 #---------------------------------------------------------------------------
157 # 'FrontendWidget' interface
161 # 'FrontendWidget' interface
158 #---------------------------------------------------------------------------
162 #---------------------------------------------------------------------------
159
163
160 def execute_file(self, path, hidden=False):
164 def execute_file(self, path, hidden=False):
161 """ Attempts to execute file with 'path'. If 'hidden', no output is
165 """ Attempts to execute file with 'path'. If 'hidden', no output is
162 shown.
166 shown.
163 """
167 """
164 self.execute('execfile("%s")' % path, hidden=hidden)
168 self.execute('execfile("%s")' % path, hidden=hidden)
165
169
166 def _get_kernel_manager(self):
170 def _get_kernel_manager(self):
167 """ Returns the current kernel manager.
171 """ Returns the current kernel manager.
168 """
172 """
169 return self._kernel_manager
173 return self._kernel_manager
170
174
171 def _set_kernel_manager(self, kernel_manager):
175 def _set_kernel_manager(self, kernel_manager):
172 """ Disconnect from the current kernel manager (if any) and set a new
176 """ Disconnect from the current kernel manager (if any) and set a new
173 kernel manager.
177 kernel manager.
174 """
178 """
175 # Disconnect the old kernel manager, if necessary.
179 # Disconnect the old kernel manager, if necessary.
176 if self._kernel_manager is not None:
180 if self._kernel_manager is not None:
177 self._kernel_manager.started_channels.disconnect(
181 self._kernel_manager.started_channels.disconnect(
178 self._started_channels)
182 self._started_channels)
179 self._kernel_manager.stopped_channels.disconnect(
183 self._kernel_manager.stopped_channels.disconnect(
180 self._stopped_channels)
184 self._stopped_channels)
181
185
182 # Disconnect the old kernel manager's channels.
186 # Disconnect the old kernel manager's channels.
183 sub = self._kernel_manager.sub_channel
187 sub = self._kernel_manager.sub_channel
184 xreq = self._kernel_manager.xreq_channel
188 xreq = self._kernel_manager.xreq_channel
185 rep = self._kernel_manager.rep_channel
189 rep = self._kernel_manager.rep_channel
186 sub.message_received.disconnect(self._handle_sub)
190 sub.message_received.disconnect(self._handle_sub)
187 xreq.execute_reply.disconnect(self._handle_execute_reply)
191 xreq.execute_reply.disconnect(self._handle_execute_reply)
188 xreq.complete_reply.disconnect(self._handle_complete_reply)
192 xreq.complete_reply.disconnect(self._handle_complete_reply)
189 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
193 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
190 rep.readline_requested.disconnect(self._handle_req)
194 rep.readline_requested.disconnect(self._handle_req)
191
195
192 # Handle the case where the old kernel manager is still listening.
196 # Handle the case where the old kernel manager is still listening.
193 if self._kernel_manager.channels_running:
197 if self._kernel_manager.channels_running:
194 self._stopped_channels()
198 self._stopped_channels()
195
199
196 # Set the new kernel manager.
200 # Set the new kernel manager.
197 self._kernel_manager = kernel_manager
201 self._kernel_manager = kernel_manager
198 if kernel_manager is None:
202 if kernel_manager is None:
199 return
203 return
200
204
201 # Connect the new kernel manager.
205 # Connect the new kernel manager.
202 kernel_manager.started_channels.connect(self._started_channels)
206 kernel_manager.started_channels.connect(self._started_channels)
203 kernel_manager.stopped_channels.connect(self._stopped_channels)
207 kernel_manager.stopped_channels.connect(self._stopped_channels)
204
208
205 # Connect the new kernel manager's channels.
209 # Connect the new kernel manager's channels.
206 sub = kernel_manager.sub_channel
210 sub = kernel_manager.sub_channel
207 xreq = kernel_manager.xreq_channel
211 xreq = kernel_manager.xreq_channel
208 rep = kernel_manager.rep_channel
212 rep = kernel_manager.rep_channel
209 sub.message_received.connect(self._handle_sub)
213 sub.message_received.connect(self._handle_sub)
210 xreq.execute_reply.connect(self._handle_execute_reply)
214 xreq.execute_reply.connect(self._handle_execute_reply)
211 xreq.complete_reply.connect(self._handle_complete_reply)
215 xreq.complete_reply.connect(self._handle_complete_reply)
212 xreq.object_info_reply.connect(self._handle_object_info_reply)
216 xreq.object_info_reply.connect(self._handle_object_info_reply)
213 rep.readline_requested.connect(self._handle_req)
217 rep.readline_requested.connect(self._handle_req)
214
218
215 # Handle the case where the kernel manager started channels before
219 # Handle the case where the kernel manager started channels before
216 # we connected.
220 # we connected.
217 if kernel_manager.channels_running:
221 if kernel_manager.channels_running:
218 self._started_channels()
222 self._started_channels()
219
223
220 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
224 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
221
225
222 #---------------------------------------------------------------------------
226 #---------------------------------------------------------------------------
223 # 'FrontendWidget' protected interface
227 # 'FrontendWidget' protected interface
224 #---------------------------------------------------------------------------
228 #---------------------------------------------------------------------------
225
229
226 def _call_tip(self):
230 def _call_tip(self):
227 """ Shows a call tip, if appropriate, at the current cursor location.
231 """ Shows a call tip, if appropriate, at the current cursor location.
228 """
232 """
229 # Decide if it makes sense to show a call tip
233 # Decide if it makes sense to show a call tip
230 cursor = self.textCursor()
234 cursor = self.textCursor()
231 cursor.movePosition(QtGui.QTextCursor.Left)
235 cursor.movePosition(QtGui.QTextCursor.Left)
232 document = self.document()
236 document = self.document()
233 if document.characterAt(cursor.position()).toAscii() != '(':
237 if document.characterAt(cursor.position()).toAscii() != '(':
234 return False
238 return False
235 context = self._get_context(cursor)
239 context = self._get_context(cursor)
236 if not context:
240 if not context:
237 return False
241 return False
238
242
239 # Send the metadata request to the kernel
243 # Send the metadata request to the kernel
240 name = '.'.join(context)
244 name = '.'.join(context)
241 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
245 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
242 self._calltip_pos = self.textCursor().position()
246 self._calltip_pos = self.textCursor().position()
243 return True
247 return True
244
248
245 def _complete(self):
249 def _complete(self):
246 """ Performs completion at the current cursor location.
250 """ Performs completion at the current cursor location.
247 """
251 """
248 # Decide if it makes sense to do completion
252 # Decide if it makes sense to do completion
249 context = self._get_context()
253 context = self._get_context()
250 if not context:
254 if not context:
251 return False
255 return False
252
256
253 # Send the completion request to the kernel
257 # Send the completion request to the kernel
254 text = '.'.join(context)
258 text = '.'.join(context)
255 self._complete_id = self.kernel_manager.xreq_channel.complete(
259 self._complete_id = self.kernel_manager.xreq_channel.complete(
256 text, self.input_buffer_cursor_line, self.input_buffer)
260 text, self.input_buffer_cursor_line, self.input_buffer)
257 self._complete_pos = self.textCursor().position()
261 self._complete_pos = self.textCursor().position()
258 return True
262 return True
259
263
260 def _get_banner(self):
264 def _get_banner(self):
261 """ Gets a banner to display at the beginning of a session.
265 """ Gets a banner to display at the beginning of a session.
262 """
266 """
263 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
267 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
264 '"license" for more information.'
268 '"license" for more information.'
265 return banner % (sys.version, sys.platform)
269 return banner % (sys.version, sys.platform)
266
270
267 def _get_context(self, cursor=None):
271 def _get_context(self, cursor=None):
268 """ Gets the context at the current cursor location.
272 """ Gets the context at the current cursor location.
269 """
273 """
270 if cursor is None:
274 if cursor is None:
271 cursor = self.textCursor()
275 cursor = self.textCursor()
272 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
276 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
273 QtGui.QTextCursor.KeepAnchor)
277 QtGui.QTextCursor.KeepAnchor)
274 text = unicode(cursor.selectedText())
278 text = unicode(cursor.selectedText())
275 return self._completion_lexer.get_context(text)
279 return self._completion_lexer.get_context(text)
276
280
277 def _interrupt_kernel(self):
281 def _interrupt_kernel(self):
278 """ Attempts to the interrupt the kernel.
282 """ Attempts to the interrupt the kernel.
279 """
283 """
280 if self.kernel_manager.has_kernel:
284 if self.kernel_manager.has_kernel:
281 self.kernel_manager.signal_kernel(signal.SIGINT)
285 self.kernel_manager.signal_kernel(signal.SIGINT)
282 else:
286 else:
283 self.appendPlainText('Kernel process is either remote or '
287 self.appendPlainText('Kernel process is either remote or '
284 'unspecified. Cannot interrupt.\n')
288 'unspecified. Cannot interrupt.\n')
285
289
290 def _show_interpreter_prompt(self):
291 """ Shows a prompt for the interpreter.
292 """
293 self._show_prompt('>>> ')
294
286 #------ Signal handlers ----------------------------------------------------
295 #------ Signal handlers ----------------------------------------------------
287
296
288 def _started_channels(self):
297 def _started_channels(self):
289 """ Called when the kernel manager has started listening.
298 """ Called when the kernel manager has started listening.
290 """
299 """
291 QtGui.QPlainTextEdit.clear(self)
300 self._reset()
292 if self._reading:
293 self._reading = False
294 self.appendPlainText(self._get_banner())
301 self.appendPlainText(self._get_banner())
295 self._show_prompt()
302 self._show_interpreter_prompt()
296
303
297 def _stopped_channels(self):
304 def _stopped_channels(self):
298 """ Called when the kernel manager has stopped listening.
305 """ Called when the kernel manager has stopped listening.
299 """
306 """
307 # FIXME: Print a message here?
300 pass
308 pass
301
309
302 def _document_contents_change(self, position, removed, added):
310 def _document_contents_change(self, position, removed, added):
303 """ Called whenever the document's content changes. Display a calltip
311 """ Called whenever the document's content changes. Display a calltip
304 if appropriate.
312 if appropriate.
305 """
313 """
306 # Calculate where the cursor should be *after* the change:
314 # Calculate where the cursor should be *after* the change:
307 position += added
315 position += added
308
316
309 document = self.document()
317 document = self.document()
310 if position == self.textCursor().position():
318 if position == self.textCursor().position():
311 self._call_tip()
319 self._call_tip()
312
320
313 def _handle_req(self, req):
321 def _handle_req(self, req):
314 # Make sure that all output from the SUB channel has been processed
322 # Make sure that all output from the SUB channel has been processed
315 # before entering readline mode.
323 # before entering readline mode.
316 self.kernel_manager.sub_channel.flush()
324 self.kernel_manager.sub_channel.flush()
317
325
318 def callback(line):
326 def callback(line):
319 self.kernel_manager.rep_channel.readline(line)
327 self.kernel_manager.rep_channel.readline(line)
320 self._readline(callback=callback)
328 self._readline(callback=callback)
321
329
322 def _handle_sub(self, omsg):
330 def _handle_sub(self, omsg):
323 if self._hidden:
331 if self._hidden:
324 return
332 return
325 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
333 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
326 if handler is not None:
334 if handler is not None:
327 handler(omsg)
335 handler(omsg)
328
336
329 def _handle_pyout(self, omsg):
337 def _handle_pyout(self, omsg):
330 session = omsg['parent_header']['session']
338 self.appendPlainText(omsg['content']['data'] + '\n')
331 if session == self.kernel_manager.session.session:
332 self.appendPlainText(omsg['content']['data'] + '\n')
333
339
334 def _handle_stream(self, omsg):
340 def _handle_stream(self, omsg):
335 self.appendPlainText(omsg['content']['data'])
341 self.appendPlainText(omsg['content']['data'])
336 self.moveCursor(QtGui.QTextCursor.End)
342 self.moveCursor(QtGui.QTextCursor.End)
337
343
338 def _handle_execute_reply(self, rep):
344 def _handle_execute_reply(self, rep):
339 if self._hidden:
345 if self._hidden:
340 return
346 return
341
347
342 # Make sure that all output from the SUB channel has been processed
348 # Make sure that all output from the SUB channel has been processed
343 # before writing a new prompt.
349 # before writing a new prompt.
344 self.kernel_manager.sub_channel.flush()
350 self.kernel_manager.sub_channel.flush()
345
351
346 content = rep['content']
352 content = rep['content']
347 status = content['status']
353 status = content['status']
348 if status == 'error':
354 if status == 'error':
349 self.appendPlainText(content['traceback'][-1])
355 self.appendPlainText(content['traceback'][-1])
350 elif status == 'aborted':
356 elif status == 'aborted':
351 text = "ERROR: ABORTED\n"
357 text = "ERROR: ABORTED\n"
352 self.appendPlainText(text)
358 self.appendPlainText(text)
353 self._hidden = True
359 self._hidden = True
354 self._show_prompt()
360 self._show_interpreter_prompt()
355 self.executed.emit(rep)
361 self.executed.emit(rep)
356
362
357 def _handle_complete_reply(self, rep):
363 def _handle_complete_reply(self, rep):
358 cursor = self.textCursor()
364 cursor = self.textCursor()
359 if rep['parent_header']['msg_id'] == self._complete_id and \
365 if rep['parent_header']['msg_id'] == self._complete_id and \
360 cursor.position() == self._complete_pos:
366 cursor.position() == self._complete_pos:
361 text = '.'.join(self._get_context())
367 text = '.'.join(self._get_context())
362 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
368 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
363 self._complete_with_items(cursor, rep['content']['matches'])
369 self._complete_with_items(cursor, rep['content']['matches'])
364
370
365 def _handle_object_info_reply(self, rep):
371 def _handle_object_info_reply(self, rep):
366 cursor = self.textCursor()
372 cursor = self.textCursor()
367 if rep['parent_header']['msg_id'] == self._calltip_id and \
373 if rep['parent_header']['msg_id'] == self._calltip_id and \
368 cursor.position() == self._calltip_pos:
374 cursor.position() == self._calltip_pos:
369 doc = rep['content']['docstring']
375 doc = rep['content']['docstring']
370 if doc:
376 if doc:
371 self._call_tip_widget.show_docstring(doc)
377 self._call_tip_widget.show_docstring(doc)
@@ -1,104 +1,148 b''
1 # System library imports
1 # System library imports
2 from PyQt4 import QtCore, QtGui
2 from PyQt4 import QtCore, QtGui
3
3
4 # Local imports
4 # Local imports
5 from IPython.core.usage import default_banner
5 from IPython.core.usage import default_banner
6 from frontend_widget import FrontendWidget
6 from frontend_widget import FrontendWidget
7
7
8
8
9 class IPythonWidget(FrontendWidget):
9 class IPythonWidget(FrontendWidget):
10 """ A FrontendWidget for an IPython kernel.
10 """ A FrontendWidget for an IPython kernel.
11 """
11 """
12
12
13 # The default stylesheet for prompts, colors, etc.
14 default_stylesheet = """
15 .in-prompt { color: navy; }
16 .in-prompt-number { font-weight: bold; }
17 .out-prompt { color: darkred; }
18 .out-prompt-number { font-weight: bold; }
19 """
20
13 #---------------------------------------------------------------------------
21 #---------------------------------------------------------------------------
14 # 'QObject' interface
22 # 'QObject' interface
15 #---------------------------------------------------------------------------
23 #---------------------------------------------------------------------------
16
24
17 def __init__(self, parent=None):
25 def __init__(self, parent=None):
18 super(IPythonWidget, self).__init__(parent)
26 super(IPythonWidget, self).__init__(parent)
19
27
28 # Initialize protected variables.
20 self._magic_overrides = {}
29 self._magic_overrides = {}
30 self._prompt_count = 0
31
32 # Set a default stylesheet.
33 self.set_style_sheet(self.default_stylesheet)
21
34
22 #---------------------------------------------------------------------------
35 #---------------------------------------------------------------------------
23 # 'ConsoleWidget' abstract interface
36 # 'ConsoleWidget' abstract interface
24 #---------------------------------------------------------------------------
37 #---------------------------------------------------------------------------
25
38
26 def _execute(self, source, hidden):
39 def _execute(self, source, hidden):
27 """ Reimplemented to override magic commands.
40 """ Reimplemented to override magic commands.
28 """
41 """
29 magic_source = source.strip()
42 magic_source = source.strip()
30 if magic_source.startswith('%'):
43 if magic_source.startswith('%'):
31 magic_source = magic_source[1:]
44 magic_source = magic_source[1:]
32 magic, sep, arguments = magic_source.partition(' ')
45 magic, sep, arguments = magic_source.partition(' ')
33 if not magic:
46 if not magic:
34 magic = magic_source
47 magic = magic_source
35
48
36 callback = self._magic_overrides.get(magic)
49 callback = self._magic_overrides.get(magic)
37 if callback:
50 if callback:
38 output = callback(arguments)
51 output = callback(arguments)
39 if output:
52 if output:
40 self.appendPlainText(output)
53 self.appendPlainText(output)
41 self._show_prompt()
54 self._show_interpreter_prompt()
42 else:
55 else:
43 super(IPythonWidget, self)._execute(source, hidden)
56 super(IPythonWidget, self)._execute(source, hidden)
44
57
45 #---------------------------------------------------------------------------
58 #---------------------------------------------------------------------------
46 # 'FrontendWidget' interface
59 # 'FrontendWidget' interface
47 #---------------------------------------------------------------------------
60 #---------------------------------------------------------------------------
48
61
49 def execute_file(self, path, hidden=False):
62 def execute_file(self, path, hidden=False):
50 """ Reimplemented to use the 'run' magic.
63 """ Reimplemented to use the 'run' magic.
51 """
64 """
52 self.execute('run %s' % path, hidden=hidden)
65 self.execute('run %s' % path, hidden=hidden)
53
66
54 #---------------------------------------------------------------------------
67 #---------------------------------------------------------------------------
55 # 'FrontendWidget' protected interface
68 # 'FrontendWidget' protected interface
56 #---------------------------------------------------------------------------
69 #---------------------------------------------------------------------------
57
70
58 def _get_banner(self):
71 def _get_banner(self):
59 """ Reimplemented to a return IPython's default banner.
72 """ Reimplemented to return IPython's default banner.
60 """
73 """
61 return default_banner
74 return default_banner
62
75
76 def _show_interpreter_prompt(self):
77 """ Reimplemented for IPython-style prompts.
78 """
79 self._prompt_count += 1
80 prompt_template = '<span class="in-prompt">%s</span>'
81 prompt_body = '<br/>In [<span class="in-prompt-number">%i</span>]: '
82 prompt = (prompt_template % prompt_body) % self._prompt_count
83 self._show_prompt(prompt, html=True)
84
85 # Update continuation prompt to reflect (possibly) new prompt length.
86 cont_prompt_chars = '...: '
87 space_count = len(self._prompt.lstrip()) - len(cont_prompt_chars)
88 cont_prompt_body = '&nbsp;' * space_count + cont_prompt_chars
89 self._continuation_prompt_html = prompt_template % cont_prompt_body
90
91 #------ Signal handlers ----------------------------------------------------
92
93 def _handle_pyout(self, omsg):
94 """ Reimplemented for IPython-style "display hook".
95 """
96 prompt_template = '<span class="out-prompt">%s</span>'
97 prompt_body = 'Out[<span class="out-prompt-number">%i</span>]: '
98 prompt = (prompt_template % prompt_body) % self._prompt_count
99 self.appendHtml(prompt)
100 self.appendPlainText(omsg['content']['data'] + '\n')
101
63 #---------------------------------------------------------------------------
102 #---------------------------------------------------------------------------
64 # 'IPythonWidget' interface
103 # 'IPythonWidget' interface
65 #---------------------------------------------------------------------------
104 #---------------------------------------------------------------------------
66
105
67 def set_magic_override(self, magic, callback):
106 def set_magic_override(self, magic, callback):
68 """ Overrides an IPython magic command. This magic will be intercepted
107 """ Overrides an IPython magic command. This magic will be intercepted
69 by the frontend rather than passed on to the kernel and 'callback'
108 by the frontend rather than passed on to the kernel and 'callback'
70 will be called with a single argument: a string of argument(s) for
109 will be called with a single argument: a string of argument(s) for
71 the magic. The callback can (optionally) return text to print to the
110 the magic. The callback can (optionally) return text to print to the
72 console.
111 console.
73 """
112 """
74 self._magic_overrides[magic] = callback
113 self._magic_overrides[magic] = callback
75
114
76 def remove_magic_override(self, magic):
115 def remove_magic_override(self, magic):
77 """ Removes the override for the specified magic, if there is one.
116 """ Removes the override for the specified magic, if there is one.
78 """
117 """
79 try:
118 try:
80 del self._magic_overrides[magic]
119 del self._magic_overrides[magic]
81 except KeyError:
120 except KeyError:
82 pass
121 pass
83
122
123 def set_style_sheet(self, stylesheet):
124 """ Sets the style sheet.
125 """
126 self.document().setDefaultStyleSheet(stylesheet)
127
84
128
85 if __name__ == '__main__':
129 if __name__ == '__main__':
86 from IPython.frontend.qt.kernelmanager import QtKernelManager
130 from IPython.frontend.qt.kernelmanager import QtKernelManager
87
131
88 # Don't let Qt or ZMQ swallow KeyboardInterupts.
132 # Don't let Qt or ZMQ swallow KeyboardInterupts.
89 import signal
133 import signal
90 signal.signal(signal.SIGINT, signal.SIG_DFL)
134 signal.signal(signal.SIGINT, signal.SIG_DFL)
91
135
92 # Create a KernelManager.
136 # Create a KernelManager.
93 kernel_manager = QtKernelManager()
137 kernel_manager = QtKernelManager()
94 kernel_manager.start_kernel()
138 kernel_manager.start_kernel()
95 kernel_manager.start_channels()
139 kernel_manager.start_channels()
96
140
97 # Launch the application.
141 # Launch the application.
98 app = QtGui.QApplication([])
142 app = QtGui.QApplication([])
99 widget = IPythonWidget()
143 widget = IPythonWidget()
100 widget.kernel_manager = kernel_manager
144 widget.kernel_manager = kernel_manager
101 widget.setWindowTitle('Python')
145 widget.setWindowTitle('Python')
102 widget.resize(640, 480)
146 widget.resize(640, 480)
103 widget.show()
147 widget.show()
104 app.exec_()
148 app.exec_()
@@ -1,183 +1,182 b''
1 # System library imports.
1 # System library imports.
2 from PyQt4 import QtGui
2 from PyQt4 import QtGui
3 from pygments.lexer import RegexLexer, _TokenType, Text, Error
3 from pygments.lexer import RegexLexer, _TokenType, Text, Error
4 from pygments.lexers import CLexer, CppLexer, PythonLexer
4 from pygments.lexers import PythonLexer
5 from pygments.styles.default import DefaultStyle
5 from pygments.styles.default import DefaultStyle
6 from pygments.token import Comment
6 from pygments.token import Comment
7
7
8
8
9 def get_tokens_unprocessed(self, text, stack=('root',)):
9 def get_tokens_unprocessed(self, text, stack=('root',)):
10 """ Split ``text`` into (tokentype, text) pairs.
10 """ Split ``text`` into (tokentype, text) pairs.
11
11
12 Monkeypatched to store the final stack on the object itself.
12 Monkeypatched to store the final stack on the object itself.
13 """
13 """
14 pos = 0
14 pos = 0
15 tokendefs = self._tokens
15 tokendefs = self._tokens
16 if hasattr(self, '_saved_state_stack'):
16 if hasattr(self, '_saved_state_stack'):
17 statestack = list(self._saved_state_stack)
17 statestack = list(self._saved_state_stack)
18 else:
18 else:
19 statestack = list(stack)
19 statestack = list(stack)
20 statetokens = tokendefs[statestack[-1]]
20 statetokens = tokendefs[statestack[-1]]
21 while 1:
21 while 1:
22 for rexmatch, action, new_state in statetokens:
22 for rexmatch, action, new_state in statetokens:
23 m = rexmatch(text, pos)
23 m = rexmatch(text, pos)
24 if m:
24 if m:
25 if type(action) is _TokenType:
25 if type(action) is _TokenType:
26 yield pos, action, m.group()
26 yield pos, action, m.group()
27 else:
27 else:
28 for item in action(self, m):
28 for item in action(self, m):
29 yield item
29 yield item
30 pos = m.end()
30 pos = m.end()
31 if new_state is not None:
31 if new_state is not None:
32 # state transition
32 # state transition
33 if isinstance(new_state, tuple):
33 if isinstance(new_state, tuple):
34 for state in new_state:
34 for state in new_state:
35 if state == '#pop':
35 if state == '#pop':
36 statestack.pop()
36 statestack.pop()
37 elif state == '#push':
37 elif state == '#push':
38 statestack.append(statestack[-1])
38 statestack.append(statestack[-1])
39 else:
39 else:
40 statestack.append(state)
40 statestack.append(state)
41 elif isinstance(new_state, int):
41 elif isinstance(new_state, int):
42 # pop
42 # pop
43 del statestack[new_state:]
43 del statestack[new_state:]
44 elif new_state == '#push':
44 elif new_state == '#push':
45 statestack.append(statestack[-1])
45 statestack.append(statestack[-1])
46 else:
46 else:
47 assert False, "wrong state def: %r" % new_state
47 assert False, "wrong state def: %r" % new_state
48 statetokens = tokendefs[statestack[-1]]
48 statetokens = tokendefs[statestack[-1]]
49 break
49 break
50 else:
50 else:
51 try:
51 try:
52 if text[pos] == '\n':
52 if text[pos] == '\n':
53 # at EOL, reset state to "root"
53 # at EOL, reset state to "root"
54 pos += 1
54 pos += 1
55 statestack = ['root']
55 statestack = ['root']
56 statetokens = tokendefs['root']
56 statetokens = tokendefs['root']
57 yield pos, Text, u'\n'
57 yield pos, Text, u'\n'
58 continue
58 continue
59 yield pos, Error, text[pos]
59 yield pos, Error, text[pos]
60 pos += 1
60 pos += 1
61 except IndexError:
61 except IndexError:
62 break
62 break
63 self._saved_state_stack = list(statestack)
63 self._saved_state_stack = list(statestack)
64
64
65 # Monkeypatch!
65 # Monkeypatch!
66 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
66 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
67
67
68
68
69 class BlockUserData(QtGui.QTextBlockUserData):
69 class BlockUserData(QtGui.QTextBlockUserData):
70 """ Storage for the user data associated with each line.
70 """ Storage for the user data associated with each line.
71 """
71 """
72
72
73 syntax_stack = ('root',)
73 syntax_stack = ('root',)
74
74
75 def __init__(self, **kwds):
75 def __init__(self, **kwds):
76 for key, value in kwds.iteritems():
76 for key, value in kwds.iteritems():
77 setattr(self, key, value)
77 setattr(self, key, value)
78 QtGui.QTextBlockUserData.__init__(self)
78 QtGui.QTextBlockUserData.__init__(self)
79
79
80 def __repr__(self):
80 def __repr__(self):
81 attrs = ['syntax_stack']
81 attrs = ['syntax_stack']
82 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
82 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
83 for attr in attrs ])
83 for attr in attrs ])
84 return 'BlockUserData(%s)' % kwds
84 return 'BlockUserData(%s)' % kwds
85
85
86
86
87 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
87 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
88 """ Syntax highlighter that uses Pygments for parsing. """
88 """ Syntax highlighter that uses Pygments for parsing. """
89
89
90 def __init__(self, parent, lexer=None):
90 def __init__(self, parent, lexer=None):
91 super(PygmentsHighlighter, self).__init__(parent)
91 super(PygmentsHighlighter, self).__init__(parent)
92
92
93 self._lexer = lexer if lexer else PythonLexer()
93 self._lexer = lexer if lexer else PythonLexer()
94 self._style = DefaultStyle
94 self._style = DefaultStyle
95 # Caches for formats and brushes.
95 # Caches for formats and brushes.
96 self._brushes = {}
96 self._brushes = {}
97 self._formats = {}
97 self._formats = {}
98
98
99 def highlightBlock(self, qstring):
99 def highlightBlock(self, qstring):
100 """ Highlight a block of text.
100 """ Highlight a block of text.
101 """
101 """
102 qstring = unicode(qstring)
102 qstring = unicode(qstring)
103 prev_data = self.previous_block_data()
103 prev_data = self.previous_block_data()
104
104
105 if prev_data is not None:
105 if prev_data is not None:
106 self._lexer._saved_state_stack = prev_data.syntax_stack
106 self._lexer._saved_state_stack = prev_data.syntax_stack
107 elif hasattr(self._lexer, '_saved_state_stack'):
107 elif hasattr(self._lexer, '_saved_state_stack'):
108 del self._lexer._saved_state_stack
108 del self._lexer._saved_state_stack
109
109
110 index = 0
110 index = 0
111 # Lex the text using Pygments
111 # Lex the text using Pygments
112 for token, text in self._lexer.get_tokens(qstring):
112 for token, text in self._lexer.get_tokens(qstring):
113 l = len(text)
113 l = len(text)
114 format = self._get_format(token)
114 format = self._get_format(token)
115 if format is not None:
115 if format is not None:
116 self.setFormat(index, l, format)
116 self.setFormat(index, l, format)
117 index += l
117 index += l
118
118
119 if hasattr(self._lexer, '_saved_state_stack'):
119 if hasattr(self._lexer, '_saved_state_stack'):
120 data = BlockUserData(syntax_stack=self._lexer._saved_state_stack)
120 data = BlockUserData(syntax_stack=self._lexer._saved_state_stack)
121 self.currentBlock().setUserData(data)
121 self.currentBlock().setUserData(data)
122 # Clean up for the next go-round.
122 # Clean up for the next go-round.
123 del self._lexer._saved_state_stack
123 del self._lexer._saved_state_stack
124
124
125 def previous_block_data(self):
125 def previous_block_data(self):
126 """ Convenience method for returning the previous block's user data.
126 """ Convenience method for returning the previous block's user data.
127 """
127 """
128 return self.currentBlock().previous().userData()
128 return self.currentBlock().previous().userData()
129
129
130 def _get_format(self, token):
130 def _get_format(self, token):
131 """ Returns a QTextCharFormat for token or None.
131 """ Returns a QTextCharFormat for token or None.
132 """
132 """
133 if token in self._formats:
133 if token in self._formats:
134 return self._formats[token]
134 return self._formats[token]
135 result = None
135 result = None
136 for key, value in self._style.style_for_token(token) .items():
136 for key, value in self._style.style_for_token(token).items():
137 if value:
137 if value:
138 if result is None:
138 if result is None:
139 result = QtGui.QTextCharFormat()
139 result = QtGui.QTextCharFormat()
140 if key == 'color':
140 if key == 'color':
141 result.setForeground(self._get_brush(value))
141 result.setForeground(self._get_brush(value))
142 elif key == 'bgcolor':
142 elif key == 'bgcolor':
143 result.setBackground(self._get_brush(value))
143 result.setBackground(self._get_brush(value))
144 elif key == 'bold':
144 elif key == 'bold':
145 result.setFontWeight(QtGui.QFont.Bold)
145 result.setFontWeight(QtGui.QFont.Bold)
146 elif key == 'italic':
146 elif key == 'italic':
147 result.setFontItalic(True)
147 result.setFontItalic(True)
148 elif key == 'underline':
148 elif key == 'underline':
149 result.setUnderlineStyle(
149 result.setUnderlineStyle(
150 QtGui.QTextCharFormat.SingleUnderline)
150 QtGui.QTextCharFormat.SingleUnderline)
151 elif key == 'sans':
151 elif key == 'sans':
152 result.setFontStyleHint(QtGui.QFont.SansSerif)
152 result.setFontStyleHint(QtGui.QFont.SansSerif)
153 elif key == 'roman':
153 elif key == 'roman':
154 result.setFontStyleHint(QtGui.QFont.Times)
154 result.setFontStyleHint(QtGui.QFont.Times)
155 elif key == 'mono':
155 elif key == 'mono':
156 result.setFontStyleHint(QtGui.QFont.TypeWriter)
156 result.setFontStyleHint(QtGui.QFont.TypeWriter)
157 elif key == 'border':
157 elif key == 'border':
158 # Borders are normally used for errors. We can't do a border
158 # Borders are normally used for errors. We can't do a border
159 # so instead we do a wavy underline
159 # so instead we do a wavy underline
160 result.setUnderlineStyle(
160 result.setUnderlineStyle(
161 QtGui.QTextCharFormat.WaveUnderline)
161 QtGui.QTextCharFormat.WaveUnderline)
162 result.setUnderlineColor(self._get_color(value))
162 result.setUnderlineColor(self._get_color(value))
163 self._formats[token] = result
163 self._formats[token] = result
164 return result
164 return result
165
165
166 def _get_brush(self, color):
166 def _get_brush(self, color):
167 """ Returns a brush for the color.
167 """ Returns a brush for the color.
168 """
168 """
169 result = self._brushes.get(color)
169 result = self._brushes.get(color)
170 if result is None:
170 if result is None:
171 qcolor = self._get_color(color)
171 qcolor = self._get_color(color)
172 result = QtGui.QBrush(qcolor)
172 result = QtGui.QBrush(qcolor)
173 self._brushes[color] = result
173 self._brushes[color] = result
174
175 return result
174 return result
176
175
177 def _get_color(self, color):
176 def _get_color(self, color):
178 qcolor = QtGui.QColor()
177 qcolor = QtGui.QColor()
179 qcolor.setRgb(int(color[:2],base=16),
178 qcolor.setRgb(int(color[:2], base=16),
180 int(color[2:4], base=16),
179 int(color[2:4], base=16),
181 int(color[4:6], base=16))
180 int(color[4:6], base=16))
182 return qcolor
181 return qcolor
183
182
General Comments 0
You need to be logged in to leave comments. Login now