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