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