##// END OF EJS Templates
Numerous usability enhancements to the Qt console widget, particularly with regard to key bindings.
epatters -
Show More
@@ -1,1392 +1,1430 b''
1 # Standard library imports
1 # Standard library imports
2 import re
2 import re
3 import sys
3 import sys
4 from textwrap import dedent
4 from textwrap import dedent
5
5
6 # System library imports
6 # System library imports
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8
8
9 # Local imports
9 # Local imports
10 from IPython.config.configurable import Configurable
10 from IPython.config.configurable import Configurable
11 from IPython.frontend.qt.util import MetaQObjectHasTraits
11 from IPython.frontend.qt.util import MetaQObjectHasTraits
12 from IPython.utils.traitlets import Bool, Enum, Int
12 from IPython.utils.traitlets import Bool, Enum, Int
13 from ansi_code_processor import QtAnsiCodeProcessor
13 from ansi_code_processor import QtAnsiCodeProcessor
14 from completion_widget import CompletionWidget
14 from completion_widget import CompletionWidget
15
15
16
16
17 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
17 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
18 """ A QPlainTextEdit suitable for use with ConsoleWidget.
18 """ A QPlainTextEdit suitable for use with ConsoleWidget.
19 """
19 """
20 # Prevents text from being moved by drag and drop. Note that is not, for
20 # Prevents text from being moved by drag and drop. Note that is not, for
21 # some reason, sufficient to catch drag events in the ConsoleWidget's
21 # some reason, sufficient to catch drag events in the ConsoleWidget's
22 # event filter.
22 # event filter.
23 def dragEnterEvent(self, event): pass
23 def dragEnterEvent(self, event): pass
24 def dragLeaveEvent(self, event): pass
24 def dragLeaveEvent(self, event): pass
25 def dragMoveEvent(self, event): pass
25 def dragMoveEvent(self, event): pass
26 def dropEvent(self, event): pass
26 def dropEvent(self, event): pass
27
27
28 class ConsoleTextEdit(QtGui.QTextEdit):
28 class ConsoleTextEdit(QtGui.QTextEdit):
29 """ A QTextEdit suitable for use with ConsoleWidget.
29 """ A QTextEdit suitable for use with ConsoleWidget.
30 """
30 """
31 # See above.
31 # See above.
32 def dragEnterEvent(self, event): pass
32 def dragEnterEvent(self, event): pass
33 def dragLeaveEvent(self, event): pass
33 def dragLeaveEvent(self, event): pass
34 def dragMoveEvent(self, event): pass
34 def dragMoveEvent(self, event): pass
35 def dropEvent(self, event): pass
35 def dropEvent(self, event): pass
36
36
37
37
38 class ConsoleWidget(Configurable, QtGui.QWidget):
38 class ConsoleWidget(Configurable, QtGui.QWidget):
39 """ An abstract base class for console-type widgets. This class has
39 """ An abstract base class for console-type widgets. This class has
40 functionality for:
40 functionality for:
41
41
42 * Maintaining a prompt and editing region
42 * Maintaining a prompt and editing region
43 * Providing the traditional Unix-style console keyboard shortcuts
43 * Providing the traditional Unix-style console keyboard shortcuts
44 * Performing tab completion
44 * Performing tab completion
45 * Paging text
45 * Paging text
46 * Handling ANSI escape codes
46 * Handling ANSI escape codes
47
47
48 ConsoleWidget also provides a number of utility methods that will be
48 ConsoleWidget also provides a number of utility methods that will be
49 convenient to implementors of a console-style widget.
49 convenient to implementors of a console-style widget.
50 """
50 """
51 __metaclass__ = MetaQObjectHasTraits
51 __metaclass__ = MetaQObjectHasTraits
52
52
53 # Whether to process ANSI escape codes.
53 # Whether to process ANSI escape codes.
54 ansi_codes = Bool(True, config=True)
54 ansi_codes = Bool(True, config=True)
55
55
56 # The maximum number of lines of text before truncation. Specifying a
56 # The maximum number of lines of text before truncation. Specifying a
57 # non-positive number disables text truncation (not recommended).
57 # non-positive number disables text truncation (not recommended).
58 buffer_size = Int(500, config=True)
58 buffer_size = Int(500, config=True)
59
59
60 # Whether to use a list widget or plain text output for tab completion.
60 # Whether to use a list widget or plain text output for tab completion.
61 gui_completion = Bool(True, config=True)
61 gui_completion = Bool(True, config=True)
62
62
63 # The type of underlying text widget to use. Valid values are 'plain', which
63 # The type of underlying text widget to use. Valid values are 'plain', which
64 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
64 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
65 # NOTE: this value can only be specified during initialization.
65 # NOTE: this value can only be specified during initialization.
66 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
66 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
67
67
68 # The type of paging to use. Valid values are:
68 # The type of paging to use. Valid values are:
69 # 'inside' : The widget pages like a traditional terminal pager.
69 # 'inside' : The widget pages like a traditional terminal pager.
70 # 'hsplit' : When paging is requested, the widget is split
70 # 'hsplit' : When paging is requested, the widget is split
71 # horizontally. The top pane contains the console, and the
71 # horizontally. The top pane contains the console, and the
72 # bottom pane contains the paged text.
72 # bottom pane contains the paged text.
73 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
73 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
74 # 'custom' : No action is taken by the widget beyond emitting a
74 # 'custom' : No action is taken by the widget beyond emitting a
75 # 'custom_page_requested(str)' signal.
75 # 'custom_page_requested(str)' signal.
76 # 'none' : The text is written directly to the console.
76 # 'none' : The text is written directly to the console.
77 # NOTE: this value can only be specified during initialization.
77 # NOTE: this value can only be specified during initialization.
78 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
78 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
79 default_value='inside', config=True)
79 default_value='inside', config=True)
80
80
81 # Whether to override ShortcutEvents for the keybindings defined by this
81 # Whether to override ShortcutEvents for the keybindings defined by this
82 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
82 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
83 # priority (when it has focus) over, e.g., window-level menu shortcuts.
83 # priority (when it has focus) over, e.g., window-level menu shortcuts.
84 override_shortcuts = Bool(False)
84 override_shortcuts = Bool(False)
85
85
86 # Signals that indicate ConsoleWidget state.
86 # Signals that indicate ConsoleWidget state.
87 copy_available = QtCore.pyqtSignal(bool)
87 copy_available = QtCore.pyqtSignal(bool)
88 redo_available = QtCore.pyqtSignal(bool)
88 redo_available = QtCore.pyqtSignal(bool)
89 undo_available = QtCore.pyqtSignal(bool)
89 undo_available = QtCore.pyqtSignal(bool)
90
90
91 # Signal emitted when paging is needed and the paging style has been
91 # Signal emitted when paging is needed and the paging style has been
92 # specified as 'custom'.
92 # specified as 'custom'.
93 custom_page_requested = QtCore.pyqtSignal(object)
93 custom_page_requested = QtCore.pyqtSignal(object)
94
94
95 # Protected class variables.
95 # Protected class variables.
96 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
96 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
97 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
97 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
98 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
98 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
99 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
99 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
100 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
100 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
101 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
101 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
102 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
102 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
103 _shortcuts = set(_ctrl_down_remap.keys() +
103 _shortcuts = set(_ctrl_down_remap.keys() +
104 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
104 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V, QtCore.Qt.Key_O ])
105
105
106 #---------------------------------------------------------------------------
106 #---------------------------------------------------------------------------
107 # 'QObject' interface
107 # 'QObject' interface
108 #---------------------------------------------------------------------------
108 #---------------------------------------------------------------------------
109
109
110 def __init__(self, parent=None, **kw):
110 def __init__(self, parent=None, **kw):
111 """ Create a ConsoleWidget.
111 """ Create a ConsoleWidget.
112
112
113 Parameters:
113 Parameters:
114 -----------
114 -----------
115 parent : QWidget, optional [default None]
115 parent : QWidget, optional [default None]
116 The parent for this widget.
116 The parent for this widget.
117 """
117 """
118 QtGui.QWidget.__init__(self, parent)
118 QtGui.QWidget.__init__(self, parent)
119 Configurable.__init__(self, **kw)
119 Configurable.__init__(self, **kw)
120
120
121 # Create the layout and underlying text widget.
121 # Create the layout and underlying text widget.
122 layout = QtGui.QStackedLayout(self)
122 layout = QtGui.QStackedLayout(self)
123 layout.setContentsMargins(0, 0, 0, 0)
123 layout.setContentsMargins(0, 0, 0, 0)
124 self._control = self._create_control()
124 self._control = self._create_control()
125 self._page_control = None
125 self._page_control = None
126 self._splitter = None
126 self._splitter = None
127 if self.paging in ('hsplit', 'vsplit'):
127 if self.paging in ('hsplit', 'vsplit'):
128 self._splitter = QtGui.QSplitter()
128 self._splitter = QtGui.QSplitter()
129 if self.paging == 'hsplit':
129 if self.paging == 'hsplit':
130 self._splitter.setOrientation(QtCore.Qt.Horizontal)
130 self._splitter.setOrientation(QtCore.Qt.Horizontal)
131 else:
131 else:
132 self._splitter.setOrientation(QtCore.Qt.Vertical)
132 self._splitter.setOrientation(QtCore.Qt.Vertical)
133 self._splitter.addWidget(self._control)
133 self._splitter.addWidget(self._control)
134 layout.addWidget(self._splitter)
134 layout.addWidget(self._splitter)
135 else:
135 else:
136 layout.addWidget(self._control)
136 layout.addWidget(self._control)
137
137
138 # Create the paging widget, if necessary.
138 # Create the paging widget, if necessary.
139 if self.paging in ('inside', 'hsplit', 'vsplit'):
139 if self.paging in ('inside', 'hsplit', 'vsplit'):
140 self._page_control = self._create_page_control()
140 self._page_control = self._create_page_control()
141 if self._splitter:
141 if self._splitter:
142 self._page_control.hide()
142 self._page_control.hide()
143 self._splitter.addWidget(self._page_control)
143 self._splitter.addWidget(self._page_control)
144 else:
144 else:
145 layout.addWidget(self._page_control)
145 layout.addWidget(self._page_control)
146
146
147 # Initialize protected variables. Some variables contain useful state
147 # Initialize protected variables. Some variables contain useful state
148 # information for subclasses; they should be considered read-only.
148 # information for subclasses; they should be considered read-only.
149 self._ansi_processor = QtAnsiCodeProcessor()
149 self._ansi_processor = QtAnsiCodeProcessor()
150 self._completion_widget = CompletionWidget(self._control)
150 self._completion_widget = CompletionWidget(self._control)
151 self._continuation_prompt = '> '
151 self._continuation_prompt = '> '
152 self._continuation_prompt_html = None
152 self._continuation_prompt_html = None
153 self._executing = False
153 self._executing = False
154 self._prompt = ''
154 self._prompt = ''
155 self._prompt_html = None
155 self._prompt_html = None
156 self._prompt_pos = 0
156 self._prompt_pos = 0
157 self._prompt_sep = ''
157 self._prompt_sep = ''
158 self._reading = False
158 self._reading = False
159 self._reading_callback = None
159 self._reading_callback = None
160 self._tab_width = 8
160 self._tab_width = 8
161
161
162 # Set a monospaced font.
162 # Set a monospaced font.
163 self.reset_font()
163 self.reset_font()
164
164
165 def eventFilter(self, obj, event):
165 def eventFilter(self, obj, event):
166 """ Reimplemented to ensure a console-like behavior in the underlying
166 """ Reimplemented to ensure a console-like behavior in the underlying
167 text widget.
167 text widgets.
168 """
168 """
169 # Re-map keys for all filtered widgets.
170 etype = event.type()
169 etype = event.type()
171 if etype == QtCore.QEvent.KeyPress and \
170 if etype == QtCore.QEvent.KeyPress:
172 self._control_key_down(event.modifiers()) and \
171
173 event.key() in self._ctrl_down_remap:
172 # Re-map keys for all filtered widgets.
174 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
173 key = event.key()
175 self._ctrl_down_remap[event.key()],
174 if self._control_key_down(event.modifiers()) and \
176 QtCore.Qt.NoModifier)
175 key in self._ctrl_down_remap:
177 QtGui.qApp.sendEvent(obj, new_event)
176 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
178 return True
177 self._ctrl_down_remap[key],
178 QtCore.Qt.NoModifier)
179 QtGui.qApp.sendEvent(obj, new_event)
180 return True
181
182 elif obj == self._control:
183 return self._event_filter_console_keypress(event)
184
185 elif obj == self._page_control:
186 return self._event_filter_page_keypress(event)
179
187
180 # Override shortucts for all filtered widgets. Note that on Mac OS it is
188 # Override shortucts for all filtered widgets. Note that on Mac OS it is
181 # always unnecessary to override shortcuts, hence the check below (users
189 # always unnecessary to override shortcuts, hence the check below (users
182 # should just use the Control key instead of the Command key).
190 # should just use the Control key instead of the Command key).
183 elif etype == QtCore.QEvent.ShortcutOverride and \
191 elif etype == QtCore.QEvent.ShortcutOverride and \
184 sys.platform != 'darwin' and \
192 sys.platform != 'darwin' and \
185 self._control_key_down(event.modifiers()) and \
193 self._control_key_down(event.modifiers()) and \
186 event.key() in self._shortcuts:
194 event.key() in self._shortcuts:
187 event.accept()
195 event.accept()
188 return False
196 return False
189
197
190 elif etype == QtCore.QEvent.KeyPress:
191 if obj == self._control:
192 return self._event_filter_console_keypress(event)
193 elif obj == self._page_control:
194 return self._event_filter_page_keypress(event)
195
196 return super(ConsoleWidget, self).eventFilter(obj, event)
198 return super(ConsoleWidget, self).eventFilter(obj, event)
197
199
198 #---------------------------------------------------------------------------
200 #---------------------------------------------------------------------------
199 # 'QWidget' interface
201 # 'QWidget' interface
200 #---------------------------------------------------------------------------
202 #---------------------------------------------------------------------------
201
203
202 def sizeHint(self):
204 def sizeHint(self):
203 """ Reimplemented to suggest a size that is 80 characters wide and
205 """ Reimplemented to suggest a size that is 80 characters wide and
204 25 lines high.
206 25 lines high.
205 """
207 """
206 font_metrics = QtGui.QFontMetrics(self.font)
208 font_metrics = QtGui.QFontMetrics(self.font)
207 margin = (self._control.frameWidth() +
209 margin = (self._control.frameWidth() +
208 self._control.document().documentMargin()) * 2
210 self._control.document().documentMargin()) * 2
209 style = self.style()
211 style = self.style()
210 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
212 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
211
213
212 # Despite my best efforts to take the various margins into account, the
214 # Despite my best efforts to take the various margins into account, the
213 # width is still coming out a bit too small, so we include a fudge
215 # width is still coming out a bit too small, so we include a fudge
214 # factor of one character here.
216 # factor of one character here.
215 width = font_metrics.maxWidth() * 81 + margin
217 width = font_metrics.maxWidth() * 81 + margin
216 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
218 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
217 if self.paging == 'hsplit':
219 if self.paging == 'hsplit':
218 width = width * 2 + splitwidth
220 width = width * 2 + splitwidth
219
221
220 height = font_metrics.height() * 25 + margin
222 height = font_metrics.height() * 25 + margin
221 if self.paging == 'vsplit':
223 if self.paging == 'vsplit':
222 height = height * 2 + splitwidth
224 height = height * 2 + splitwidth
223
225
224 return QtCore.QSize(width, height)
226 return QtCore.QSize(width, height)
225
227
226 #---------------------------------------------------------------------------
228 #---------------------------------------------------------------------------
227 # 'ConsoleWidget' public interface
229 # 'ConsoleWidget' public interface
228 #---------------------------------------------------------------------------
230 #---------------------------------------------------------------------------
229
231
230 def can_paste(self):
232 def can_paste(self):
231 """ Returns whether text can be pasted from the clipboard.
233 """ Returns whether text can be pasted from the clipboard.
232 """
234 """
233 # Only accept text that can be ASCII encoded.
235 # Only accept text that can be ASCII encoded.
234 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
236 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
235 text = QtGui.QApplication.clipboard().text()
237 text = QtGui.QApplication.clipboard().text()
236 if not text.isEmpty():
238 if not text.isEmpty():
237 try:
239 try:
238 str(text)
240 str(text)
239 return True
241 return True
240 except UnicodeEncodeError:
242 except UnicodeEncodeError:
241 pass
243 pass
242 return False
244 return False
243
245
244 def clear(self, keep_input=True):
246 def clear(self, keep_input=True):
245 """ Clear the console, then write a new prompt. If 'keep_input' is set,
247 """ Clear the console, then write a new prompt. If 'keep_input' is set,
246 restores the old input buffer when the new prompt is written.
248 restores the old input buffer when the new prompt is written.
247 """
249 """
248 if keep_input:
250 if keep_input:
249 input_buffer = self.input_buffer
251 input_buffer = self.input_buffer
250 self._control.clear()
252 self._control.clear()
251 self._show_prompt()
253 self._show_prompt()
252 if keep_input:
254 if keep_input:
253 self.input_buffer = input_buffer
255 self.input_buffer = input_buffer
254
256
255 def copy(self):
257 def copy(self):
256 """ Copy the current selected text to the clipboard.
258 """ Copy the current selected text to the clipboard.
257 """
259 """
258 self._control.copy()
260 self._control.copy()
259
261
260 def execute(self, source=None, hidden=False, interactive=False):
262 def execute(self, source=None, hidden=False, interactive=False):
261 """ Executes source or the input buffer, possibly prompting for more
263 """ Executes source or the input buffer, possibly prompting for more
262 input.
264 input.
263
265
264 Parameters:
266 Parameters:
265 -----------
267 -----------
266 source : str, optional
268 source : str, optional
267
269
268 The source to execute. If not specified, the input buffer will be
270 The source to execute. If not specified, the input buffer will be
269 used. If specified and 'hidden' is False, the input buffer will be
271 used. If specified and 'hidden' is False, the input buffer will be
270 replaced with the source before execution.
272 replaced with the source before execution.
271
273
272 hidden : bool, optional (default False)
274 hidden : bool, optional (default False)
273
275
274 If set, no output will be shown and the prompt will not be modified.
276 If set, no output will be shown and the prompt will not be modified.
275 In other words, it will be completely invisible to the user that
277 In other words, it will be completely invisible to the user that
276 an execution has occurred.
278 an execution has occurred.
277
279
278 interactive : bool, optional (default False)
280 interactive : bool, optional (default False)
279
281
280 Whether the console is to treat the source as having been manually
282 Whether the console is to treat the source as having been manually
281 entered by the user. The effect of this parameter depends on the
283 entered by the user. The effect of this parameter depends on the
282 subclass implementation.
284 subclass implementation.
283
285
284 Raises:
286 Raises:
285 -------
287 -------
286 RuntimeError
288 RuntimeError
287 If incomplete input is given and 'hidden' is True. In this case,
289 If incomplete input is given and 'hidden' is True. In this case,
288 it is not possible to prompt for more input.
290 it is not possible to prompt for more input.
289
291
290 Returns:
292 Returns:
291 --------
293 --------
292 A boolean indicating whether the source was executed.
294 A boolean indicating whether the source was executed.
293 """
295 """
294 # WARNING: The order in which things happen here is very particular, in
296 # WARNING: The order in which things happen here is very particular, in
295 # large part because our syntax highlighting is fragile. If you change
297 # large part because our syntax highlighting is fragile. If you change
296 # something, test carefully!
298 # something, test carefully!
297
299
298 # Decide what to execute.
300 # Decide what to execute.
299 if source is None:
301 if source is None:
300 source = self.input_buffer
302 source = self.input_buffer
301 if not hidden:
303 if not hidden:
302 # A newline is appended later, but it should be considered part
304 # A newline is appended later, but it should be considered part
303 # of the input buffer.
305 # of the input buffer.
304 source += '\n'
306 source += '\n'
305 elif not hidden:
307 elif not hidden:
306 self.input_buffer = source
308 self.input_buffer = source
307
309
308 # Execute the source or show a continuation prompt if it is incomplete.
310 # Execute the source or show a continuation prompt if it is incomplete.
309 complete = self._is_complete(source, interactive)
311 complete = self._is_complete(source, interactive)
310 if hidden:
312 if hidden:
311 if complete:
313 if complete:
312 self._execute(source, hidden)
314 self._execute(source, hidden)
313 else:
315 else:
314 error = 'Incomplete noninteractive input: "%s"'
316 error = 'Incomplete noninteractive input: "%s"'
315 raise RuntimeError(error % source)
317 raise RuntimeError(error % source)
316 else:
318 else:
317 if complete:
319 if complete:
318 self._append_plain_text('\n')
320 self._append_plain_text('\n')
319 self._executing_input_buffer = self.input_buffer
321 self._executing_input_buffer = self.input_buffer
320 self._executing = True
322 self._executing = True
321 self._prompt_finished()
323 self._prompt_finished()
322
324
323 # The maximum block count is only in effect during execution.
325 # The maximum block count is only in effect during execution.
324 # This ensures that _prompt_pos does not become invalid due to
326 # This ensures that _prompt_pos does not become invalid due to
325 # text truncation.
327 # text truncation.
326 self._control.document().setMaximumBlockCount(self.buffer_size)
328 self._control.document().setMaximumBlockCount(self.buffer_size)
327
329
328 # Setting a positive maximum block count will automatically
330 # Setting a positive maximum block count will automatically
329 # disable the undo/redo history, but just to be safe:
331 # disable the undo/redo history, but just to be safe:
330 self._control.setUndoRedoEnabled(False)
332 self._control.setUndoRedoEnabled(False)
331
333
332 self._execute(source, hidden)
334 self._execute(source, hidden)
333
335
334 else:
336 else:
335 # Do this inside an edit block so continuation prompts are
337 # Do this inside an edit block so continuation prompts are
336 # removed seamlessly via undo/redo.
338 # removed seamlessly via undo/redo.
337 cursor = self._get_end_cursor()
339 cursor = self._get_end_cursor()
338 cursor.beginEditBlock()
340 cursor.beginEditBlock()
339 cursor.insertText('\n')
341 cursor.insertText('\n')
340 self._insert_continuation_prompt(cursor)
342 self._insert_continuation_prompt(cursor)
341 self._control.moveCursor(QtGui.QTextCursor.End)
343 self._control.moveCursor(QtGui.QTextCursor.End)
342 cursor.endEditBlock()
344 cursor.endEditBlock()
343
345
344 return complete
346 return complete
345
347
346 def _get_input_buffer(self):
348 def _get_input_buffer(self):
347 """ The text that the user has entered entered at the current prompt.
349 """ The text that the user has entered entered at the current prompt.
348 """
350 """
349 # If we're executing, the input buffer may not even exist anymore due to
351 # If we're executing, the input buffer may not even exist anymore due to
350 # the limit imposed by 'buffer_size'. Therefore, we store it.
352 # the limit imposed by 'buffer_size'. Therefore, we store it.
351 if self._executing:
353 if self._executing:
352 return self._executing_input_buffer
354 return self._executing_input_buffer
353
355
354 cursor = self._get_end_cursor()
356 cursor = self._get_end_cursor()
355 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
357 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
356 input_buffer = str(cursor.selection().toPlainText())
358 input_buffer = str(cursor.selection().toPlainText())
357
359
358 # Strip out continuation prompts.
360 # Strip out continuation prompts.
359 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
361 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
360
362
361 def _set_input_buffer(self, string):
363 def _set_input_buffer(self, string):
362 """ Replaces the text in the input buffer with 'string'.
364 """ Replaces the text in the input buffer with 'string'.
363 """
365 """
364 # For now, it is an error to modify the input buffer during execution.
366 # For now, it is an error to modify the input buffer during execution.
365 if self._executing:
367 if self._executing:
366 raise RuntimeError("Cannot change input buffer during execution.")
368 raise RuntimeError("Cannot change input buffer during execution.")
367
369
368 # Remove old text.
370 # Remove old text.
369 cursor = self._get_end_cursor()
371 cursor = self._get_end_cursor()
370 cursor.beginEditBlock()
372 cursor.beginEditBlock()
371 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
373 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
372 cursor.removeSelectedText()
374 cursor.removeSelectedText()
373
375
374 # Insert new text with continuation prompts.
376 # Insert new text with continuation prompts.
375 lines = string.splitlines(True)
377 lines = string.splitlines(True)
376 if lines:
378 if lines:
377 self._append_plain_text(lines[0])
379 self._append_plain_text(lines[0])
378 for i in xrange(1, len(lines)):
380 for i in xrange(1, len(lines)):
379 if self._continuation_prompt_html is None:
381 if self._continuation_prompt_html is None:
380 self._append_plain_text(self._continuation_prompt)
382 self._append_plain_text(self._continuation_prompt)
381 else:
383 else:
382 self._append_html(self._continuation_prompt_html)
384 self._append_html(self._continuation_prompt_html)
383 self._append_plain_text(lines[i])
385 self._append_plain_text(lines[i])
384 cursor.endEditBlock()
386 cursor.endEditBlock()
385 self._control.moveCursor(QtGui.QTextCursor.End)
387 self._control.moveCursor(QtGui.QTextCursor.End)
386
388
387 input_buffer = property(_get_input_buffer, _set_input_buffer)
389 input_buffer = property(_get_input_buffer, _set_input_buffer)
388
390
389 def _get_font(self):
391 def _get_font(self):
390 """ The base font being used by the ConsoleWidget.
392 """ The base font being used by the ConsoleWidget.
391 """
393 """
392 return self._control.document().defaultFont()
394 return self._control.document().defaultFont()
393
395
394 def _set_font(self, font):
396 def _set_font(self, font):
395 """ Sets the base font for the ConsoleWidget to the specified QFont.
397 """ Sets the base font for the ConsoleWidget to the specified QFont.
396 """
398 """
397 font_metrics = QtGui.QFontMetrics(font)
399 font_metrics = QtGui.QFontMetrics(font)
398 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
400 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
399
401
400 self._completion_widget.setFont(font)
402 self._completion_widget.setFont(font)
401 self._control.document().setDefaultFont(font)
403 self._control.document().setDefaultFont(font)
402 if self._page_control:
404 if self._page_control:
403 self._page_control.document().setDefaultFont(font)
405 self._page_control.document().setDefaultFont(font)
404
406
405 font = property(_get_font, _set_font)
407 font = property(_get_font, _set_font)
406
408
407 def paste(self):
409 def paste(self):
408 """ Paste the contents of the clipboard into the input region.
410 """ Paste the contents of the clipboard into the input region.
409 """
411 """
410 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
412 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
411 try:
413 try:
412 text = str(QtGui.QApplication.clipboard().text())
414 text = str(QtGui.QApplication.clipboard().text())
413 except UnicodeEncodeError:
415 except UnicodeEncodeError:
414 pass
416 pass
415 else:
417 else:
416 self._insert_plain_text_into_buffer(dedent(text))
418 self._insert_plain_text_into_buffer(dedent(text))
417
419
418 def print_(self, printer):
420 def print_(self, printer):
419 """ Print the contents of the ConsoleWidget to the specified QPrinter.
421 """ Print the contents of the ConsoleWidget to the specified QPrinter.
420 """
422 """
421 self._control.print_(printer)
423 self._control.print_(printer)
422
424
423 def redo(self):
425 def redo(self):
424 """ Redo the last operation. If there is no operation to redo, nothing
426 """ Redo the last operation. If there is no operation to redo, nothing
425 happens.
427 happens.
426 """
428 """
427 self._control.redo()
429 self._control.redo()
428
430
429 def reset_font(self):
431 def reset_font(self):
430 """ Sets the font to the default fixed-width font for this platform.
432 """ Sets the font to the default fixed-width font for this platform.
431 """
433 """
432 # FIXME: font family and size should be configurable by the user.
434 # FIXME: font family and size should be configurable by the user.
433 if sys.platform == 'win32':
435 if sys.platform == 'win32':
434 # FIXME: we should test whether Consolas is available and use it
436 # FIXME: we should test whether Consolas is available and use it
435 # first if it is. Consolas ships by default from Vista onwards,
437 # first if it is. Consolas ships by default from Vista onwards,
436 # it's *vastly* more readable and prettier than Courier, and is
438 # it's *vastly* more readable and prettier than Courier, and is
437 # often installed even on XP systems. So we should first check for
439 # often installed even on XP systems. So we should first check for
438 # it, and only fallback to Courier if absolutely necessary.
440 # it, and only fallback to Courier if absolutely necessary.
439 name = 'Courier'
441 name = 'Courier'
440 elif sys.platform == 'darwin':
442 elif sys.platform == 'darwin':
441 name = 'Monaco'
443 name = 'Monaco'
442 else:
444 else:
443 name = 'Monospace'
445 name = 'Monospace'
444 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
446 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
445 font.setStyleHint(QtGui.QFont.TypeWriter)
447 font.setStyleHint(QtGui.QFont.TypeWriter)
446 self._set_font(font)
448 self._set_font(font)
447
449
448 def select_all(self):
450 def select_all(self):
449 """ Selects all the text in the buffer.
451 """ Selects all the text in the buffer.
450 """
452 """
451 self._control.selectAll()
453 self._control.selectAll()
452
454
453 def _get_tab_width(self):
455 def _get_tab_width(self):
454 """ The width (in terms of space characters) for tab characters.
456 """ The width (in terms of space characters) for tab characters.
455 """
457 """
456 return self._tab_width
458 return self._tab_width
457
459
458 def _set_tab_width(self, tab_width):
460 def _set_tab_width(self, tab_width):
459 """ Sets the width (in terms of space characters) for tab characters.
461 """ Sets the width (in terms of space characters) for tab characters.
460 """
462 """
461 font_metrics = QtGui.QFontMetrics(self.font)
463 font_metrics = QtGui.QFontMetrics(self.font)
462 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
464 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
463
465
464 self._tab_width = tab_width
466 self._tab_width = tab_width
465
467
466 tab_width = property(_get_tab_width, _set_tab_width)
468 tab_width = property(_get_tab_width, _set_tab_width)
467
469
468 def undo(self):
470 def undo(self):
469 """ Undo the last operation. If there is no operation to undo, nothing
471 """ Undo the last operation. If there is no operation to undo, nothing
470 happens.
472 happens.
471 """
473 """
472 self._control.undo()
474 self._control.undo()
473
475
474 #---------------------------------------------------------------------------
476 #---------------------------------------------------------------------------
475 # 'ConsoleWidget' abstract interface
477 # 'ConsoleWidget' abstract interface
476 #---------------------------------------------------------------------------
478 #---------------------------------------------------------------------------
477
479
478 def _is_complete(self, source, interactive):
480 def _is_complete(self, source, interactive):
479 """ Returns whether 'source' can be executed. When triggered by an
481 """ Returns whether 'source' can be executed. When triggered by an
480 Enter/Return key press, 'interactive' is True; otherwise, it is
482 Enter/Return key press, 'interactive' is True; otherwise, it is
481 False.
483 False.
482 """
484 """
483 raise NotImplementedError
485 raise NotImplementedError
484
486
485 def _execute(self, source, hidden):
487 def _execute(self, source, hidden):
486 """ Execute 'source'. If 'hidden', do not show any output.
488 """ Execute 'source'. If 'hidden', do not show any output.
487 """
489 """
488 raise NotImplementedError
490 raise NotImplementedError
489
491
490 def _prompt_started_hook(self):
492 def _prompt_started_hook(self):
491 """ Called immediately after a new prompt is displayed.
493 """ Called immediately after a new prompt is displayed.
492 """
494 """
493 pass
495 pass
494
496
495 def _prompt_finished_hook(self):
497 def _prompt_finished_hook(self):
496 """ Called immediately after a prompt is finished, i.e. when some input
498 """ Called immediately after a prompt is finished, i.e. when some input
497 will be processed and a new prompt displayed.
499 will be processed and a new prompt displayed.
498 """
500 """
499 pass
501 pass
500
502
501 def _up_pressed(self):
503 def _up_pressed(self):
502 """ Called when the up key is pressed. Returns whether to continue
504 """ Called when the up key is pressed. Returns whether to continue
503 processing the event.
505 processing the event.
504 """
506 """
505 return True
507 return True
506
508
507 def _down_pressed(self):
509 def _down_pressed(self):
508 """ Called when the down key is pressed. Returns whether to continue
510 """ Called when the down key is pressed. Returns whether to continue
509 processing the event.
511 processing the event.
510 """
512 """
511 return True
513 return True
512
514
513 def _tab_pressed(self):
515 def _tab_pressed(self):
514 """ Called when the tab key is pressed. Returns whether to continue
516 """ Called when the tab key is pressed. Returns whether to continue
515 processing the event.
517 processing the event.
516 """
518 """
517 return False
519 return False
518
520
519 #--------------------------------------------------------------------------
521 #--------------------------------------------------------------------------
520 # 'ConsoleWidget' protected interface
522 # 'ConsoleWidget' protected interface
521 #--------------------------------------------------------------------------
523 #--------------------------------------------------------------------------
522
524
523 def _append_html(self, html):
525 def _append_html(self, html):
524 """ Appends html at the end of the console buffer.
526 """ Appends html at the end of the console buffer.
525 """
527 """
526 cursor = self._get_end_cursor()
528 cursor = self._get_end_cursor()
527 self._insert_html(cursor, html)
529 self._insert_html(cursor, html)
528
530
529 def _append_html_fetching_plain_text(self, html):
531 def _append_html_fetching_plain_text(self, html):
530 """ Appends 'html', then returns the plain text version of it.
532 """ Appends 'html', then returns the plain text version of it.
531 """
533 """
532 cursor = self._get_end_cursor()
534 cursor = self._get_end_cursor()
533 return self._insert_html_fetching_plain_text(cursor, html)
535 return self._insert_html_fetching_plain_text(cursor, html)
534
536
535 def _append_plain_text(self, text):
537 def _append_plain_text(self, text):
536 """ Appends plain text at the end of the console buffer, processing
538 """ Appends plain text at the end of the console buffer, processing
537 ANSI codes if enabled.
539 ANSI codes if enabled.
538 """
540 """
539 cursor = self._get_end_cursor()
541 cursor = self._get_end_cursor()
540 self._insert_plain_text(cursor, text)
542 self._insert_plain_text(cursor, text)
541
543
542 def _append_plain_text_keeping_prompt(self, text):
544 def _append_plain_text_keeping_prompt(self, text):
543 """ Writes 'text' after the current prompt, then restores the old prompt
545 """ Writes 'text' after the current prompt, then restores the old prompt
544 with its old input buffer.
546 with its old input buffer.
545 """
547 """
546 input_buffer = self.input_buffer
548 input_buffer = self.input_buffer
547 self._append_plain_text('\n')
549 self._append_plain_text('\n')
548 self._prompt_finished()
550 self._prompt_finished()
549
551
550 self._append_plain_text(text)
552 self._append_plain_text(text)
551 self._show_prompt()
553 self._show_prompt()
552 self.input_buffer = input_buffer
554 self.input_buffer = input_buffer
553
555
554 def _complete_with_items(self, cursor, items):
556 def _complete_with_items(self, cursor, items):
555 """ Performs completion with 'items' at the specified cursor location.
557 """ Performs completion with 'items' at the specified cursor location.
556 """
558 """
557 if len(items) == 1:
559 if len(items) == 1:
558 cursor.setPosition(self._control.textCursor().position(),
560 cursor.setPosition(self._control.textCursor().position(),
559 QtGui.QTextCursor.KeepAnchor)
561 QtGui.QTextCursor.KeepAnchor)
560 cursor.insertText(items[0])
562 cursor.insertText(items[0])
561 elif len(items) > 1:
563 elif len(items) > 1:
562 if self.gui_completion:
564 if self.gui_completion:
563 self._completion_widget.show_items(cursor, items)
565 self._completion_widget.show_items(cursor, items)
564 else:
566 else:
565 text = self._format_as_columns(items)
567 text = self._format_as_columns(items)
566 self._append_plain_text_keeping_prompt(text)
568 self._append_plain_text_keeping_prompt(text)
567
569
568 def _control_key_down(self, modifiers):
570 def _control_key_down(self, modifiers):
569 """ Given a KeyboardModifiers flags object, return whether the Control
571 """ Given a KeyboardModifiers flags object, return whether the Control
570 key is down (on Mac OS, treat the Command key as a synonym for
572 key is down (on Mac OS, treat the Command key as a synonym for
571 Control).
573 Control).
572 """
574 """
573 down = bool(modifiers & QtCore.Qt.ControlModifier)
575 down = bool(modifiers & QtCore.Qt.ControlModifier)
574
576
575 # Note: on Mac OS, ControlModifier corresponds to the Command key while
577 # Note: on Mac OS, ControlModifier corresponds to the Command key while
576 # MetaModifier corresponds to the Control key.
578 # MetaModifier corresponds to the Control key.
577 if sys.platform == 'darwin':
579 if sys.platform == 'darwin':
578 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
580 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
579
581
580 return down
582 return down
581
583
582 def _create_control(self):
584 def _create_control(self):
583 """ Creates and connects the underlying text widget.
585 """ Creates and connects the underlying text widget.
584 """
586 """
585 if self.kind == 'plain':
587 if self.kind == 'plain':
586 control = ConsolePlainTextEdit()
588 control = ConsolePlainTextEdit()
587 elif self.kind == 'rich':
589 elif self.kind == 'rich':
588 control = ConsoleTextEdit()
590 control = ConsoleTextEdit()
589 control.setAcceptRichText(False)
591 control.setAcceptRichText(False)
590 control.installEventFilter(self)
592 control.installEventFilter(self)
591 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
593 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
592 control.customContextMenuRequested.connect(self._show_context_menu)
594 control.customContextMenuRequested.connect(self._show_context_menu)
593 control.copyAvailable.connect(self.copy_available)
595 control.copyAvailable.connect(self.copy_available)
594 control.redoAvailable.connect(self.redo_available)
596 control.redoAvailable.connect(self.redo_available)
595 control.undoAvailable.connect(self.undo_available)
597 control.undoAvailable.connect(self.undo_available)
596 control.setReadOnly(True)
598 control.setReadOnly(True)
597 control.setUndoRedoEnabled(False)
599 control.setUndoRedoEnabled(False)
598 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
600 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
599 return control
601 return control
600
602
601 def _create_page_control(self):
603 def _create_page_control(self):
602 """ Creates and connects the underlying paging widget.
604 """ Creates and connects the underlying paging widget.
603 """
605 """
604 control = ConsolePlainTextEdit()
606 control = ConsolePlainTextEdit()
605 control.installEventFilter(self)
607 control.installEventFilter(self)
606 control.setReadOnly(True)
608 control.setReadOnly(True)
607 control.setUndoRedoEnabled(False)
609 control.setUndoRedoEnabled(False)
608 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
610 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
609 return control
611 return control
610
612
611 def _event_filter_console_keypress(self, event):
613 def _event_filter_console_keypress(self, event):
612 """ Filter key events for the underlying text widget to create a
614 """ Filter key events for the underlying text widget to create a
613 console-like interface.
615 console-like interface.
614 """
616 """
615 intercepted = False
617 intercepted = False
616 cursor = self._control.textCursor()
618 cursor = self._control.textCursor()
617 position = cursor.position()
619 position = cursor.position()
618 key = event.key()
620 key = event.key()
619 ctrl_down = self._control_key_down(event.modifiers())
621 ctrl_down = self._control_key_down(event.modifiers())
620 alt_down = event.modifiers() & QtCore.Qt.AltModifier
622 alt_down = event.modifiers() & QtCore.Qt.AltModifier
621 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
623 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
622
624
623 if event.matches(QtGui.QKeySequence.Paste):
625 if event.matches(QtGui.QKeySequence.Paste):
624 # Call our paste instead of the underlying text widget's.
626 # Call our paste instead of the underlying text widget's.
625 self.paste()
627 self.paste()
626 intercepted = True
628 intercepted = True
627
629
628 elif ctrl_down:
630 elif ctrl_down:
629 if key == QtCore.Qt.Key_K:
631 if key == QtCore.Qt.Key_K:
630 if self._in_buffer(position):
632 if self._in_buffer(position):
631 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
633 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
632 QtGui.QTextCursor.KeepAnchor)
634 QtGui.QTextCursor.KeepAnchor)
633 if not cursor.hasSelection():
635 if not cursor.hasSelection():
634 # Line deletion (remove continuation prompt)
636 # Line deletion (remove continuation prompt)
635 cursor.movePosition(QtGui.QTextCursor.NextBlock,
637 cursor.movePosition(QtGui.QTextCursor.NextBlock,
636 QtGui.QTextCursor.KeepAnchor)
638 QtGui.QTextCursor.KeepAnchor)
637 cursor.movePosition(QtGui.QTextCursor.Right,
639 cursor.movePosition(QtGui.QTextCursor.Right,
638 QtGui.QTextCursor.KeepAnchor,
640 QtGui.QTextCursor.KeepAnchor,
639 len(self._continuation_prompt))
641 len(self._continuation_prompt))
640 cursor.removeSelectedText()
642 cursor.removeSelectedText()
641 intercepted = True
643 intercepted = True
642
644
643 elif key == QtCore.Qt.Key_L:
645 elif key == QtCore.Qt.Key_L:
644 # It would be better to simply move the prompt block to the top
646 # It would be better to simply move the prompt block to the top
645 # of the control viewport. QPlainTextEdit has a private method
647 # of the control viewport. QPlainTextEdit has a private method
646 # to do this (setTopBlock), but it cannot be duplicated here
648 # to do this (setTopBlock), but it cannot be duplicated here
647 # because it requires access to the QTextControl that underlies
649 # because it requires access to the QTextControl that underlies
648 # both QPlainTextEdit and QTextEdit. In short, this can only be
650 # both QPlainTextEdit and QTextEdit. In short, this can only be
649 # achieved by appending newlines after the prompt, which is a
651 # achieved by appending newlines after the prompt, which is a
650 # gigantic hack and likely to cause other problems.
652 # gigantic hack and likely to cause other problems.
651 self.clear()
653 self.clear()
652 intercepted = True
654 intercepted = True
653
655
656 elif key == QtCore.Qt.Key_O:
657 if self._page_control and self._page_control.isVisible():
658 self._page_control.setFocus()
659 intercept = True
660
654 elif key == QtCore.Qt.Key_X:
661 elif key == QtCore.Qt.Key_X:
655 # FIXME: Instead of disabling cut completely, only allow it
662 # FIXME: Instead of disabling cut completely, only allow it
656 # when safe.
663 # when safe.
657 intercepted = True
664 intercepted = True
658
665
659 elif key == QtCore.Qt.Key_Y:
666 elif key == QtCore.Qt.Key_Y:
660 self.paste()
667 self.paste()
661 intercepted = True
668 intercepted = True
662
669
663 elif alt_down:
670 elif alt_down:
664 if key == QtCore.Qt.Key_B:
671 if key == QtCore.Qt.Key_B:
665 self._set_cursor(self._get_word_start_cursor(position))
672 self._set_cursor(self._get_word_start_cursor(position))
666 intercepted = True
673 intercepted = True
667
674
668 elif key == QtCore.Qt.Key_F:
675 elif key == QtCore.Qt.Key_F:
669 self._set_cursor(self._get_word_end_cursor(position))
676 self._set_cursor(self._get_word_end_cursor(position))
670 intercepted = True
677 intercepted = True
671
678
672 elif key == QtCore.Qt.Key_Backspace:
679 elif key == QtCore.Qt.Key_Backspace:
673 cursor = self._get_word_start_cursor(position)
680 cursor = self._get_word_start_cursor(position)
674 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
681 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
675 cursor.removeSelectedText()
682 cursor.removeSelectedText()
676 intercepted = True
683 intercepted = True
677
684
678 elif key == QtCore.Qt.Key_D:
685 elif key == QtCore.Qt.Key_D:
679 cursor = self._get_word_end_cursor(position)
686 cursor = self._get_word_end_cursor(position)
680 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
687 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
681 cursor.removeSelectedText()
688 cursor.removeSelectedText()
682 intercepted = True
689 intercepted = True
683
690
691 elif key == QtCore.Qt.Key_Greater:
692 self._control.moveCursor(QtGui.QTextCursor.End)
693 intercepted = True
694
695 elif key == QtCore.Qt.Key_Less:
696 self._control.setTextCursor(self._get_prompt_cursor())
697 intercepted = True
698
684 else:
699 else:
685 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
700 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
686 intercepted = True
701 intercepted = True
687 if self._in_buffer(position):
702 if self._in_buffer(position):
688 if self._reading:
703 if self._reading:
689 self._append_plain_text('\n')
704 self._append_plain_text('\n')
690 self._reading = False
705 self._reading = False
691 if self._reading_callback:
706 if self._reading_callback:
692 self._reading_callback()
707 self._reading_callback()
693
708
694 # If there is only whitespace after the cursor, execute.
709 # If there is only whitespace after the cursor, execute.
695 # Otherwise, split the line with a continuation prompt.
710 # Otherwise, split the line with a continuation prompt.
696 elif not self._executing:
711 elif not self._executing:
697 cursor.movePosition(QtGui.QTextCursor.End,
712 cursor.movePosition(QtGui.QTextCursor.End,
698 QtGui.QTextCursor.KeepAnchor)
713 QtGui.QTextCursor.KeepAnchor)
699 if cursor.selectedText().trimmed().isEmpty():
714 if cursor.selectedText().trimmed().isEmpty():
700 self.execute(interactive=True)
715 self.execute(interactive=True)
701 else:
716 else:
702 cursor.beginEditBlock()
717 cursor.beginEditBlock()
703 cursor.setPosition(position)
718 cursor.setPosition(position)
704 cursor.insertText('\n')
719 cursor.insertText('\n')
705 self._insert_continuation_prompt(cursor)
720 self._insert_continuation_prompt(cursor)
706
721
707 # Ensure that the whole input buffer is visible.
722 # Ensure that the whole input buffer is visible.
708 # FIXME: This will not be usable if the input buffer
723 # FIXME: This will not be usable if the input buffer
709 # is taller than the console widget.
724 # is taller than the console widget.
710 self._control.moveCursor(QtGui.QTextCursor.End)
725 self._control.moveCursor(QtGui.QTextCursor.End)
711 self._control.setTextCursor(cursor)
726 self._control.setTextCursor(cursor)
712
727
713 cursor.endEditBlock()
728 cursor.endEditBlock()
714
729
715 elif key == QtCore.Qt.Key_Up:
730 elif key == QtCore.Qt.Key_Up:
716 if self._reading or not self._up_pressed():
731 if self._reading or not self._up_pressed():
717 intercepted = True
732 intercepted = True
718 else:
733 else:
719 prompt_line = self._get_prompt_cursor().blockNumber()
734 prompt_line = self._get_prompt_cursor().blockNumber()
720 intercepted = cursor.blockNumber() <= prompt_line
735 intercepted = cursor.blockNumber() <= prompt_line
721
736
722 elif key == QtCore.Qt.Key_Down:
737 elif key == QtCore.Qt.Key_Down:
723 if self._reading or not self._down_pressed():
738 if self._reading or not self._down_pressed():
724 intercepted = True
739 intercepted = True
725 else:
740 else:
726 end_line = self._get_end_cursor().blockNumber()
741 end_line = self._get_end_cursor().blockNumber()
727 intercepted = cursor.blockNumber() == end_line
742 intercepted = cursor.blockNumber() == end_line
728
743
729 elif key == QtCore.Qt.Key_Tab:
744 elif key == QtCore.Qt.Key_Tab:
730 if not self._reading:
745 if not self._reading:
731 intercepted = not self._tab_pressed()
746 intercepted = not self._tab_pressed()
732
747
733 elif key == QtCore.Qt.Key_Left:
748 elif key == QtCore.Qt.Key_Left:
734 intercepted = not self._in_buffer(position - 1)
749 intercepted = not self._in_buffer(position - 1)
735
750
736 elif key == QtCore.Qt.Key_Home:
751 elif key == QtCore.Qt.Key_Home:
737 start_line = cursor.blockNumber()
752 start_line = cursor.blockNumber()
738 if start_line == self._get_prompt_cursor().blockNumber():
753 if start_line == self._get_prompt_cursor().blockNumber():
739 start_pos = self._prompt_pos
754 start_pos = self._prompt_pos
740 else:
755 else:
741 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
756 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
742 QtGui.QTextCursor.KeepAnchor)
757 QtGui.QTextCursor.KeepAnchor)
743 start_pos = cursor.position()
758 start_pos = cursor.position()
744 start_pos += len(self._continuation_prompt)
759 start_pos += len(self._continuation_prompt)
745 cursor.setPosition(position)
760 cursor.setPosition(position)
746 if shift_down and self._in_buffer(position):
761 if shift_down and self._in_buffer(position):
747 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
762 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
748 else:
763 else:
749 cursor.setPosition(start_pos)
764 cursor.setPosition(start_pos)
750 self._set_cursor(cursor)
765 self._set_cursor(cursor)
751 intercepted = True
766 intercepted = True
752
767
753 elif key == QtCore.Qt.Key_Backspace:
768 elif key == QtCore.Qt.Key_Backspace:
754
769
755 # Line deletion (remove continuation prompt)
770 # Line deletion (remove continuation prompt)
756 len_prompt = len(self._continuation_prompt)
771 len_prompt = len(self._continuation_prompt)
757 if not self._reading and \
772 if not self._reading and \
758 cursor.columnNumber() == len_prompt and \
773 cursor.columnNumber() == len_prompt and \
759 position != self._prompt_pos:
774 position != self._prompt_pos:
760 cursor.beginEditBlock()
775 cursor.beginEditBlock()
761 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
776 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
762 QtGui.QTextCursor.KeepAnchor)
777 QtGui.QTextCursor.KeepAnchor)
763 cursor.removeSelectedText()
778 cursor.removeSelectedText()
764 cursor.deletePreviousChar()
779 cursor.deletePreviousChar()
765 cursor.endEditBlock()
780 cursor.endEditBlock()
766 intercepted = True
781 intercepted = True
767
782
768 # Regular backwards deletion
783 # Regular backwards deletion
769 else:
784 else:
770 anchor = cursor.anchor()
785 anchor = cursor.anchor()
771 if anchor == position:
786 if anchor == position:
772 intercepted = not self._in_buffer(position - 1)
787 intercepted = not self._in_buffer(position - 1)
773 else:
788 else:
774 intercepted = not self._in_buffer(min(anchor, position))
789 intercepted = not self._in_buffer(min(anchor, position))
775
790
776 elif key == QtCore.Qt.Key_Delete:
791 elif key == QtCore.Qt.Key_Delete:
777
792
778 # Line deletion (remove continuation prompt)
793 # Line deletion (remove continuation prompt)
779 if not self._reading and cursor.atBlockEnd() and not \
794 if not self._reading and cursor.atBlockEnd() and not \
780 cursor.hasSelection():
795 cursor.hasSelection():
781 cursor.movePosition(QtGui.QTextCursor.NextBlock,
796 cursor.movePosition(QtGui.QTextCursor.NextBlock,
782 QtGui.QTextCursor.KeepAnchor)
797 QtGui.QTextCursor.KeepAnchor)
783 cursor.movePosition(QtGui.QTextCursor.Right,
798 cursor.movePosition(QtGui.QTextCursor.Right,
784 QtGui.QTextCursor.KeepAnchor,
799 QtGui.QTextCursor.KeepAnchor,
785 len(self._continuation_prompt))
800 len(self._continuation_prompt))
786 cursor.removeSelectedText()
801 cursor.removeSelectedText()
787 intercepted = True
802 intercepted = True
788
803
789 # Regular forwards deletion:
804 # Regular forwards deletion:
790 else:
805 else:
791 anchor = cursor.anchor()
806 anchor = cursor.anchor()
792 intercepted = (not self._in_buffer(anchor) or
807 intercepted = (not self._in_buffer(anchor) or
793 not self._in_buffer(position))
808 not self._in_buffer(position))
794
809
795 # Don't move the cursor if control is down to allow copy-paste using
810 # Don't move the cursor if control is down to allow copy-paste using
796 # the keyboard in any part of the buffer.
811 # the keyboard in any part of the buffer.
797 if not ctrl_down:
812 if not ctrl_down:
798 self._keep_cursor_in_buffer()
813 self._keep_cursor_in_buffer()
799
814
800 return intercepted
815 return intercepted
801
816
802 def _event_filter_page_keypress(self, event):
817 def _event_filter_page_keypress(self, event):
803 """ Filter key events for the paging widget to create console-like
818 """ Filter key events for the paging widget to create console-like
804 interface.
819 interface.
805 """
820 """
806 key = event.key()
821 key = event.key()
822 ctrl_down = self._control_key_down(event.modifiers())
823 alt_down = event.modifiers() & QtCore.Qt.AltModifier
824
825 if ctrl_down:
826 if key == QtCore.Qt.Key_O:
827 self._control.setFocus()
828 intercept = True
807
829
808 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
830 elif alt_down:
831 if key == QtCore.Qt.Key_Greater:
832 self._page_control.moveCursor(QtGui.QTextCursor.End)
833 intercepted = True
834
835 elif key == QtCore.Qt.Key_Less:
836 self._page_control.moveCursor(QtGui.QTextCursor.Start)
837 intercepted = True
838
839 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
809 if self._splitter:
840 if self._splitter:
810 self._page_control.hide()
841 self._page_control.hide()
811 else:
842 else:
812 self.layout().setCurrentWidget(self._control)
843 self.layout().setCurrentWidget(self._control)
813 return True
844 return True
814
845
815 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
846 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
816 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
847 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
817 QtCore.Qt.Key_Down,
848 QtCore.Qt.Key_PageDown,
849 QtCore.Qt.NoModifier)
850 QtGui.qApp.sendEvent(self._page_control, new_event)
851 return True
852
853 elif key == QtCore.Qt.Key_Backspace:
854 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
855 QtCore.Qt.Key_PageUp,
818 QtCore.Qt.NoModifier)
856 QtCore.Qt.NoModifier)
819 QtGui.qApp.sendEvent(self._page_control, new_event)
857 QtGui.qApp.sendEvent(self._page_control, new_event)
820 return True
858 return True
821
859
822 return False
860 return False
823
861
824 def _format_as_columns(self, items, separator=' '):
862 def _format_as_columns(self, items, separator=' '):
825 """ Transform a list of strings into a single string with columns.
863 """ Transform a list of strings into a single string with columns.
826
864
827 Parameters
865 Parameters
828 ----------
866 ----------
829 items : sequence of strings
867 items : sequence of strings
830 The strings to process.
868 The strings to process.
831
869
832 separator : str, optional [default is two spaces]
870 separator : str, optional [default is two spaces]
833 The string that separates columns.
871 The string that separates columns.
834
872
835 Returns
873 Returns
836 -------
874 -------
837 The formatted string.
875 The formatted string.
838 """
876 """
839 # Note: this code is adapted from columnize 0.3.2.
877 # Note: this code is adapted from columnize 0.3.2.
840 # See http://code.google.com/p/pycolumnize/
878 # See http://code.google.com/p/pycolumnize/
841
879
842 # Calculate the number of characters available.
880 # Calculate the number of characters available.
843 width = self._control.viewport().width()
881 width = self._control.viewport().width()
844 char_width = QtGui.QFontMetrics(self.font).maxWidth()
882 char_width = QtGui.QFontMetrics(self.font).maxWidth()
845 displaywidth = max(10, (width / char_width) - 1)
883 displaywidth = max(10, (width / char_width) - 1)
846
884
847 # Some degenerate cases.
885 # Some degenerate cases.
848 size = len(items)
886 size = len(items)
849 if size == 0:
887 if size == 0:
850 return '\n'
888 return '\n'
851 elif size == 1:
889 elif size == 1:
852 return '%s\n' % str(items[0])
890 return '%s\n' % str(items[0])
853
891
854 # Try every row count from 1 upwards
892 # Try every row count from 1 upwards
855 array_index = lambda nrows, row, col: nrows*col + row
893 array_index = lambda nrows, row, col: nrows*col + row
856 for nrows in range(1, size):
894 for nrows in range(1, size):
857 ncols = (size + nrows - 1) // nrows
895 ncols = (size + nrows - 1) // nrows
858 colwidths = []
896 colwidths = []
859 totwidth = -len(separator)
897 totwidth = -len(separator)
860 for col in range(ncols):
898 for col in range(ncols):
861 # Get max column width for this column
899 # Get max column width for this column
862 colwidth = 0
900 colwidth = 0
863 for row in range(nrows):
901 for row in range(nrows):
864 i = array_index(nrows, row, col)
902 i = array_index(nrows, row, col)
865 if i >= size: break
903 if i >= size: break
866 x = items[i]
904 x = items[i]
867 colwidth = max(colwidth, len(x))
905 colwidth = max(colwidth, len(x))
868 colwidths.append(colwidth)
906 colwidths.append(colwidth)
869 totwidth += colwidth + len(separator)
907 totwidth += colwidth + len(separator)
870 if totwidth > displaywidth:
908 if totwidth > displaywidth:
871 break
909 break
872 if totwidth <= displaywidth:
910 if totwidth <= displaywidth:
873 break
911 break
874
912
875 # The smallest number of rows computed and the max widths for each
913 # The smallest number of rows computed and the max widths for each
876 # column has been obtained. Now we just have to format each of the rows.
914 # column has been obtained. Now we just have to format each of the rows.
877 string = ''
915 string = ''
878 for row in range(nrows):
916 for row in range(nrows):
879 texts = []
917 texts = []
880 for col in range(ncols):
918 for col in range(ncols):
881 i = row + nrows*col
919 i = row + nrows*col
882 if i >= size:
920 if i >= size:
883 texts.append('')
921 texts.append('')
884 else:
922 else:
885 texts.append(items[i])
923 texts.append(items[i])
886 while texts and not texts[-1]:
924 while texts and not texts[-1]:
887 del texts[-1]
925 del texts[-1]
888 for col in range(len(texts)):
926 for col in range(len(texts)):
889 texts[col] = texts[col].ljust(colwidths[col])
927 texts[col] = texts[col].ljust(colwidths[col])
890 string += '%s\n' % str(separator.join(texts))
928 string += '%s\n' % str(separator.join(texts))
891 return string
929 return string
892
930
893 def _get_block_plain_text(self, block):
931 def _get_block_plain_text(self, block):
894 """ Given a QTextBlock, return its unformatted text.
932 """ Given a QTextBlock, return its unformatted text.
895 """
933 """
896 cursor = QtGui.QTextCursor(block)
934 cursor = QtGui.QTextCursor(block)
897 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
935 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
898 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
936 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
899 QtGui.QTextCursor.KeepAnchor)
937 QtGui.QTextCursor.KeepAnchor)
900 return str(cursor.selection().toPlainText())
938 return str(cursor.selection().toPlainText())
901
939
902 def _get_cursor(self):
940 def _get_cursor(self):
903 """ Convenience method that returns a cursor for the current position.
941 """ Convenience method that returns a cursor for the current position.
904 """
942 """
905 return self._control.textCursor()
943 return self._control.textCursor()
906
944
907 def _get_end_cursor(self):
945 def _get_end_cursor(self):
908 """ Convenience method that returns a cursor for the last character.
946 """ Convenience method that returns a cursor for the last character.
909 """
947 """
910 cursor = self._control.textCursor()
948 cursor = self._control.textCursor()
911 cursor.movePosition(QtGui.QTextCursor.End)
949 cursor.movePosition(QtGui.QTextCursor.End)
912 return cursor
950 return cursor
913
951
914 def _get_input_buffer_cursor_column(self):
952 def _get_input_buffer_cursor_column(self):
915 """ Returns the column of the cursor in the input buffer, excluding the
953 """ Returns the column of the cursor in the input buffer, excluding the
916 contribution by the prompt, or -1 if there is no such column.
954 contribution by the prompt, or -1 if there is no such column.
917 """
955 """
918 prompt = self._get_input_buffer_cursor_prompt()
956 prompt = self._get_input_buffer_cursor_prompt()
919 if prompt is None:
957 if prompt is None:
920 return -1
958 return -1
921 else:
959 else:
922 cursor = self._control.textCursor()
960 cursor = self._control.textCursor()
923 return cursor.columnNumber() - len(prompt)
961 return cursor.columnNumber() - len(prompt)
924
962
925 def _get_input_buffer_cursor_line(self):
963 def _get_input_buffer_cursor_line(self):
926 """ Returns line of the input buffer that contains the cursor, or None
964 """ Returns line of the input buffer that contains the cursor, or None
927 if there is no such line.
965 if there is no such line.
928 """
966 """
929 prompt = self._get_input_buffer_cursor_prompt()
967 prompt = self._get_input_buffer_cursor_prompt()
930 if prompt is None:
968 if prompt is None:
931 return None
969 return None
932 else:
970 else:
933 cursor = self._control.textCursor()
971 cursor = self._control.textCursor()
934 text = self._get_block_plain_text(cursor.block())
972 text = self._get_block_plain_text(cursor.block())
935 return text[len(prompt):]
973 return text[len(prompt):]
936
974
937 def _get_input_buffer_cursor_prompt(self):
975 def _get_input_buffer_cursor_prompt(self):
938 """ Returns the (plain text) prompt for line of the input buffer that
976 """ Returns the (plain text) prompt for line of the input buffer that
939 contains the cursor, or None if there is no such line.
977 contains the cursor, or None if there is no such line.
940 """
978 """
941 if self._executing:
979 if self._executing:
942 return None
980 return None
943 cursor = self._control.textCursor()
981 cursor = self._control.textCursor()
944 if cursor.position() >= self._prompt_pos:
982 if cursor.position() >= self._prompt_pos:
945 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
983 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
946 return self._prompt
984 return self._prompt
947 else:
985 else:
948 return self._continuation_prompt
986 return self._continuation_prompt
949 else:
987 else:
950 return None
988 return None
951
989
952 def _get_prompt_cursor(self):
990 def _get_prompt_cursor(self):
953 """ Convenience method that returns a cursor for the prompt position.
991 """ Convenience method that returns a cursor for the prompt position.
954 """
992 """
955 cursor = self._control.textCursor()
993 cursor = self._control.textCursor()
956 cursor.setPosition(self._prompt_pos)
994 cursor.setPosition(self._prompt_pos)
957 return cursor
995 return cursor
958
996
959 def _get_selection_cursor(self, start, end):
997 def _get_selection_cursor(self, start, end):
960 """ Convenience method that returns a cursor with text selected between
998 """ Convenience method that returns a cursor with text selected between
961 the positions 'start' and 'end'.
999 the positions 'start' and 'end'.
962 """
1000 """
963 cursor = self._control.textCursor()
1001 cursor = self._control.textCursor()
964 cursor.setPosition(start)
1002 cursor.setPosition(start)
965 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1003 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
966 return cursor
1004 return cursor
967
1005
968 def _get_word_start_cursor(self, position):
1006 def _get_word_start_cursor(self, position):
969 """ Find the start of the word to the left the given position. If a
1007 """ Find the start of the word to the left the given position. If a
970 sequence of non-word characters precedes the first word, skip over
1008 sequence of non-word characters precedes the first word, skip over
971 them. (This emulates the behavior of bash, emacs, etc.)
1009 them. (This emulates the behavior of bash, emacs, etc.)
972 """
1010 """
973 document = self._control.document()
1011 document = self._control.document()
974 position -= 1
1012 position -= 1
975 while position >= self._prompt_pos and \
1013 while position >= self._prompt_pos and \
976 not document.characterAt(position).isLetterOrNumber():
1014 not document.characterAt(position).isLetterOrNumber():
977 position -= 1
1015 position -= 1
978 while position >= self._prompt_pos and \
1016 while position >= self._prompt_pos and \
979 document.characterAt(position).isLetterOrNumber():
1017 document.characterAt(position).isLetterOrNumber():
980 position -= 1
1018 position -= 1
981 cursor = self._control.textCursor()
1019 cursor = self._control.textCursor()
982 cursor.setPosition(position + 1)
1020 cursor.setPosition(position + 1)
983 return cursor
1021 return cursor
984
1022
985 def _get_word_end_cursor(self, position):
1023 def _get_word_end_cursor(self, position):
986 """ Find the end of the word to the right the given position. If a
1024 """ Find the end of the word to the right the given position. If a
987 sequence of non-word characters precedes the first word, skip over
1025 sequence of non-word characters precedes the first word, skip over
988 them. (This emulates the behavior of bash, emacs, etc.)
1026 them. (This emulates the behavior of bash, emacs, etc.)
989 """
1027 """
990 document = self._control.document()
1028 document = self._control.document()
991 end = self._get_end_cursor().position()
1029 end = self._get_end_cursor().position()
992 while position < end and \
1030 while position < end and \
993 not document.characterAt(position).isLetterOrNumber():
1031 not document.characterAt(position).isLetterOrNumber():
994 position += 1
1032 position += 1
995 while position < end and \
1033 while position < end and \
996 document.characterAt(position).isLetterOrNumber():
1034 document.characterAt(position).isLetterOrNumber():
997 position += 1
1035 position += 1
998 cursor = self._control.textCursor()
1036 cursor = self._control.textCursor()
999 cursor.setPosition(position)
1037 cursor.setPosition(position)
1000 return cursor
1038 return cursor
1001
1039
1002 def _insert_continuation_prompt(self, cursor):
1040 def _insert_continuation_prompt(self, cursor):
1003 """ Inserts new continuation prompt using the specified cursor.
1041 """ Inserts new continuation prompt using the specified cursor.
1004 """
1042 """
1005 if self._continuation_prompt_html is None:
1043 if self._continuation_prompt_html is None:
1006 self._insert_plain_text(cursor, self._continuation_prompt)
1044 self._insert_plain_text(cursor, self._continuation_prompt)
1007 else:
1045 else:
1008 self._continuation_prompt = self._insert_html_fetching_plain_text(
1046 self._continuation_prompt = self._insert_html_fetching_plain_text(
1009 cursor, self._continuation_prompt_html)
1047 cursor, self._continuation_prompt_html)
1010
1048
1011 def _insert_html(self, cursor, html):
1049 def _insert_html(self, cursor, html):
1012 """ Inserts HTML using the specified cursor in such a way that future
1050 """ Inserts HTML using the specified cursor in such a way that future
1013 formatting is unaffected.
1051 formatting is unaffected.
1014 """
1052 """
1015 cursor.beginEditBlock()
1053 cursor.beginEditBlock()
1016 cursor.insertHtml(html)
1054 cursor.insertHtml(html)
1017
1055
1018 # After inserting HTML, the text document "remembers" it's in "html
1056 # After inserting HTML, the text document "remembers" it's in "html
1019 # mode", which means that subsequent calls adding plain text will result
1057 # mode", which means that subsequent calls adding plain text will result
1020 # in unwanted formatting, lost tab characters, etc. The following code
1058 # in unwanted formatting, lost tab characters, etc. The following code
1021 # hacks around this behavior, which I consider to be a bug in Qt, by
1059 # hacks around this behavior, which I consider to be a bug in Qt, by
1022 # (crudely) resetting the document's style state.
1060 # (crudely) resetting the document's style state.
1023 cursor.movePosition(QtGui.QTextCursor.Left,
1061 cursor.movePosition(QtGui.QTextCursor.Left,
1024 QtGui.QTextCursor.KeepAnchor)
1062 QtGui.QTextCursor.KeepAnchor)
1025 if cursor.selection().toPlainText() == ' ':
1063 if cursor.selection().toPlainText() == ' ':
1026 cursor.removeSelectedText()
1064 cursor.removeSelectedText()
1027 else:
1065 else:
1028 cursor.movePosition(QtGui.QTextCursor.Right)
1066 cursor.movePosition(QtGui.QTextCursor.Right)
1029 cursor.insertText(' ', QtGui.QTextCharFormat())
1067 cursor.insertText(' ', QtGui.QTextCharFormat())
1030 cursor.endEditBlock()
1068 cursor.endEditBlock()
1031
1069
1032 def _insert_html_fetching_plain_text(self, cursor, html):
1070 def _insert_html_fetching_plain_text(self, cursor, html):
1033 """ Inserts HTML using the specified cursor, then returns its plain text
1071 """ Inserts HTML using the specified cursor, then returns its plain text
1034 version.
1072 version.
1035 """
1073 """
1036 cursor.beginEditBlock()
1074 cursor.beginEditBlock()
1037 cursor.removeSelectedText()
1075 cursor.removeSelectedText()
1038
1076
1039 start = cursor.position()
1077 start = cursor.position()
1040 self._insert_html(cursor, html)
1078 self._insert_html(cursor, html)
1041 end = cursor.position()
1079 end = cursor.position()
1042 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1080 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1043 text = str(cursor.selection().toPlainText())
1081 text = str(cursor.selection().toPlainText())
1044
1082
1045 cursor.setPosition(end)
1083 cursor.setPosition(end)
1046 cursor.endEditBlock()
1084 cursor.endEditBlock()
1047 return text
1085 return text
1048
1086
1049 def _insert_plain_text(self, cursor, text):
1087 def _insert_plain_text(self, cursor, text):
1050 """ Inserts plain text using the specified cursor, processing ANSI codes
1088 """ Inserts plain text using the specified cursor, processing ANSI codes
1051 if enabled.
1089 if enabled.
1052 """
1090 """
1053 cursor.beginEditBlock()
1091 cursor.beginEditBlock()
1054 if self.ansi_codes:
1092 if self.ansi_codes:
1055 for substring in self._ansi_processor.split_string(text):
1093 for substring in self._ansi_processor.split_string(text):
1056 for action in self._ansi_processor.actions:
1094 for action in self._ansi_processor.actions:
1057 if action.kind == 'erase' and action.area == 'screen':
1095 if action.kind == 'erase' and action.area == 'screen':
1058 cursor.select(QtGui.QTextCursor.Document)
1096 cursor.select(QtGui.QTextCursor.Document)
1059 cursor.removeSelectedText()
1097 cursor.removeSelectedText()
1060 format = self._ansi_processor.get_format()
1098 format = self._ansi_processor.get_format()
1061 cursor.insertText(substring, format)
1099 cursor.insertText(substring, format)
1062 else:
1100 else:
1063 cursor.insertText(text)
1101 cursor.insertText(text)
1064 cursor.endEditBlock()
1102 cursor.endEditBlock()
1065
1103
1066 def _insert_plain_text_into_buffer(self, text):
1104 def _insert_plain_text_into_buffer(self, text):
1067 """ Inserts text into the input buffer at the current cursor position,
1105 """ Inserts text into the input buffer at the current cursor position,
1068 ensuring that continuation prompts are inserted as necessary.
1106 ensuring that continuation prompts are inserted as necessary.
1069 """
1107 """
1070 lines = str(text).splitlines(True)
1108 lines = str(text).splitlines(True)
1071 if lines:
1109 if lines:
1072 self._keep_cursor_in_buffer()
1110 self._keep_cursor_in_buffer()
1073 cursor = self._control.textCursor()
1111 cursor = self._control.textCursor()
1074 cursor.beginEditBlock()
1112 cursor.beginEditBlock()
1075 cursor.insertText(lines[0])
1113 cursor.insertText(lines[0])
1076 for line in lines[1:]:
1114 for line in lines[1:]:
1077 if self._continuation_prompt_html is None:
1115 if self._continuation_prompt_html is None:
1078 cursor.insertText(self._continuation_prompt)
1116 cursor.insertText(self._continuation_prompt)
1079 else:
1117 else:
1080 self._continuation_prompt = \
1118 self._continuation_prompt = \
1081 self._insert_html_fetching_plain_text(
1119 self._insert_html_fetching_plain_text(
1082 cursor, self._continuation_prompt_html)
1120 cursor, self._continuation_prompt_html)
1083 cursor.insertText(line)
1121 cursor.insertText(line)
1084 cursor.endEditBlock()
1122 cursor.endEditBlock()
1085 self._control.setTextCursor(cursor)
1123 self._control.setTextCursor(cursor)
1086
1124
1087 def _in_buffer(self, position=None):
1125 def _in_buffer(self, position=None):
1088 """ Returns whether the current cursor (or, if specified, a position) is
1126 """ Returns whether the current cursor (or, if specified, a position) is
1089 inside the editing region.
1127 inside the editing region.
1090 """
1128 """
1091 cursor = self._control.textCursor()
1129 cursor = self._control.textCursor()
1092 if position is None:
1130 if position is None:
1093 position = cursor.position()
1131 position = cursor.position()
1094 else:
1132 else:
1095 cursor.setPosition(position)
1133 cursor.setPosition(position)
1096 line = cursor.blockNumber()
1134 line = cursor.blockNumber()
1097 prompt_line = self._get_prompt_cursor().blockNumber()
1135 prompt_line = self._get_prompt_cursor().blockNumber()
1098 if line == prompt_line:
1136 if line == prompt_line:
1099 return position >= self._prompt_pos
1137 return position >= self._prompt_pos
1100 elif line > prompt_line:
1138 elif line > prompt_line:
1101 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1139 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1102 prompt_pos = cursor.position() + len(self._continuation_prompt)
1140 prompt_pos = cursor.position() + len(self._continuation_prompt)
1103 return position >= prompt_pos
1141 return position >= prompt_pos
1104 return False
1142 return False
1105
1143
1106 def _keep_cursor_in_buffer(self):
1144 def _keep_cursor_in_buffer(self):
1107 """ Ensures that the cursor is inside the editing region. Returns
1145 """ Ensures that the cursor is inside the editing region. Returns
1108 whether the cursor was moved.
1146 whether the cursor was moved.
1109 """
1147 """
1110 moved = not self._in_buffer()
1148 moved = not self._in_buffer()
1111 if moved:
1149 if moved:
1112 cursor = self._control.textCursor()
1150 cursor = self._control.textCursor()
1113 cursor.movePosition(QtGui.QTextCursor.End)
1151 cursor.movePosition(QtGui.QTextCursor.End)
1114 self._control.setTextCursor(cursor)
1152 self._control.setTextCursor(cursor)
1115 return moved
1153 return moved
1116
1154
1117 def _page(self, text):
1155 def _page(self, text):
1118 """ Displays text using the pager if it exceeds the height of the
1156 """ Displays text using the pager if it exceeds the height of the
1119 visible area.
1157 visible area.
1120 """
1158 """
1121 if self.paging == 'none':
1159 if self.paging == 'none':
1122 self._append_plain_text(text)
1160 self._append_plain_text(text)
1123 else:
1161 else:
1124 line_height = QtGui.QFontMetrics(self.font).height()
1162 line_height = QtGui.QFontMetrics(self.font).height()
1125 minlines = self._control.viewport().height() / line_height
1163 minlines = self._control.viewport().height() / line_height
1126 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1164 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1127 if self.paging == 'custom':
1165 if self.paging == 'custom':
1128 self.custom_page_requested.emit(text)
1166 self.custom_page_requested.emit(text)
1129 else:
1167 else:
1130 self._page_control.clear()
1168 self._page_control.clear()
1131 cursor = self._page_control.textCursor()
1169 cursor = self._page_control.textCursor()
1132 self._insert_plain_text(cursor, text)
1170 self._insert_plain_text(cursor, text)
1133 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1171 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1134
1172
1135 self._page_control.viewport().resize(self._control.size())
1173 self._page_control.viewport().resize(self._control.size())
1136 if self._splitter:
1174 if self._splitter:
1137 self._page_control.show()
1175 self._page_control.show()
1138 self._page_control.setFocus()
1176 self._page_control.setFocus()
1139 else:
1177 else:
1140 self.layout().setCurrentWidget(self._page_control)
1178 self.layout().setCurrentWidget(self._page_control)
1141 else:
1179 else:
1142 self._append_plain_text(text)
1180 self._append_plain_text(text)
1143
1181
1144 def _prompt_started(self):
1182 def _prompt_started(self):
1145 """ Called immediately after a new prompt is displayed.
1183 """ Called immediately after a new prompt is displayed.
1146 """
1184 """
1147 # Temporarily disable the maximum block count to permit undo/redo and
1185 # Temporarily disable the maximum block count to permit undo/redo and
1148 # to ensure that the prompt position does not change due to truncation.
1186 # to ensure that the prompt position does not change due to truncation.
1149 # Because setting this property clears the undo/redo history, we only
1187 # Because setting this property clears the undo/redo history, we only
1150 # set it if we have to.
1188 # set it if we have to.
1151 if self._control.document().maximumBlockCount() > 0:
1189 if self._control.document().maximumBlockCount() > 0:
1152 self._control.document().setMaximumBlockCount(0)
1190 self._control.document().setMaximumBlockCount(0)
1153 self._control.setUndoRedoEnabled(True)
1191 self._control.setUndoRedoEnabled(True)
1154
1192
1155 self._control.setReadOnly(False)
1193 self._control.setReadOnly(False)
1156 self._control.moveCursor(QtGui.QTextCursor.End)
1194 self._control.moveCursor(QtGui.QTextCursor.End)
1157
1195
1158 self._executing = False
1196 self._executing = False
1159 self._prompt_started_hook()
1197 self._prompt_started_hook()
1160
1198
1161 def _prompt_finished(self):
1199 def _prompt_finished(self):
1162 """ Called immediately after a prompt is finished, i.e. when some input
1200 """ Called immediately after a prompt is finished, i.e. when some input
1163 will be processed and a new prompt displayed.
1201 will be processed and a new prompt displayed.
1164 """
1202 """
1165 self._control.setReadOnly(True)
1203 self._control.setReadOnly(True)
1166 self._prompt_finished_hook()
1204 self._prompt_finished_hook()
1167
1205
1168 def _readline(self, prompt='', callback=None):
1206 def _readline(self, prompt='', callback=None):
1169 """ Reads one line of input from the user.
1207 """ Reads one line of input from the user.
1170
1208
1171 Parameters
1209 Parameters
1172 ----------
1210 ----------
1173 prompt : str, optional
1211 prompt : str, optional
1174 The prompt to print before reading the line.
1212 The prompt to print before reading the line.
1175
1213
1176 callback : callable, optional
1214 callback : callable, optional
1177 A callback to execute with the read line. If not specified, input is
1215 A callback to execute with the read line. If not specified, input is
1178 read *synchronously* and this method does not return until it has
1216 read *synchronously* and this method does not return until it has
1179 been read.
1217 been read.
1180
1218
1181 Returns
1219 Returns
1182 -------
1220 -------
1183 If a callback is specified, returns nothing. Otherwise, returns the
1221 If a callback is specified, returns nothing. Otherwise, returns the
1184 input string with the trailing newline stripped.
1222 input string with the trailing newline stripped.
1185 """
1223 """
1186 if self._reading:
1224 if self._reading:
1187 raise RuntimeError('Cannot read a line. Widget is already reading.')
1225 raise RuntimeError('Cannot read a line. Widget is already reading.')
1188
1226
1189 if not callback and not self.isVisible():
1227 if not callback and not self.isVisible():
1190 # If the user cannot see the widget, this function cannot return.
1228 # If the user cannot see the widget, this function cannot return.
1191 raise RuntimeError('Cannot synchronously read a line if the widget '
1229 raise RuntimeError('Cannot synchronously read a line if the widget '
1192 'is not visible!')
1230 'is not visible!')
1193
1231
1194 self._reading = True
1232 self._reading = True
1195 self._show_prompt(prompt, newline=False)
1233 self._show_prompt(prompt, newline=False)
1196
1234
1197 if callback is None:
1235 if callback is None:
1198 self._reading_callback = None
1236 self._reading_callback = None
1199 while self._reading:
1237 while self._reading:
1200 QtCore.QCoreApplication.processEvents()
1238 QtCore.QCoreApplication.processEvents()
1201 return self.input_buffer.rstrip('\n')
1239 return self.input_buffer.rstrip('\n')
1202
1240
1203 else:
1241 else:
1204 self._reading_callback = lambda: \
1242 self._reading_callback = lambda: \
1205 callback(self.input_buffer.rstrip('\n'))
1243 callback(self.input_buffer.rstrip('\n'))
1206
1244
1207 def _set_continuation_prompt(self, prompt, html=False):
1245 def _set_continuation_prompt(self, prompt, html=False):
1208 """ Sets the continuation prompt.
1246 """ Sets the continuation prompt.
1209
1247
1210 Parameters
1248 Parameters
1211 ----------
1249 ----------
1212 prompt : str
1250 prompt : str
1213 The prompt to show when more input is needed.
1251 The prompt to show when more input is needed.
1214
1252
1215 html : bool, optional (default False)
1253 html : bool, optional (default False)
1216 If set, the prompt will be inserted as formatted HTML. Otherwise,
1254 If set, the prompt will be inserted as formatted HTML. Otherwise,
1217 the prompt will be treated as plain text, though ANSI color codes
1255 the prompt will be treated as plain text, though ANSI color codes
1218 will be handled.
1256 will be handled.
1219 """
1257 """
1220 if html:
1258 if html:
1221 self._continuation_prompt_html = prompt
1259 self._continuation_prompt_html = prompt
1222 else:
1260 else:
1223 self._continuation_prompt = prompt
1261 self._continuation_prompt = prompt
1224 self._continuation_prompt_html = None
1262 self._continuation_prompt_html = None
1225
1263
1226 def _set_cursor(self, cursor):
1264 def _set_cursor(self, cursor):
1227 """ Convenience method to set the current cursor.
1265 """ Convenience method to set the current cursor.
1228 """
1266 """
1229 self._control.setTextCursor(cursor)
1267 self._control.setTextCursor(cursor)
1230
1268
1231 def _show_context_menu(self, pos):
1269 def _show_context_menu(self, pos):
1232 """ Shows a context menu at the given QPoint (in widget coordinates).
1270 """ Shows a context menu at the given QPoint (in widget coordinates).
1233 """
1271 """
1234 menu = QtGui.QMenu()
1272 menu = QtGui.QMenu()
1235
1273
1236 copy_action = menu.addAction('Copy', self.copy)
1274 copy_action = menu.addAction('Copy', self.copy)
1237 copy_action.setEnabled(self._get_cursor().hasSelection())
1275 copy_action.setEnabled(self._get_cursor().hasSelection())
1238 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1276 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1239
1277
1240 paste_action = menu.addAction('Paste', self.paste)
1278 paste_action = menu.addAction('Paste', self.paste)
1241 paste_action.setEnabled(self.can_paste())
1279 paste_action.setEnabled(self.can_paste())
1242 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1280 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1243
1281
1244 menu.addSeparator()
1282 menu.addSeparator()
1245 menu.addAction('Select All', self.select_all)
1283 menu.addAction('Select All', self.select_all)
1246
1284
1247 menu.exec_(self._control.mapToGlobal(pos))
1285 menu.exec_(self._control.mapToGlobal(pos))
1248
1286
1249 def _show_prompt(self, prompt=None, html=False, newline=True):
1287 def _show_prompt(self, prompt=None, html=False, newline=True):
1250 """ Writes a new prompt at the end of the buffer.
1288 """ Writes a new prompt at the end of the buffer.
1251
1289
1252 Parameters
1290 Parameters
1253 ----------
1291 ----------
1254 prompt : str, optional
1292 prompt : str, optional
1255 The prompt to show. If not specified, the previous prompt is used.
1293 The prompt to show. If not specified, the previous prompt is used.
1256
1294
1257 html : bool, optional (default False)
1295 html : bool, optional (default False)
1258 Only relevant when a prompt is specified. If set, the prompt will
1296 Only relevant when a prompt is specified. If set, the prompt will
1259 be inserted as formatted HTML. Otherwise, the prompt will be treated
1297 be inserted as formatted HTML. Otherwise, the prompt will be treated
1260 as plain text, though ANSI color codes will be handled.
1298 as plain text, though ANSI color codes will be handled.
1261
1299
1262 newline : bool, optional (default True)
1300 newline : bool, optional (default True)
1263 If set, a new line will be written before showing the prompt if
1301 If set, a new line will be written before showing the prompt if
1264 there is not already a newline at the end of the buffer.
1302 there is not already a newline at the end of the buffer.
1265 """
1303 """
1266 # Insert a preliminary newline, if necessary.
1304 # Insert a preliminary newline, if necessary.
1267 if newline:
1305 if newline:
1268 cursor = self._get_end_cursor()
1306 cursor = self._get_end_cursor()
1269 if cursor.position() > 0:
1307 if cursor.position() > 0:
1270 cursor.movePosition(QtGui.QTextCursor.Left,
1308 cursor.movePosition(QtGui.QTextCursor.Left,
1271 QtGui.QTextCursor.KeepAnchor)
1309 QtGui.QTextCursor.KeepAnchor)
1272 if str(cursor.selection().toPlainText()) != '\n':
1310 if str(cursor.selection().toPlainText()) != '\n':
1273 self._append_plain_text('\n')
1311 self._append_plain_text('\n')
1274
1312
1275 # Write the prompt.
1313 # Write the prompt.
1276 self._append_plain_text(self._prompt_sep)
1314 self._append_plain_text(self._prompt_sep)
1277 if prompt is None:
1315 if prompt is None:
1278 if self._prompt_html is None:
1316 if self._prompt_html is None:
1279 self._append_plain_text(self._prompt)
1317 self._append_plain_text(self._prompt)
1280 else:
1318 else:
1281 self._append_html(self._prompt_html)
1319 self._append_html(self._prompt_html)
1282 else:
1320 else:
1283 if html:
1321 if html:
1284 self._prompt = self._append_html_fetching_plain_text(prompt)
1322 self._prompt = self._append_html_fetching_plain_text(prompt)
1285 self._prompt_html = prompt
1323 self._prompt_html = prompt
1286 else:
1324 else:
1287 self._append_plain_text(prompt)
1325 self._append_plain_text(prompt)
1288 self._prompt = prompt
1326 self._prompt = prompt
1289 self._prompt_html = None
1327 self._prompt_html = None
1290
1328
1291 self._prompt_pos = self._get_end_cursor().position()
1329 self._prompt_pos = self._get_end_cursor().position()
1292 self._prompt_started()
1330 self._prompt_started()
1293
1331
1294
1332
1295 class HistoryConsoleWidget(ConsoleWidget):
1333 class HistoryConsoleWidget(ConsoleWidget):
1296 """ A ConsoleWidget that keeps a history of the commands that have been
1334 """ A ConsoleWidget that keeps a history of the commands that have been
1297 executed.
1335 executed.
1298 """
1336 """
1299
1337
1300 #---------------------------------------------------------------------------
1338 #---------------------------------------------------------------------------
1301 # 'object' interface
1339 # 'object' interface
1302 #---------------------------------------------------------------------------
1340 #---------------------------------------------------------------------------
1303
1341
1304 def __init__(self, *args, **kw):
1342 def __init__(self, *args, **kw):
1305 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1343 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1306 self._history = []
1344 self._history = []
1307 self._history_index = 0
1345 self._history_index = 0
1308
1346
1309 #---------------------------------------------------------------------------
1347 #---------------------------------------------------------------------------
1310 # 'ConsoleWidget' public interface
1348 # 'ConsoleWidget' public interface
1311 #---------------------------------------------------------------------------
1349 #---------------------------------------------------------------------------
1312
1350
1313 def execute(self, source=None, hidden=False, interactive=False):
1351 def execute(self, source=None, hidden=False, interactive=False):
1314 """ Reimplemented to the store history.
1352 """ Reimplemented to the store history.
1315 """
1353 """
1316 if not hidden:
1354 if not hidden:
1317 history = self.input_buffer if source is None else source
1355 history = self.input_buffer if source is None else source
1318
1356
1319 executed = super(HistoryConsoleWidget, self).execute(
1357 executed = super(HistoryConsoleWidget, self).execute(
1320 source, hidden, interactive)
1358 source, hidden, interactive)
1321
1359
1322 if executed and not hidden:
1360 if executed and not hidden:
1323 # Save the command unless it was a blank line.
1361 # Save the command unless it was a blank line.
1324 history = history.rstrip()
1362 history = history.rstrip()
1325 if history:
1363 if history:
1326 self._history.append(history)
1364 self._history.append(history)
1327 self._history_index = len(self._history)
1365 self._history_index = len(self._history)
1328
1366
1329 return executed
1367 return executed
1330
1368
1331 #---------------------------------------------------------------------------
1369 #---------------------------------------------------------------------------
1332 # 'ConsoleWidget' abstract interface
1370 # 'ConsoleWidget' abstract interface
1333 #---------------------------------------------------------------------------
1371 #---------------------------------------------------------------------------
1334
1372
1335 def _up_pressed(self):
1373 def _up_pressed(self):
1336 """ Called when the up key is pressed. Returns whether to continue
1374 """ Called when the up key is pressed. Returns whether to continue
1337 processing the event.
1375 processing the event.
1338 """
1376 """
1339 prompt_cursor = self._get_prompt_cursor()
1377 prompt_cursor = self._get_prompt_cursor()
1340 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1378 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1341 self.history_previous()
1379 self.history_previous()
1342
1380
1343 # Go to the first line of prompt for seemless history scrolling.
1381 # Go to the first line of prompt for seemless history scrolling.
1344 cursor = self._get_prompt_cursor()
1382 cursor = self._get_prompt_cursor()
1345 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1383 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1346 self._set_cursor(cursor)
1384 self._set_cursor(cursor)
1347
1385
1348 return False
1386 return False
1349 return True
1387 return True
1350
1388
1351 def _down_pressed(self):
1389 def _down_pressed(self):
1352 """ Called when the down key is pressed. Returns whether to continue
1390 """ Called when the down key is pressed. Returns whether to continue
1353 processing the event.
1391 processing the event.
1354 """
1392 """
1355 end_cursor = self._get_end_cursor()
1393 end_cursor = self._get_end_cursor()
1356 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1394 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1357 self.history_next()
1395 self.history_next()
1358 return False
1396 return False
1359 return True
1397 return True
1360
1398
1361 #---------------------------------------------------------------------------
1399 #---------------------------------------------------------------------------
1362 # 'HistoryConsoleWidget' public interface
1400 # 'HistoryConsoleWidget' public interface
1363 #---------------------------------------------------------------------------
1401 #---------------------------------------------------------------------------
1364
1402
1365 def history_previous(self):
1403 def history_previous(self):
1366 """ If possible, set the input buffer to the previous item in the
1404 """ If possible, set the input buffer to the previous item in the
1367 history.
1405 history.
1368 """
1406 """
1369 if self._history_index > 0:
1407 if self._history_index > 0:
1370 self._history_index -= 1
1408 self._history_index -= 1
1371 self.input_buffer = self._history[self._history_index]
1409 self.input_buffer = self._history[self._history_index]
1372
1410
1373 def history_next(self):
1411 def history_next(self):
1374 """ Set the input buffer to the next item in the history, or a blank
1412 """ Set the input buffer to the next item in the history, or a blank
1375 line if there is no subsequent item.
1413 line if there is no subsequent item.
1376 """
1414 """
1377 if self._history_index < len(self._history):
1415 if self._history_index < len(self._history):
1378 self._history_index += 1
1416 self._history_index += 1
1379 if self._history_index < len(self._history):
1417 if self._history_index < len(self._history):
1380 self.input_buffer = self._history[self._history_index]
1418 self.input_buffer = self._history[self._history_index]
1381 else:
1419 else:
1382 self.input_buffer = ''
1420 self.input_buffer = ''
1383
1421
1384 #---------------------------------------------------------------------------
1422 #---------------------------------------------------------------------------
1385 # 'HistoryConsoleWidget' protected interface
1423 # 'HistoryConsoleWidget' protected interface
1386 #---------------------------------------------------------------------------
1424 #---------------------------------------------------------------------------
1387
1425
1388 def _set_history(self, history):
1426 def _set_history(self, history):
1389 """ Replace the current history with a sequence of history items.
1427 """ Replace the current history with a sequence of history items.
1390 """
1428 """
1391 self._history = list(history)
1429 self._history = list(history)
1392 self._history_index = len(self._history)
1430 self._history_index = len(self._history)
@@ -1,420 +1,425 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
8
9 # Local imports
9 # Local imports
10 from IPython.core.inputsplitter import InputSplitter
10 from IPython.core.inputsplitter import InputSplitter
11 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
11 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 from IPython.utils.traitlets import Bool
12 from IPython.utils.traitlets import Bool
13 from bracket_matcher import BracketMatcher
13 from bracket_matcher import BracketMatcher
14 from call_tip_widget import CallTipWidget
14 from call_tip_widget import CallTipWidget
15 from completion_lexer import CompletionLexer
15 from completion_lexer import CompletionLexer
16 from console_widget import HistoryConsoleWidget
16 from console_widget import HistoryConsoleWidget
17 from pygments_highlighter import PygmentsHighlighter
17 from pygments_highlighter import PygmentsHighlighter
18
18
19
19
20 class FrontendHighlighter(PygmentsHighlighter):
20 class FrontendHighlighter(PygmentsHighlighter):
21 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 """ A PygmentsHighlighter that can be turned on and off and that ignores
22 prompts.
22 prompts.
23 """
23 """
24
24
25 def __init__(self, frontend):
25 def __init__(self, frontend):
26 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 super(FrontendHighlighter, self).__init__(frontend._control.document())
27 self._current_offset = 0
27 self._current_offset = 0
28 self._frontend = frontend
28 self._frontend = frontend
29 self.highlighting_on = False
29 self.highlighting_on = False
30
30
31 def highlightBlock(self, qstring):
31 def highlightBlock(self, qstring):
32 """ Highlight a block of text. Reimplemented to highlight selectively.
32 """ Highlight a block of text. Reimplemented to highlight selectively.
33 """
33 """
34 if not self.highlighting_on:
34 if not self.highlighting_on:
35 return
35 return
36
36
37 # The input to this function is unicode string that may contain
37 # The input to this function is unicode string that may contain
38 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 # paragraph break characters, non-breaking spaces, etc. Here we acquire
39 # the string as plain text so we can compare it.
39 # the string as plain text so we can compare it.
40 current_block = self.currentBlock()
40 current_block = self.currentBlock()
41 string = self._frontend._get_block_plain_text(current_block)
41 string = self._frontend._get_block_plain_text(current_block)
42
42
43 # Decide whether to check for the regular or continuation prompt.
43 # Decide whether to check for the regular or continuation prompt.
44 if current_block.contains(self._frontend._prompt_pos):
44 if current_block.contains(self._frontend._prompt_pos):
45 prompt = self._frontend._prompt
45 prompt = self._frontend._prompt
46 else:
46 else:
47 prompt = self._frontend._continuation_prompt
47 prompt = self._frontend._continuation_prompt
48
48
49 # Don't highlight the part of the string that contains the prompt.
49 # Don't highlight the part of the string that contains the prompt.
50 if string.startswith(prompt):
50 if string.startswith(prompt):
51 self._current_offset = len(prompt)
51 self._current_offset = len(prompt)
52 qstring.remove(0, len(prompt))
52 qstring.remove(0, len(prompt))
53 else:
53 else:
54 self._current_offset = 0
54 self._current_offset = 0
55
55
56 PygmentsHighlighter.highlightBlock(self, qstring)
56 PygmentsHighlighter.highlightBlock(self, qstring)
57
57
58 def rehighlightBlock(self, block):
58 def rehighlightBlock(self, block):
59 """ Reimplemented to temporarily enable highlighting if disabled.
59 """ Reimplemented to temporarily enable highlighting if disabled.
60 """
60 """
61 old = self.highlighting_on
61 old = self.highlighting_on
62 self.highlighting_on = True
62 self.highlighting_on = True
63 super(FrontendHighlighter, self).rehighlightBlock(block)
63 super(FrontendHighlighter, self).rehighlightBlock(block)
64 self.highlighting_on = old
64 self.highlighting_on = old
65
65
66 def setFormat(self, start, count, format):
66 def setFormat(self, start, count, format):
67 """ Reimplemented to highlight selectively.
67 """ Reimplemented to highlight selectively.
68 """
68 """
69 start += self._current_offset
69 start += self._current_offset
70 PygmentsHighlighter.setFormat(self, start, count, format)
70 PygmentsHighlighter.setFormat(self, start, count, format)
71
71
72
72
73 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
73 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
74 """ A Qt frontend for a generic Python kernel.
74 """ A Qt frontend for a generic Python kernel.
75 """
75 """
76
76
77 # An option and corresponding signal for overriding the default kernel
77 # An option and corresponding signal for overriding the default kernel
78 # interrupt behavior.
78 # interrupt behavior.
79 custom_interrupt = Bool(False)
79 custom_interrupt = Bool(False)
80 custom_interrupt_requested = QtCore.pyqtSignal()
80 custom_interrupt_requested = QtCore.pyqtSignal()
81
81
82 # An option and corresponding signal for overriding the default kernel
82 # An option and corresponding signal for overriding the default kernel
83 # restart behavior.
83 # restart behavior.
84 custom_restart = Bool(False)
84 custom_restart = Bool(False)
85 custom_restart_requested = QtCore.pyqtSignal()
85 custom_restart_requested = QtCore.pyqtSignal()
86
86
87 # Emitted when an 'execute_reply' has been received from the kernel and
87 # Emitted when an 'execute_reply' has been received from the kernel and
88 # processed by the FrontendWidget.
88 # processed by the FrontendWidget.
89 executed = QtCore.pyqtSignal(object)
89 executed = QtCore.pyqtSignal(object)
90
90
91 # Protected class variables.
91 # Protected class variables.
92 _input_splitter_class = InputSplitter
92 _input_splitter_class = InputSplitter
93
93
94 #---------------------------------------------------------------------------
94 #---------------------------------------------------------------------------
95 # 'object' interface
95 # 'object' interface
96 #---------------------------------------------------------------------------
96 #---------------------------------------------------------------------------
97
97
98 def __init__(self, *args, **kw):
98 def __init__(self, *args, **kw):
99 super(FrontendWidget, self).__init__(*args, **kw)
99 super(FrontendWidget, self).__init__(*args, **kw)
100
100
101 # FrontendWidget protected variables.
101 # FrontendWidget protected variables.
102 self._bracket_matcher = BracketMatcher(self._control)
102 self._bracket_matcher = BracketMatcher(self._control)
103 self._call_tip_widget = CallTipWidget(self._control)
103 self._call_tip_widget = CallTipWidget(self._control)
104 self._completion_lexer = CompletionLexer(PythonLexer())
104 self._completion_lexer = CompletionLexer(PythonLexer())
105 self._hidden = False
105 self._hidden = False
106 self._highlighter = FrontendHighlighter(self)
106 self._highlighter = FrontendHighlighter(self)
107 self._input_splitter = self._input_splitter_class(input_mode='block')
107 self._input_splitter = self._input_splitter_class(input_mode='block')
108 self._kernel_manager = None
108 self._kernel_manager = None
109
109
110 # Configure the ConsoleWidget.
110 # Configure the ConsoleWidget.
111 self.tab_width = 4
111 self.tab_width = 4
112 self._set_continuation_prompt('... ')
112 self._set_continuation_prompt('... ')
113
113
114 # Connect signal handlers.
114 # Connect signal handlers.
115 document = self._control.document()
115 document = self._control.document()
116 document.contentsChange.connect(self._document_contents_change)
116 document.contentsChange.connect(self._document_contents_change)
117
117
118 #---------------------------------------------------------------------------
118 #---------------------------------------------------------------------------
119 # 'ConsoleWidget' abstract interface
119 # 'ConsoleWidget' abstract interface
120 #---------------------------------------------------------------------------
120 #---------------------------------------------------------------------------
121
121
122 def _is_complete(self, source, interactive):
122 def _is_complete(self, source, interactive):
123 """ Returns whether 'source' can be completely processed and a new
123 """ Returns whether 'source' can be completely processed and a new
124 prompt created. When triggered by an Enter/Return key press,
124 prompt created. When triggered by an Enter/Return key press,
125 'interactive' is True; otherwise, it is False.
125 'interactive' is True; otherwise, it is False.
126 """
126 """
127 complete = self._input_splitter.push(source.expandtabs(4))
127 complete = self._input_splitter.push(source.expandtabs(4))
128 if interactive:
128 if interactive:
129 complete = not self._input_splitter.push_accepts_more()
129 complete = not self._input_splitter.push_accepts_more()
130 return complete
130 return complete
131
131
132 def _execute(self, source, hidden):
132 def _execute(self, source, hidden):
133 """ Execute 'source'. If 'hidden', do not show any output.
133 """ Execute 'source'. If 'hidden', do not show any output.
134 """
134 """
135 self.kernel_manager.xreq_channel.execute(source, hidden)
135 self.kernel_manager.xreq_channel.execute(source, hidden)
136 self._hidden = hidden
136 self._hidden = hidden
137
137
138 def _prompt_started_hook(self):
138 def _prompt_started_hook(self):
139 """ Called immediately after a new prompt is displayed.
139 """ Called immediately after a new prompt is displayed.
140 """
140 """
141 if not self._reading:
141 if not self._reading:
142 self._highlighter.highlighting_on = True
142 self._highlighter.highlighting_on = True
143
143
144 def _prompt_finished_hook(self):
144 def _prompt_finished_hook(self):
145 """ Called immediately after a prompt is finished, i.e. when some input
145 """ Called immediately after a prompt is finished, i.e. when some input
146 will be processed and a new prompt displayed.
146 will be processed and a new prompt displayed.
147 """
147 """
148 if not self._reading:
148 if not self._reading:
149 self._highlighter.highlighting_on = False
149 self._highlighter.highlighting_on = False
150
150
151 def _tab_pressed(self):
151 def _tab_pressed(self):
152 """ Called when the tab key is pressed. Returns whether to continue
152 """ Called when the tab key is pressed. Returns whether to continue
153 processing the event.
153 processing the event.
154 """
154 """
155 # Perform tab completion if:
155 # Perform tab completion if:
156 # 1) The cursor is in the input buffer.
156 # 1) The cursor is in the input buffer.
157 # 2) There is a non-whitespace character before the cursor.
157 # 2) There is a non-whitespace character before the cursor.
158 text = self._get_input_buffer_cursor_line()
158 text = self._get_input_buffer_cursor_line()
159 if text is None:
159 if text is None:
160 return False
160 return False
161 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
161 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
162 if complete:
162 if complete:
163 self._complete()
163 self._complete()
164 return not complete
164 return not complete
165
165
166 #---------------------------------------------------------------------------
166 #---------------------------------------------------------------------------
167 # 'ConsoleWidget' protected interface
167 # 'ConsoleWidget' protected interface
168 #---------------------------------------------------------------------------
168 #---------------------------------------------------------------------------
169
169
170 def _event_filter_console_keypress(self, event):
170 def _event_filter_console_keypress(self, event):
171 """ Reimplemented to allow execution interruption.
171 """ Reimplemented to allow execution interruption.
172 """
172 """
173 key = event.key()
173 key = event.key()
174 if self._executing and self._control_key_down(event.modifiers()):
174 if self._control_key_down(event.modifiers()):
175 if key == QtCore.Qt.Key_C:
175 if key == QtCore.Qt.Key_C and self._executing:
176 self._kernel_interrupt()
176 self._kernel_interrupt()
177 return True
177 return True
178 elif key == QtCore.Qt.Key_Period:
178 elif key == QtCore.Qt.Key_Period:
179 self._kernel_restart()
179 self._kernel_restart()
180 return True
180 return True
181 return super(FrontendWidget, self)._event_filter_console_keypress(event)
181 return super(FrontendWidget, self)._event_filter_console_keypress(event)
182
182
183 def _insert_continuation_prompt(self, cursor):
183 def _insert_continuation_prompt(self, cursor):
184 """ Reimplemented for auto-indentation.
184 """ Reimplemented for auto-indentation.
185 """
185 """
186 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
186 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
187 spaces = self._input_splitter.indent_spaces
187 spaces = self._input_splitter.indent_spaces
188 cursor.insertText('\t' * (spaces / self.tab_width))
188 cursor.insertText('\t' * (spaces / self.tab_width))
189 cursor.insertText(' ' * (spaces % self.tab_width))
189 cursor.insertText(' ' * (spaces % self.tab_width))
190
190
191 #---------------------------------------------------------------------------
191 #---------------------------------------------------------------------------
192 # 'BaseFrontendMixin' abstract interface
192 # 'BaseFrontendMixin' abstract interface
193 #---------------------------------------------------------------------------
193 #---------------------------------------------------------------------------
194
194
195 def _handle_complete_reply(self, rep):
195 def _handle_complete_reply(self, rep):
196 """ Handle replies for tab completion.
196 """ Handle replies for tab completion.
197 """
197 """
198 cursor = self._get_cursor()
198 cursor = self._get_cursor()
199 if rep['parent_header']['msg_id'] == self._complete_id and \
199 if rep['parent_header']['msg_id'] == self._complete_id and \
200 cursor.position() == self._complete_pos:
200 cursor.position() == self._complete_pos:
201 text = '.'.join(self._get_context())
201 text = '.'.join(self._get_context())
202 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
202 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
203 self._complete_with_items(cursor, rep['content']['matches'])
203 self._complete_with_items(cursor, rep['content']['matches'])
204
204
205 def _handle_execute_reply(self, msg):
205 def _handle_execute_reply(self, msg):
206 """ Handles replies for code execution.
206 """ Handles replies for code execution.
207 """
207 """
208 if not self._hidden:
208 if not self._hidden:
209 # Make sure that all output from the SUB channel has been processed
209 # Make sure that all output from the SUB channel has been processed
210 # before writing a new prompt.
210 # before writing a new prompt.
211 self.kernel_manager.sub_channel.flush()
211 self.kernel_manager.sub_channel.flush()
212
212
213 content = msg['content']
213 content = msg['content']
214 status = content['status']
214 status = content['status']
215 if status == 'ok':
215 if status == 'ok':
216 self._process_execute_ok(msg)
216 self._process_execute_ok(msg)
217 elif status == 'error':
217 elif status == 'error':
218 self._process_execute_error(msg)
218 self._process_execute_error(msg)
219 elif status == 'abort':
219 elif status == 'abort':
220 self._process_execute_abort(msg)
220 self._process_execute_abort(msg)
221
221
222 self._show_interpreter_prompt_for_reply(msg)
222 self._show_interpreter_prompt_for_reply(msg)
223 self.executed.emit(msg)
223 self.executed.emit(msg)
224
224
225 def _handle_input_request(self, msg):
225 def _handle_input_request(self, msg):
226 """ Handle requests for raw_input.
226 """ Handle requests for raw_input.
227 """
227 """
228 if self._hidden:
228 if self._hidden:
229 raise RuntimeError('Request for raw input during hidden execution.')
229 raise RuntimeError('Request for raw input during hidden execution.')
230
230
231 # Make sure that all output from the SUB channel has been processed
231 # Make sure that all output from the SUB channel has been processed
232 # before entering readline mode.
232 # before entering readline mode.
233 self.kernel_manager.sub_channel.flush()
233 self.kernel_manager.sub_channel.flush()
234
234
235 def callback(line):
235 def callback(line):
236 self.kernel_manager.rep_channel.input(line)
236 self.kernel_manager.rep_channel.input(line)
237 self._readline(msg['content']['prompt'], callback=callback)
237 self._readline(msg['content']['prompt'], callback=callback)
238
238
239 def _handle_object_info_reply(self, rep):
239 def _handle_object_info_reply(self, rep):
240 """ Handle replies for call tips.
240 """ Handle replies for call tips.
241 """
241 """
242 cursor = self._get_cursor()
242 cursor = self._get_cursor()
243 if rep['parent_header']['msg_id'] == self._call_tip_id and \
243 if rep['parent_header']['msg_id'] == self._call_tip_id and \
244 cursor.position() == self._call_tip_pos:
244 cursor.position() == self._call_tip_pos:
245 doc = rep['content']['docstring']
245 doc = rep['content']['docstring']
246 if doc:
246 if doc:
247 self._call_tip_widget.show_docstring(doc)
247 self._call_tip_widget.show_docstring(doc)
248
248
249 def _handle_pyout(self, msg):
249 def _handle_pyout(self, msg):
250 """ Handle display hook output.
250 """ Handle display hook output.
251 """
251 """
252 if not self._hidden and self._is_from_this_session(msg):
252 if not self._hidden and self._is_from_this_session(msg):
253 self._append_plain_text(msg['content']['data'] + '\n')
253 self._append_plain_text(msg['content']['data'] + '\n')
254
254
255 def _handle_stream(self, msg):
255 def _handle_stream(self, msg):
256 """ Handle stdout, stderr, and stdin.
256 """ Handle stdout, stderr, and stdin.
257 """
257 """
258 if not self._hidden and self._is_from_this_session(msg):
258 if not self._hidden and self._is_from_this_session(msg):
259 self._append_plain_text(msg['content']['data'])
259 self._append_plain_text(msg['content']['data'])
260 self._control.moveCursor(QtGui.QTextCursor.End)
260 self._control.moveCursor(QtGui.QTextCursor.End)
261
261
262 def _started_channels(self):
262 def _started_channels(self):
263 """ Called when the KernelManager channels have started listening or
263 """ Called when the KernelManager channels have started listening or
264 when the frontend is assigned an already listening KernelManager.
264 when the frontend is assigned an already listening KernelManager.
265 """
265 """
266 self._control.clear()
266 self._control.clear()
267 self._append_plain_text(self._get_banner())
267 self._append_plain_text(self._get_banner())
268 self._show_interpreter_prompt()
268 self._show_interpreter_prompt()
269
269
270 def _stopped_channels(self):
270 def _stopped_channels(self):
271 """ Called when the KernelManager channels have stopped listening or
271 """ Called when the KernelManager channels have stopped listening or
272 when a listening KernelManager is removed from the frontend.
272 when a listening KernelManager is removed from the frontend.
273 """
273 """
274 self._executing = self._reading = False
274 self._executing = self._reading = False
275 self._highlighter.highlighting_on = False
275 self._highlighter.highlighting_on = False
276
276
277 #---------------------------------------------------------------------------
277 #---------------------------------------------------------------------------
278 # 'FrontendWidget' interface
278 # 'FrontendWidget' interface
279 #---------------------------------------------------------------------------
279 #---------------------------------------------------------------------------
280
280
281 def execute_file(self, path, hidden=False):
281 def execute_file(self, path, hidden=False):
282 """ Attempts to execute file with 'path'. If 'hidden', no output is
282 """ Attempts to execute file with 'path'. If 'hidden', no output is
283 shown.
283 shown.
284 """
284 """
285 self.execute('execfile("%s")' % path, hidden=hidden)
285 self.execute('execfile("%s")' % path, hidden=hidden)
286
286
287 #---------------------------------------------------------------------------
287 #---------------------------------------------------------------------------
288 # 'FrontendWidget' protected interface
288 # 'FrontendWidget' protected interface
289 #---------------------------------------------------------------------------
289 #---------------------------------------------------------------------------
290
290
291 def _call_tip(self):
291 def _call_tip(self):
292 """ Shows a call tip, if appropriate, at the current cursor location.
292 """ Shows a call tip, if appropriate, at the current cursor location.
293 """
293 """
294 # Decide if it makes sense to show a call tip
294 # Decide if it makes sense to show a call tip
295 cursor = self._get_cursor()
295 cursor = self._get_cursor()
296 cursor.movePosition(QtGui.QTextCursor.Left)
296 cursor.movePosition(QtGui.QTextCursor.Left)
297 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
297 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
298 return False
298 return False
299 context = self._get_context(cursor)
299 context = self._get_context(cursor)
300 if not context:
300 if not context:
301 return False
301 return False
302
302
303 # Send the metadata request to the kernel
303 # Send the metadata request to the kernel
304 name = '.'.join(context)
304 name = '.'.join(context)
305 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
305 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
306 self._call_tip_pos = self._get_cursor().position()
306 self._call_tip_pos = self._get_cursor().position()
307 return True
307 return True
308
308
309 def _complete(self):
309 def _complete(self):
310 """ Performs completion at the current cursor location.
310 """ Performs completion at the current cursor location.
311 """
311 """
312 context = self._get_context()
312 context = self._get_context()
313 if context:
313 if context:
314 # Send the completion request to the kernel
314 # Send the completion request to the kernel
315 self._complete_id = self.kernel_manager.xreq_channel.complete(
315 self._complete_id = self.kernel_manager.xreq_channel.complete(
316 '.'.join(context), # text
316 '.'.join(context), # text
317 self._get_input_buffer_cursor_line(), # line
317 self._get_input_buffer_cursor_line(), # line
318 self._get_input_buffer_cursor_column(), # cursor_pos
318 self._get_input_buffer_cursor_column(), # cursor_pos
319 self.input_buffer) # block
319 self.input_buffer) # block
320 self._complete_pos = self._get_cursor().position()
320 self._complete_pos = self._get_cursor().position()
321
321
322 def _get_banner(self):
322 def _get_banner(self):
323 """ Gets a banner to display at the beginning of a session.
323 """ Gets a banner to display at the beginning of a session.
324 """
324 """
325 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
325 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
326 '"license" for more information.'
326 '"license" for more information.'
327 return banner % (sys.version, sys.platform)
327 return banner % (sys.version, sys.platform)
328
328
329 def _get_context(self, cursor=None):
329 def _get_context(self, cursor=None):
330 """ Gets the context for the specified cursor (or the current cursor
330 """ Gets the context for the specified cursor (or the current cursor
331 if none is specified).
331 if none is specified).
332 """
332 """
333 if cursor is None:
333 if cursor is None:
334 cursor = self._get_cursor()
334 cursor = self._get_cursor()
335 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
335 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
336 QtGui.QTextCursor.KeepAnchor)
336 QtGui.QTextCursor.KeepAnchor)
337 text = str(cursor.selection().toPlainText())
337 text = str(cursor.selection().toPlainText())
338 return self._completion_lexer.get_context(text)
338 return self._completion_lexer.get_context(text)
339
339
340 def _kernel_interrupt(self):
340 def _kernel_interrupt(self):
341 """ Attempts to interrupt the running kernel.
341 """ Attempts to interrupt the running kernel.
342 """
342 """
343 if self.custom_interrupt:
343 if self.custom_interrupt:
344 self.custom_interrupt_requested.emit()
344 self.custom_interrupt_requested.emit()
345 elif self.kernel_manager.has_kernel:
345 elif self.kernel_manager.has_kernel:
346 self.kernel_manager.signal_kernel(signal.SIGINT)
346 self.kernel_manager.signal_kernel(signal.SIGINT)
347 else:
347 else:
348 self._append_plain_text('Kernel process is either remote or '
348 self._append_plain_text('Kernel process is either remote or '
349 'unspecified. Cannot interrupt.\n')
349 'unspecified. Cannot interrupt.\n')
350
350
351 def _kernel_restart(self):
351 def _kernel_restart(self):
352 """ Attempts to restart the running kernel.
352 """ Attempts to restart the running kernel.
353 """
353 """
354 if self.custom_restart:
354 if self.custom_restart:
355 self.custom_restart_requested.emit()
355 self.custom_restart_requested.emit()
356 elif self.kernel_manager.has_kernel:
356 elif self.kernel_manager.has_kernel:
357 try:
357 message = 'Are you sure you want to restart the kernel?'
358 self.kernel_manager.restart_kernel()
358 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
359 except RuntimeError:
359 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
360 message = 'Kernel started externally. Cannot restart.\n'
360 message, buttons)
361 self._append_plain_text(message)
361 if result == QtGui.QMessageBox.Yes:
362 else:
362 try:
363 self._stopped_channels()
363 self.kernel_manager.restart_kernel()
364 self._append_plain_text('Kernel restarting...\n')
364 except RuntimeError:
365 self._show_interpreter_prompt()
365 message = 'Kernel started externally. Cannot restart.\n'
366 self._append_plain_text(message)
367 else:
368 self._stopped_channels()
369 self._append_plain_text('Kernel restarting...\n')
370 self._show_interpreter_prompt()
366 else:
371 else:
367 self._append_plain_text('Kernel process is either remote or '
372 self._append_plain_text('Kernel process is either remote or '
368 'unspecified. Cannot restart.\n')
373 'unspecified. Cannot restart.\n')
369
374
370 def _process_execute_abort(self, msg):
375 def _process_execute_abort(self, msg):
371 """ Process a reply for an aborted execution request.
376 """ Process a reply for an aborted execution request.
372 """
377 """
373 self._append_plain_text("ERROR: execution aborted\n")
378 self._append_plain_text("ERROR: execution aborted\n")
374
379
375 def _process_execute_error(self, msg):
380 def _process_execute_error(self, msg):
376 """ Process a reply for an execution request that resulted in an error.
381 """ Process a reply for an execution request that resulted in an error.
377 """
382 """
378 content = msg['content']
383 content = msg['content']
379 traceback = ''.join(content['traceback'])
384 traceback = ''.join(content['traceback'])
380 self._append_plain_text(traceback)
385 self._append_plain_text(traceback)
381
386
382 def _process_execute_ok(self, msg):
387 def _process_execute_ok(self, msg):
383 """ Process a reply for a successful execution equest.
388 """ Process a reply for a successful execution equest.
384 """
389 """
385 payload = msg['content']['payload']
390 payload = msg['content']['payload']
386 for item in payload:
391 for item in payload:
387 if not self._process_execute_payload(item):
392 if not self._process_execute_payload(item):
388 warning = 'Received unknown payload of type %s\n'
393 warning = 'Received unknown payload of type %s\n'
389 self._append_plain_text(warning % repr(item['source']))
394 self._append_plain_text(warning % repr(item['source']))
390
395
391 def _process_execute_payload(self, item):
396 def _process_execute_payload(self, item):
392 """ Process a single payload item from the list of payload items in an
397 """ Process a single payload item from the list of payload items in an
393 execution reply. Returns whether the payload was handled.
398 execution reply. Returns whether the payload was handled.
394 """
399 """
395 # The basic FrontendWidget doesn't handle payloads, as they are a
400 # The basic FrontendWidget doesn't handle payloads, as they are a
396 # mechanism for going beyond the standard Python interpreter model.
401 # mechanism for going beyond the standard Python interpreter model.
397 return False
402 return False
398
403
399 def _show_interpreter_prompt(self):
404 def _show_interpreter_prompt(self):
400 """ Shows a prompt for the interpreter.
405 """ Shows a prompt for the interpreter.
401 """
406 """
402 self._show_prompt('>>> ')
407 self._show_prompt('>>> ')
403
408
404 def _show_interpreter_prompt_for_reply(self, msg):
409 def _show_interpreter_prompt_for_reply(self, msg):
405 """ Shows a prompt for the interpreter given an 'execute_reply' message.
410 """ Shows a prompt for the interpreter given an 'execute_reply' message.
406 """
411 """
407 self._show_interpreter_prompt()
412 self._show_interpreter_prompt()
408
413
409 #------ Signal handlers ----------------------------------------------------
414 #------ Signal handlers ----------------------------------------------------
410
415
411 def _document_contents_change(self, position, removed, added):
416 def _document_contents_change(self, position, removed, added):
412 """ Called whenever the document's content changes. Display a call tip
417 """ Called whenever the document's content changes. Display a call tip
413 if appropriate.
418 if appropriate.
414 """
419 """
415 # Calculate where the cursor should be *after* the change:
420 # Calculate where the cursor should be *after* the change:
416 position += added
421 position += added
417
422
418 document = self._control.document()
423 document = self._control.document()
419 if position == self._get_cursor().position():
424 if position == self._get_cursor().position():
420 self._call_tip()
425 self._call_tip()
General Comments 0
You need to be logged in to leave comments. Login now