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