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