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