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