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