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