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