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