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