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