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