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