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