##// END OF EJS Templates
Fix to ensure that the paging widget is styled appropriately.
epatters -
Show More
@@ -1,1229 +1,1231 b''
1 # Standard library imports
1 # Standard library imports
2 import sys
2 import sys
3 from textwrap import dedent
3 from textwrap import dedent
4
4
5 # System library imports
5 # System library imports
6 from PyQt4 import QtCore, QtGui
6 from PyQt4 import QtCore, QtGui
7
7
8 # Local imports
8 # Local imports
9 from ansi_code_processor import QtAnsiCodeProcessor
9 from ansi_code_processor import QtAnsiCodeProcessor
10 from completion_widget import CompletionWidget
10 from completion_widget import CompletionWidget
11
11
12
12
13 class ConsoleWidget(QtGui.QWidget):
13 class ConsoleWidget(QtGui.QWidget):
14 """ An abstract base class for console-type widgets. This class has
14 """ An abstract base class for console-type widgets. This class has
15 functionality for:
15 functionality for:
16
16
17 * Maintaining a prompt and editing region
17 * Maintaining a prompt and editing region
18 * Providing the traditional Unix-style console keyboard shortcuts
18 * Providing the traditional Unix-style console keyboard shortcuts
19 * Performing tab completion
19 * Performing tab completion
20 * Paging text
20 * Paging text
21 * Handling ANSI escape codes
21 * Handling ANSI escape codes
22
22
23 ConsoleWidget also provides a number of utility methods that will be
23 ConsoleWidget also provides a number of utility methods that will be
24 convenient to implementors of a console-style widget.
24 convenient to implementors of a console-style widget.
25 """
25 """
26
26
27 # Whether to process ANSI escape codes.
27 # Whether to process ANSI escape codes.
28 ansi_codes = True
28 ansi_codes = True
29
29
30 # The maximum number of lines of text before truncation.
30 # The maximum number of lines of text before truncation.
31 buffer_size = 500
31 buffer_size = 500
32
32
33 # Whether to use a list widget or plain text output for tab completion.
33 # Whether to use a list widget or plain text output for tab completion.
34 gui_completion = True
34 gui_completion = True
35
35
36 # Whether to override ShortcutEvents for the keybindings defined by this
36 # Whether to override ShortcutEvents for the keybindings defined by this
37 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
37 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
38 # priority (when it has focus) over, e.g., window-level menu shortcuts.
38 # priority (when it has focus) over, e.g., window-level menu shortcuts.
39 override_shortcuts = False
39 override_shortcuts = False
40
40
41 # Signals that indicate ConsoleWidget state.
41 # Signals that indicate ConsoleWidget state.
42 copy_available = QtCore.pyqtSignal(bool)
42 copy_available = QtCore.pyqtSignal(bool)
43 redo_available = QtCore.pyqtSignal(bool)
43 redo_available = QtCore.pyqtSignal(bool)
44 undo_available = QtCore.pyqtSignal(bool)
44 undo_available = QtCore.pyqtSignal(bool)
45
45
46 # Signal emitted when paging is needed and the paging style has been
46 # Signal emitted when paging is needed and the paging style has been
47 # specified as 'custom'.
47 # specified as 'custom'.
48 custom_page_requested = QtCore.pyqtSignal(QtCore.QString)
48 custom_page_requested = QtCore.pyqtSignal(QtCore.QString)
49
49
50 # Protected class variables.
50 # Protected class variables.
51 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
51 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
52 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
52 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
53 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
53 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
54 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
54 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
55 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
55 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
56 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
56 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
57 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
57 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
58 _shortcuts = set(_ctrl_down_remap.keys() +
58 _shortcuts = set(_ctrl_down_remap.keys() +
59 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
59 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
60
60
61 #---------------------------------------------------------------------------
61 #---------------------------------------------------------------------------
62 # 'QObject' interface
62 # 'QObject' interface
63 #---------------------------------------------------------------------------
63 #---------------------------------------------------------------------------
64
64
65 def __init__(self, kind='plain', paging='inside', parent=None):
65 def __init__(self, kind='plain', paging='inside', parent=None):
66 """ Create a ConsoleWidget.
66 """ Create a ConsoleWidget.
67
67
68 Parameters
68 Parameters
69 ----------
69 ----------
70 kind : str, optional [default 'plain']
70 kind : str, optional [default 'plain']
71 The type of underlying text widget to use. Valid values are 'plain',
71 The type of underlying text widget to use. Valid values are 'plain',
72 which specifies a QPlainTextEdit, and 'rich', which specifies a
72 which specifies a QPlainTextEdit, and 'rich', which specifies a
73 QTextEdit.
73 QTextEdit.
74
74
75 paging : str, optional [default 'inside']
75 paging : str, optional [default 'inside']
76 The type of paging to use. Valid values are:
76 The type of paging to use. Valid values are:
77 'inside' : The widget pages like a traditional terminal pager.
77 'inside' : The widget pages like a traditional terminal pager.
78 'hsplit' : When paging is requested, the widget is split
78 'hsplit' : When paging is requested, the widget is split
79 horizontally. The top pane contains the console,
79 horizontally. The top pane contains the console,
80 and the bottom pane contains the paged text.
80 and the bottom pane contains the paged text.
81 'vsplit' : Similar to 'hsplit', except that a vertical splitter
81 'vsplit' : Similar to 'hsplit', except that a vertical splitter
82 used.
82 used.
83 'custom' : No action is taken by the widget beyond emitting a
83 'custom' : No action is taken by the widget beyond emitting a
84 'custom_page_requested(QString)' signal.
84 'custom_page_requested(QString)' signal.
85 'none' : The text is written directly to the console.
85 'none' : The text is written directly to the console.
86
86
87 parent : QWidget, optional [default None]
87 parent : QWidget, optional [default None]
88 The parent for this widget.
88 The parent for this widget.
89 """
89 """
90 super(ConsoleWidget, self).__init__(parent)
90 super(ConsoleWidget, self).__init__(parent)
91
91
92 # Create the layout and underlying text widget.
92 # Create the layout and underlying text widget.
93 layout = QtGui.QStackedLayout(self)
93 layout = QtGui.QStackedLayout(self)
94 layout.setMargin(0)
94 layout.setMargin(0)
95 self._control = self._create_control(kind)
95 self._control = self._create_control(kind)
96 self._page_control = None
96 self._page_control = None
97 self._splitter = None
97 self._splitter = None
98 if paging in ('hsplit', 'vsplit'):
98 if paging in ('hsplit', 'vsplit'):
99 self._splitter = QtGui.QSplitter()
99 self._splitter = QtGui.QSplitter()
100 if paging == 'hsplit':
100 if paging == 'hsplit':
101 self._splitter.setOrientation(QtCore.Qt.Horizontal)
101 self._splitter.setOrientation(QtCore.Qt.Horizontal)
102 else:
102 else:
103 self._splitter.setOrientation(QtCore.Qt.Vertical)
103 self._splitter.setOrientation(QtCore.Qt.Vertical)
104 self._splitter.addWidget(self._control)
104 self._splitter.addWidget(self._control)
105 layout.addWidget(self._splitter)
105 layout.addWidget(self._splitter)
106 else:
106 else:
107 layout.addWidget(self._control)
107 layout.addWidget(self._control)
108
108
109 # Create the paging widget, if necessary.
109 # Create the paging widget, if necessary.
110 self._page_style = paging
110 self._page_style = paging
111 if paging in ('inside', 'hsplit', 'vsplit'):
111 if paging in ('inside', 'hsplit', 'vsplit'):
112 self._page_control = self._create_page_control()
112 self._page_control = self._create_page_control()
113 if self._splitter:
113 if self._splitter:
114 self._page_control.hide()
114 self._page_control.hide()
115 self._splitter.addWidget(self._page_control)
115 self._splitter.addWidget(self._page_control)
116 else:
116 else:
117 layout.addWidget(self._page_control)
117 layout.addWidget(self._page_control)
118 elif paging not in ('custom', 'none'):
118 elif paging not in ('custom', 'none'):
119 raise ValueError('Paging style %s unknown.' % repr(paging))
119 raise ValueError('Paging style %s unknown.' % repr(paging))
120
120
121 # Initialize protected variables. Some variables contain useful state
121 # Initialize protected variables. Some variables contain useful state
122 # information for subclasses; they should be considered read-only.
122 # information for subclasses; they should be considered read-only.
123 self._ansi_processor = QtAnsiCodeProcessor()
123 self._ansi_processor = QtAnsiCodeProcessor()
124 self._completion_widget = CompletionWidget(self._control)
124 self._completion_widget = CompletionWidget(self._control)
125 self._continuation_prompt = '> '
125 self._continuation_prompt = '> '
126 self._continuation_prompt_html = None
126 self._continuation_prompt_html = None
127 self._executing = False
127 self._executing = False
128 self._prompt = ''
128 self._prompt = ''
129 self._prompt_html = None
129 self._prompt_html = None
130 self._prompt_pos = 0
130 self._prompt_pos = 0
131 self._reading = False
131 self._reading = False
132 self._reading_callback = None
132 self._reading_callback = None
133 self._tab_width = 8
133 self._tab_width = 8
134
134
135 # Set a monospaced font.
135 # Set a monospaced font.
136 self.reset_font()
136 self.reset_font()
137
137
138 def eventFilter(self, obj, event):
138 def eventFilter(self, obj, event):
139 """ Reimplemented to ensure a console-like behavior in the underlying
139 """ Reimplemented to ensure a console-like behavior in the underlying
140 text widget.
140 text widget.
141 """
141 """
142 # Re-map keys for all filtered widgets.
142 # Re-map keys for all filtered widgets.
143 etype = event.type()
143 etype = event.type()
144 if etype == QtCore.QEvent.KeyPress and \
144 if etype == QtCore.QEvent.KeyPress and \
145 self._control_key_down(event.modifiers()) and \
145 self._control_key_down(event.modifiers()) and \
146 event.key() in self._ctrl_down_remap:
146 event.key() in self._ctrl_down_remap:
147 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
147 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
148 self._ctrl_down_remap[event.key()],
148 self._ctrl_down_remap[event.key()],
149 QtCore.Qt.NoModifier)
149 QtCore.Qt.NoModifier)
150 QtGui.qApp.sendEvent(obj, new_event)
150 QtGui.qApp.sendEvent(obj, new_event)
151 return True
151 return True
152
152
153 # Override shortucts for all filtered widgets. Note that on Mac OS it is
153 # Override shortucts for all filtered widgets. Note that on Mac OS it is
154 # always unnecessary to override shortcuts, hence the check below (users
154 # always unnecessary to override shortcuts, hence the check below (users
155 # should just use the Control key instead of the Command key).
155 # should just use the Control key instead of the Command key).
156 elif etype == QtCore.QEvent.ShortcutOverride and \
156 elif etype == QtCore.QEvent.ShortcutOverride and \
157 sys.platform != 'darwin' and \
157 sys.platform != 'darwin' and \
158 self._control_key_down(event.modifiers()) and \
158 self._control_key_down(event.modifiers()) and \
159 event.key() in self._shortcuts:
159 event.key() in self._shortcuts:
160 event.accept()
160 event.accept()
161 return False
161 return False
162
162
163 elif obj == self._control:
163 elif obj == self._control:
164 # Disable moving text by drag and drop.
164 # Disable moving text by drag and drop.
165 if etype == QtCore.QEvent.DragMove:
165 if etype == QtCore.QEvent.DragMove:
166 return True
166 return True
167
167
168 elif etype == QtCore.QEvent.KeyPress:
168 elif etype == QtCore.QEvent.KeyPress:
169 return self._event_filter_console_keypress(event)
169 return self._event_filter_console_keypress(event)
170
170
171 elif obj == self._page_control:
171 elif obj == self._page_control:
172 if etype == QtCore.QEvent.KeyPress:
172 if etype == QtCore.QEvent.KeyPress:
173 return self._event_filter_page_keypress(event)
173 return self._event_filter_page_keypress(event)
174
174
175 return super(ConsoleWidget, self).eventFilter(obj, event)
175 return super(ConsoleWidget, self).eventFilter(obj, event)
176
176
177 #---------------------------------------------------------------------------
177 #---------------------------------------------------------------------------
178 # 'QWidget' interface
178 # 'QWidget' interface
179 #---------------------------------------------------------------------------
179 #---------------------------------------------------------------------------
180
180
181 def sizeHint(self):
181 def sizeHint(self):
182 """ Reimplemented to suggest a size that is 80 characters wide and
182 """ Reimplemented to suggest a size that is 80 characters wide and
183 25 lines high.
183 25 lines high.
184 """
184 """
185 style = self.style()
185 style = self.style()
186 opt = QtGui.QStyleOptionHeader()
186 opt = QtGui.QStyleOptionHeader()
187 font_metrics = QtGui.QFontMetrics(self.font)
187 font_metrics = QtGui.QFontMetrics(self.font)
188 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
188 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
189
189
190 width = font_metrics.width(' ') * 80
190 width = font_metrics.width(' ') * 80
191 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
191 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
192 if self._page_style == 'hsplit':
192 if self._page_style == 'hsplit':
193 width = width * 2 + splitwidth
193 width = width * 2 + splitwidth
194
194
195 height = font_metrics.height() * 25
195 height = font_metrics.height() * 25
196 if self._page_style == 'vsplit':
196 if self._page_style == 'vsplit':
197 height = height * 2 + splitwidth
197 height = height * 2 + splitwidth
198
198
199 return QtCore.QSize(width, height)
199 return QtCore.QSize(width, height)
200
200
201 #---------------------------------------------------------------------------
201 #---------------------------------------------------------------------------
202 # 'ConsoleWidget' public interface
202 # 'ConsoleWidget' public interface
203 #---------------------------------------------------------------------------
203 #---------------------------------------------------------------------------
204
204
205 def can_paste(self):
205 def can_paste(self):
206 """ Returns whether text can be pasted from the clipboard.
206 """ Returns whether text can be pasted from the clipboard.
207 """
207 """
208 # Accept only text that can be ASCII encoded.
208 # Accept only text that can be ASCII encoded.
209 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
209 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
210 text = QtGui.QApplication.clipboard().text()
210 text = QtGui.QApplication.clipboard().text()
211 if not text.isEmpty():
211 if not text.isEmpty():
212 try:
212 try:
213 str(text)
213 str(text)
214 return True
214 return True
215 except UnicodeEncodeError:
215 except UnicodeEncodeError:
216 pass
216 pass
217 return False
217 return False
218
218
219 def clear(self, keep_input=False):
219 def clear(self, keep_input=False):
220 """ Clear the console, then write a new prompt. If 'keep_input' is set,
220 """ Clear the console, then write a new prompt. If 'keep_input' is set,
221 restores the old input buffer when the new prompt is written.
221 restores the old input buffer when the new prompt is written.
222 """
222 """
223 self._control.clear()
223 self._control.clear()
224 if keep_input:
224 if keep_input:
225 input_buffer = self.input_buffer
225 input_buffer = self.input_buffer
226 self._show_prompt()
226 self._show_prompt()
227 if keep_input:
227 if keep_input:
228 self.input_buffer = input_buffer
228 self.input_buffer = input_buffer
229
229
230 def copy(self):
230 def copy(self):
231 """ Copy the current selected text to the clipboard.
231 """ Copy the current selected text to the clipboard.
232 """
232 """
233 self._control.copy()
233 self._control.copy()
234
234
235 def execute(self, source=None, hidden=False, interactive=False):
235 def execute(self, source=None, hidden=False, interactive=False):
236 """ Executes source or the input buffer, possibly prompting for more
236 """ Executes source or the input buffer, possibly prompting for more
237 input.
237 input.
238
238
239 Parameters:
239 Parameters:
240 -----------
240 -----------
241 source : str, optional
241 source : str, optional
242
242
243 The source to execute. If not specified, the input buffer will be
243 The source to execute. If not specified, the input buffer will be
244 used. If specified and 'hidden' is False, the input buffer will be
244 used. If specified and 'hidden' is False, the input buffer will be
245 replaced with the source before execution.
245 replaced with the source before execution.
246
246
247 hidden : bool, optional (default False)
247 hidden : bool, optional (default False)
248
248
249 If set, no output will be shown and the prompt will not be modified.
249 If set, no output will be shown and the prompt will not be modified.
250 In other words, it will be completely invisible to the user that
250 In other words, it will be completely invisible to the user that
251 an execution has occurred.
251 an execution has occurred.
252
252
253 interactive : bool, optional (default False)
253 interactive : bool, optional (default False)
254
254
255 Whether the console is to treat the source as having been manually
255 Whether the console is to treat the source as having been manually
256 entered by the user. The effect of this parameter depends on the
256 entered by the user. The effect of this parameter depends on the
257 subclass implementation.
257 subclass implementation.
258
258
259 Raises:
259 Raises:
260 -------
260 -------
261 RuntimeError
261 RuntimeError
262 If incomplete input is given and 'hidden' is True. In this case,
262 If incomplete input is given and 'hidden' is True. In this case,
263 it is not possible to prompt for more input.
263 it is not possible to prompt for more input.
264
264
265 Returns:
265 Returns:
266 --------
266 --------
267 A boolean indicating whether the source was executed.
267 A boolean indicating whether the source was executed.
268 """
268 """
269 if not hidden:
269 if not hidden:
270 if source is not None:
270 if source is not None:
271 self.input_buffer = source
271 self.input_buffer = source
272
272
273 self._append_plain_text('\n')
273 self._append_plain_text('\n')
274 self._executing_input_buffer = self.input_buffer
274 self._executing_input_buffer = self.input_buffer
275 self._executing = True
275 self._executing = True
276 self._prompt_finished()
276 self._prompt_finished()
277
277
278 real_source = self.input_buffer if source is None else source
278 real_source = self.input_buffer if source is None else source
279 complete = self._is_complete(real_source, interactive)
279 complete = self._is_complete(real_source, interactive)
280 if complete:
280 if complete:
281 if not hidden:
281 if not hidden:
282 # The maximum block count is only in effect during execution.
282 # The maximum block count is only in effect during execution.
283 # This ensures that _prompt_pos does not become invalid due to
283 # This ensures that _prompt_pos does not become invalid due to
284 # text truncation.
284 # text truncation.
285 self._control.document().setMaximumBlockCount(self.buffer_size)
285 self._control.document().setMaximumBlockCount(self.buffer_size)
286 self._execute(real_source, hidden)
286 self._execute(real_source, hidden)
287 elif hidden:
287 elif hidden:
288 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
288 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
289 else:
289 else:
290 self._show_continuation_prompt()
290 self._show_continuation_prompt()
291
291
292 return complete
292 return complete
293
293
294 def _get_input_buffer(self):
294 def _get_input_buffer(self):
295 """ The text that the user has entered entered at the current prompt.
295 """ The text that the user has entered entered at the current prompt.
296 """
296 """
297 # If we're executing, the input buffer may not even exist anymore due to
297 # If we're executing, the input buffer may not even exist anymore due to
298 # the limit imposed by 'buffer_size'. Therefore, we store it.
298 # the limit imposed by 'buffer_size'. Therefore, we store it.
299 if self._executing:
299 if self._executing:
300 return self._executing_input_buffer
300 return self._executing_input_buffer
301
301
302 cursor = self._get_end_cursor()
302 cursor = self._get_end_cursor()
303 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
303 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
304 input_buffer = str(cursor.selection().toPlainText())
304 input_buffer = str(cursor.selection().toPlainText())
305
305
306 # Strip out continuation prompts.
306 # Strip out continuation prompts.
307 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
307 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
308
308
309 def _set_input_buffer(self, string):
309 def _set_input_buffer(self, string):
310 """ Replaces the text in the input buffer with 'string'.
310 """ Replaces the text in the input buffer with 'string'.
311 """
311 """
312 # For now, it is an error to modify the input buffer during execution.
312 # For now, it is an error to modify the input buffer during execution.
313 if self._executing:
313 if self._executing:
314 raise RuntimeError("Cannot change input buffer during execution.")
314 raise RuntimeError("Cannot change input buffer during execution.")
315
315
316 # Remove old text.
316 # Remove old text.
317 cursor = self._get_end_cursor()
317 cursor = self._get_end_cursor()
318 cursor.beginEditBlock()
318 cursor.beginEditBlock()
319 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
319 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
320 cursor.removeSelectedText()
320 cursor.removeSelectedText()
321
321
322 # Insert new text with continuation prompts.
322 # Insert new text with continuation prompts.
323 lines = string.splitlines(True)
323 lines = string.splitlines(True)
324 if lines:
324 if lines:
325 self._append_plain_text(lines[0])
325 self._append_plain_text(lines[0])
326 for i in xrange(1, len(lines)):
326 for i in xrange(1, len(lines)):
327 if self._continuation_prompt_html is None:
327 if self._continuation_prompt_html is None:
328 self._append_plain_text(self._continuation_prompt)
328 self._append_plain_text(self._continuation_prompt)
329 else:
329 else:
330 self._append_html(self._continuation_prompt_html)
330 self._append_html(self._continuation_prompt_html)
331 self._append_plain_text(lines[i])
331 self._append_plain_text(lines[i])
332 cursor.endEditBlock()
332 cursor.endEditBlock()
333 self._control.moveCursor(QtGui.QTextCursor.End)
333 self._control.moveCursor(QtGui.QTextCursor.End)
334
334
335 input_buffer = property(_get_input_buffer, _set_input_buffer)
335 input_buffer = property(_get_input_buffer, _set_input_buffer)
336
336
337 def _get_font(self):
337 def _get_font(self):
338 """ The base font being used by the ConsoleWidget.
338 """ The base font being used by the ConsoleWidget.
339 """
339 """
340 return self._control.document().defaultFont()
340 return self._control.document().defaultFont()
341
341
342 def _set_font(self, font):
342 def _set_font(self, font):
343 """ Sets the base font for the ConsoleWidget to the specified QFont.
343 """ Sets the base font for the ConsoleWidget to the specified QFont.
344 """
344 """
345 font_metrics = QtGui.QFontMetrics(font)
345 font_metrics = QtGui.QFontMetrics(font)
346 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
346 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
347
347
348 self._completion_widget.setFont(font)
348 self._completion_widget.setFont(font)
349 self._control.document().setDefaultFont(font)
349 self._control.document().setDefaultFont(font)
350 if self._page_control:
351 self._page_control.document().setDefaultFont(font)
350
352
351 font = property(_get_font, _set_font)
353 font = property(_get_font, _set_font)
352
354
353 def paste(self):
355 def paste(self):
354 """ Paste the contents of the clipboard into the input region.
356 """ Paste the contents of the clipboard into the input region.
355 """
357 """
356 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
358 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
357 try:
359 try:
358 text = str(QtGui.QApplication.clipboard().text())
360 text = str(QtGui.QApplication.clipboard().text())
359 except UnicodeEncodeError:
361 except UnicodeEncodeError:
360 pass
362 pass
361 else:
363 else:
362 self._insert_into_buffer(dedent(text))
364 self._insert_into_buffer(dedent(text))
363
365
364 def print_(self, printer):
366 def print_(self, printer):
365 """ Print the contents of the ConsoleWidget to the specified QPrinter.
367 """ Print the contents of the ConsoleWidget to the specified QPrinter.
366 """
368 """
367 self._control.print_(printer)
369 self._control.print_(printer)
368
370
369 def redo(self):
371 def redo(self):
370 """ Redo the last operation. If there is no operation to redo, nothing
372 """ Redo the last operation. If there is no operation to redo, nothing
371 happens.
373 happens.
372 """
374 """
373 self._control.redo()
375 self._control.redo()
374
376
375 def reset_font(self):
377 def reset_font(self):
376 """ Sets the font to the default fixed-width font for this platform.
378 """ Sets the font to the default fixed-width font for this platform.
377 """
379 """
378 if sys.platform == 'win32':
380 if sys.platform == 'win32':
379 name = 'Courier'
381 name = 'Courier'
380 elif sys.platform == 'darwin':
382 elif sys.platform == 'darwin':
381 name = 'Monaco'
383 name = 'Monaco'
382 else:
384 else:
383 name = 'Monospace'
385 name = 'Monospace'
384 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
386 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
385 font.setStyleHint(QtGui.QFont.TypeWriter)
387 font.setStyleHint(QtGui.QFont.TypeWriter)
386 self._set_font(font)
388 self._set_font(font)
387
389
388 def select_all(self):
390 def select_all(self):
389 """ Selects all the text in the buffer.
391 """ Selects all the text in the buffer.
390 """
392 """
391 self._control.selectAll()
393 self._control.selectAll()
392
394
393 def _get_tab_width(self):
395 def _get_tab_width(self):
394 """ The width (in terms of space characters) for tab characters.
396 """ The width (in terms of space characters) for tab characters.
395 """
397 """
396 return self._tab_width
398 return self._tab_width
397
399
398 def _set_tab_width(self, tab_width):
400 def _set_tab_width(self, tab_width):
399 """ Sets the width (in terms of space characters) for tab characters.
401 """ Sets the width (in terms of space characters) for tab characters.
400 """
402 """
401 font_metrics = QtGui.QFontMetrics(self.font)
403 font_metrics = QtGui.QFontMetrics(self.font)
402 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
404 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
403
405
404 self._tab_width = tab_width
406 self._tab_width = tab_width
405
407
406 tab_width = property(_get_tab_width, _set_tab_width)
408 tab_width = property(_get_tab_width, _set_tab_width)
407
409
408 def undo(self):
410 def undo(self):
409 """ Undo the last operation. If there is no operation to undo, nothing
411 """ Undo the last operation. If there is no operation to undo, nothing
410 happens.
412 happens.
411 """
413 """
412 self._control.undo()
414 self._control.undo()
413
415
414 #---------------------------------------------------------------------------
416 #---------------------------------------------------------------------------
415 # 'ConsoleWidget' abstract interface
417 # 'ConsoleWidget' abstract interface
416 #---------------------------------------------------------------------------
418 #---------------------------------------------------------------------------
417
419
418 def _is_complete(self, source, interactive):
420 def _is_complete(self, source, interactive):
419 """ Returns whether 'source' can be executed. When triggered by an
421 """ Returns whether 'source' can be executed. When triggered by an
420 Enter/Return key press, 'interactive' is True; otherwise, it is
422 Enter/Return key press, 'interactive' is True; otherwise, it is
421 False.
423 False.
422 """
424 """
423 raise NotImplementedError
425 raise NotImplementedError
424
426
425 def _execute(self, source, hidden):
427 def _execute(self, source, hidden):
426 """ Execute 'source'. If 'hidden', do not show any output.
428 """ Execute 'source'. If 'hidden', do not show any output.
427 """
429 """
428 raise NotImplementedError
430 raise NotImplementedError
429
431
430 def _execute_interrupt(self):
432 def _execute_interrupt(self):
431 """ Attempts to stop execution. Returns whether this method has an
433 """ Attempts to stop execution. Returns whether this method has an
432 implementation.
434 implementation.
433 """
435 """
434 return False
436 return False
435
437
436 def _prompt_started_hook(self):
438 def _prompt_started_hook(self):
437 """ Called immediately after a new prompt is displayed.
439 """ Called immediately after a new prompt is displayed.
438 """
440 """
439 pass
441 pass
440
442
441 def _prompt_finished_hook(self):
443 def _prompt_finished_hook(self):
442 """ Called immediately after a prompt is finished, i.e. when some input
444 """ Called immediately after a prompt is finished, i.e. when some input
443 will be processed and a new prompt displayed.
445 will be processed and a new prompt displayed.
444 """
446 """
445 pass
447 pass
446
448
447 def _up_pressed(self):
449 def _up_pressed(self):
448 """ Called when the up key is pressed. Returns whether to continue
450 """ Called when the up key is pressed. Returns whether to continue
449 processing the event.
451 processing the event.
450 """
452 """
451 return True
453 return True
452
454
453 def _down_pressed(self):
455 def _down_pressed(self):
454 """ Called when the down key is pressed. Returns whether to continue
456 """ Called when the down key is pressed. Returns whether to continue
455 processing the event.
457 processing the event.
456 """
458 """
457 return True
459 return True
458
460
459 def _tab_pressed(self):
461 def _tab_pressed(self):
460 """ Called when the tab key is pressed. Returns whether to continue
462 """ Called when the tab key is pressed. Returns whether to continue
461 processing the event.
463 processing the event.
462 """
464 """
463 return False
465 return False
464
466
465 #--------------------------------------------------------------------------
467 #--------------------------------------------------------------------------
466 # 'ConsoleWidget' protected interface
468 # 'ConsoleWidget' protected interface
467 #--------------------------------------------------------------------------
469 #--------------------------------------------------------------------------
468
470
469 def _append_html(self, html):
471 def _append_html(self, html):
470 """ Appends html at the end of the console buffer.
472 """ Appends html at the end of the console buffer.
471 """
473 """
472 cursor = self._get_end_cursor()
474 cursor = self._get_end_cursor()
473 self._insert_html(cursor, html)
475 self._insert_html(cursor, html)
474
476
475 def _append_html_fetching_plain_text(self, html):
477 def _append_html_fetching_plain_text(self, html):
476 """ Appends 'html', then returns the plain text version of it.
478 """ Appends 'html', then returns the plain text version of it.
477 """
479 """
478 anchor = self._get_end_cursor().position()
480 anchor = self._get_end_cursor().position()
479 self._append_html(html)
481 self._append_html(html)
480 cursor = self._get_end_cursor()
482 cursor = self._get_end_cursor()
481 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
483 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
482 return str(cursor.selection().toPlainText())
484 return str(cursor.selection().toPlainText())
483
485
484 def _append_plain_text(self, text):
486 def _append_plain_text(self, text):
485 """ Appends plain text at the end of the console buffer, processing
487 """ Appends plain text at the end of the console buffer, processing
486 ANSI codes if enabled.
488 ANSI codes if enabled.
487 """
489 """
488 cursor = self._get_end_cursor()
490 cursor = self._get_end_cursor()
489 self._insert_plain_text(cursor, text)
491 self._insert_plain_text(cursor, text)
490
492
491 def _append_plain_text_keeping_prompt(self, text):
493 def _append_plain_text_keeping_prompt(self, text):
492 """ Writes 'text' after the current prompt, then restores the old prompt
494 """ Writes 'text' after the current prompt, then restores the old prompt
493 with its old input buffer.
495 with its old input buffer.
494 """
496 """
495 input_buffer = self.input_buffer
497 input_buffer = self.input_buffer
496 self._append_plain_text('\n')
498 self._append_plain_text('\n')
497 self._prompt_finished()
499 self._prompt_finished()
498
500
499 self._append_plain_text(text)
501 self._append_plain_text(text)
500 self._show_prompt()
502 self._show_prompt()
501 self.input_buffer = input_buffer
503 self.input_buffer = input_buffer
502
504
503 def _complete_with_items(self, cursor, items):
505 def _complete_with_items(self, cursor, items):
504 """ Performs completion with 'items' at the specified cursor location.
506 """ Performs completion with 'items' at the specified cursor location.
505 """
507 """
506 if len(items) == 1:
508 if len(items) == 1:
507 cursor.setPosition(self._control.textCursor().position(),
509 cursor.setPosition(self._control.textCursor().position(),
508 QtGui.QTextCursor.KeepAnchor)
510 QtGui.QTextCursor.KeepAnchor)
509 cursor.insertText(items[0])
511 cursor.insertText(items[0])
510 elif len(items) > 1:
512 elif len(items) > 1:
511 if self.gui_completion:
513 if self.gui_completion:
512 self._completion_widget.show_items(cursor, items)
514 self._completion_widget.show_items(cursor, items)
513 else:
515 else:
514 text = self._format_as_columns(items)
516 text = self._format_as_columns(items)
515 self._append_plain_text_keeping_prompt(text)
517 self._append_plain_text_keeping_prompt(text)
516
518
517 def _control_key_down(self, modifiers):
519 def _control_key_down(self, modifiers):
518 """ Given a KeyboardModifiers flags object, return whether the Control
520 """ Given a KeyboardModifiers flags object, return whether the Control
519 key is down (on Mac OS, treat the Command key as a synonym for
521 key is down (on Mac OS, treat the Command key as a synonym for
520 Control).
522 Control).
521 """
523 """
522 down = bool(modifiers & QtCore.Qt.ControlModifier)
524 down = bool(modifiers & QtCore.Qt.ControlModifier)
523
525
524 # Note: on Mac OS, ControlModifier corresponds to the Command key while
526 # Note: on Mac OS, ControlModifier corresponds to the Command key while
525 # MetaModifier corresponds to the Control key.
527 # MetaModifier corresponds to the Control key.
526 if sys.platform == 'darwin':
528 if sys.platform == 'darwin':
527 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
529 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
528
530
529 return down
531 return down
530
532
531 def _create_control(self, kind):
533 def _create_control(self, kind):
532 """ Creates and connects the underlying text widget.
534 """ Creates and connects the underlying text widget.
533 """
535 """
534 if kind == 'plain':
536 if kind == 'plain':
535 control = QtGui.QPlainTextEdit()
537 control = QtGui.QPlainTextEdit()
536 elif kind == 'rich':
538 elif kind == 'rich':
537 control = QtGui.QTextEdit()
539 control = QtGui.QTextEdit()
538 control.setAcceptRichText(False)
540 control.setAcceptRichText(False)
539 else:
541 else:
540 raise ValueError("Kind %s unknown." % repr(kind))
542 raise ValueError("Kind %s unknown." % repr(kind))
541 control.installEventFilter(self)
543 control.installEventFilter(self)
542 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
544 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
543 control.customContextMenuRequested.connect(self._show_context_menu)
545 control.customContextMenuRequested.connect(self._show_context_menu)
544 control.copyAvailable.connect(self.copy_available)
546 control.copyAvailable.connect(self.copy_available)
545 control.redoAvailable.connect(self.redo_available)
547 control.redoAvailable.connect(self.redo_available)
546 control.undoAvailable.connect(self.undo_available)
548 control.undoAvailable.connect(self.undo_available)
547 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
549 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
548 return control
550 return control
549
551
550 def _create_page_control(self):
552 def _create_page_control(self):
551 """ Creates and connects the underlying paging widget.
553 """ Creates and connects the underlying paging widget.
552 """
554 """
553 control = QtGui.QPlainTextEdit()
555 control = QtGui.QPlainTextEdit()
554 control.installEventFilter(self)
556 control.installEventFilter(self)
555 control.setReadOnly(True)
557 control.setReadOnly(True)
556 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
558 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
557 return control
559 return control
558
560
559 def _event_filter_console_keypress(self, event):
561 def _event_filter_console_keypress(self, event):
560 """ Filter key events for the underlying text widget to create a
562 """ Filter key events for the underlying text widget to create a
561 console-like interface.
563 console-like interface.
562 """
564 """
563 intercepted = False
565 intercepted = False
564 cursor = self._control.textCursor()
566 cursor = self._control.textCursor()
565 position = cursor.position()
567 position = cursor.position()
566 key = event.key()
568 key = event.key()
567 ctrl_down = self._control_key_down(event.modifiers())
569 ctrl_down = self._control_key_down(event.modifiers())
568 alt_down = event.modifiers() & QtCore.Qt.AltModifier
570 alt_down = event.modifiers() & QtCore.Qt.AltModifier
569 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
571 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
570
572
571 if event.matches(QtGui.QKeySequence.Paste):
573 if event.matches(QtGui.QKeySequence.Paste):
572 # Call our paste instead of the underlying text widget's.
574 # Call our paste instead of the underlying text widget's.
573 self.paste()
575 self.paste()
574 intercepted = True
576 intercepted = True
575
577
576 elif ctrl_down:
578 elif ctrl_down:
577 if key == QtCore.Qt.Key_C:
579 if key == QtCore.Qt.Key_C:
578 intercepted = self._executing and self._execute_interrupt()
580 intercepted = self._executing and self._execute_interrupt()
579
581
580 elif key == QtCore.Qt.Key_K:
582 elif key == QtCore.Qt.Key_K:
581 if self._in_buffer(position):
583 if self._in_buffer(position):
582 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
584 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
583 QtGui.QTextCursor.KeepAnchor)
585 QtGui.QTextCursor.KeepAnchor)
584 cursor.removeSelectedText()
586 cursor.removeSelectedText()
585 intercepted = True
587 intercepted = True
586
588
587 elif key == QtCore.Qt.Key_X:
589 elif key == QtCore.Qt.Key_X:
588 intercepted = True
590 intercepted = True
589
591
590 elif key == QtCore.Qt.Key_Y:
592 elif key == QtCore.Qt.Key_Y:
591 self.paste()
593 self.paste()
592 intercepted = True
594 intercepted = True
593
595
594 elif alt_down:
596 elif alt_down:
595 if key == QtCore.Qt.Key_B:
597 if key == QtCore.Qt.Key_B:
596 self._set_cursor(self._get_word_start_cursor(position))
598 self._set_cursor(self._get_word_start_cursor(position))
597 intercepted = True
599 intercepted = True
598
600
599 elif key == QtCore.Qt.Key_F:
601 elif key == QtCore.Qt.Key_F:
600 self._set_cursor(self._get_word_end_cursor(position))
602 self._set_cursor(self._get_word_end_cursor(position))
601 intercepted = True
603 intercepted = True
602
604
603 elif key == QtCore.Qt.Key_Backspace:
605 elif key == QtCore.Qt.Key_Backspace:
604 cursor = self._get_word_start_cursor(position)
606 cursor = self._get_word_start_cursor(position)
605 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
607 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
606 cursor.removeSelectedText()
608 cursor.removeSelectedText()
607 intercepted = True
609 intercepted = True
608
610
609 elif key == QtCore.Qt.Key_D:
611 elif key == QtCore.Qt.Key_D:
610 cursor = self._get_word_end_cursor(position)
612 cursor = self._get_word_end_cursor(position)
611 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
613 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
612 cursor.removeSelectedText()
614 cursor.removeSelectedText()
613 intercepted = True
615 intercepted = True
614
616
615 else:
617 else:
616 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
618 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
617 if self._reading:
619 if self._reading:
618 self._append_plain_text('\n')
620 self._append_plain_text('\n')
619 self._reading = False
621 self._reading = False
620 if self._reading_callback:
622 if self._reading_callback:
621 self._reading_callback()
623 self._reading_callback()
622 elif not self._executing:
624 elif not self._executing:
623 self.execute(interactive=True)
625 self.execute(interactive=True)
624 intercepted = True
626 intercepted = True
625
627
626 elif key == QtCore.Qt.Key_Up:
628 elif key == QtCore.Qt.Key_Up:
627 if self._reading or not self._up_pressed():
629 if self._reading or not self._up_pressed():
628 intercepted = True
630 intercepted = True
629 else:
631 else:
630 prompt_line = self._get_prompt_cursor().blockNumber()
632 prompt_line = self._get_prompt_cursor().blockNumber()
631 intercepted = cursor.blockNumber() <= prompt_line
633 intercepted = cursor.blockNumber() <= prompt_line
632
634
633 elif key == QtCore.Qt.Key_Down:
635 elif key == QtCore.Qt.Key_Down:
634 if self._reading or not self._down_pressed():
636 if self._reading or not self._down_pressed():
635 intercepted = True
637 intercepted = True
636 else:
638 else:
637 end_line = self._get_end_cursor().blockNumber()
639 end_line = self._get_end_cursor().blockNumber()
638 intercepted = cursor.blockNumber() == end_line
640 intercepted = cursor.blockNumber() == end_line
639
641
640 elif key == QtCore.Qt.Key_Tab:
642 elif key == QtCore.Qt.Key_Tab:
641 if self._reading:
643 if self._reading:
642 intercepted = False
644 intercepted = False
643 else:
645 else:
644 intercepted = not self._tab_pressed()
646 intercepted = not self._tab_pressed()
645
647
646 elif key == QtCore.Qt.Key_Left:
648 elif key == QtCore.Qt.Key_Left:
647 intercepted = not self._in_buffer(position - 1)
649 intercepted = not self._in_buffer(position - 1)
648
650
649 elif key == QtCore.Qt.Key_Home:
651 elif key == QtCore.Qt.Key_Home:
650 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
652 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
651 start_line = cursor.blockNumber()
653 start_line = cursor.blockNumber()
652 if start_line == self._get_prompt_cursor().blockNumber():
654 if start_line == self._get_prompt_cursor().blockNumber():
653 start_pos = self._prompt_pos
655 start_pos = self._prompt_pos
654 else:
656 else:
655 start_pos = cursor.position()
657 start_pos = cursor.position()
656 start_pos += len(self._continuation_prompt)
658 start_pos += len(self._continuation_prompt)
657 if shift_down and self._in_buffer(position):
659 if shift_down and self._in_buffer(position):
658 self._set_selection(position, start_pos)
660 self._set_selection(position, start_pos)
659 else:
661 else:
660 self._set_position(start_pos)
662 self._set_position(start_pos)
661 intercepted = True
663 intercepted = True
662
664
663 elif key == QtCore.Qt.Key_Backspace:
665 elif key == QtCore.Qt.Key_Backspace:
664
666
665 # Line deletion (remove continuation prompt)
667 # Line deletion (remove continuation prompt)
666 len_prompt = len(self._continuation_prompt)
668 len_prompt = len(self._continuation_prompt)
667 if not self._reading and \
669 if not self._reading and \
668 cursor.columnNumber() == len_prompt and \
670 cursor.columnNumber() == len_prompt and \
669 position != self._prompt_pos:
671 position != self._prompt_pos:
670 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
672 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
671 QtGui.QTextCursor.KeepAnchor)
673 QtGui.QTextCursor.KeepAnchor)
672 cursor.removeSelectedText()
674 cursor.removeSelectedText()
673
675
674 # Regular backwards deletion
676 # Regular backwards deletion
675 else:
677 else:
676 anchor = cursor.anchor()
678 anchor = cursor.anchor()
677 if anchor == position:
679 if anchor == position:
678 intercepted = not self._in_buffer(position - 1)
680 intercepted = not self._in_buffer(position - 1)
679 else:
681 else:
680 intercepted = not self._in_buffer(min(anchor, position))
682 intercepted = not self._in_buffer(min(anchor, position))
681
683
682 elif key == QtCore.Qt.Key_Delete:
684 elif key == QtCore.Qt.Key_Delete:
683 anchor = cursor.anchor()
685 anchor = cursor.anchor()
684 intercepted = not self._in_buffer(min(anchor, position))
686 intercepted = not self._in_buffer(min(anchor, position))
685
687
686 # Don't move the cursor if control is down to allow copy-paste using
688 # Don't move the cursor if control is down to allow copy-paste using
687 # the keyboard in any part of the buffer.
689 # the keyboard in any part of the buffer.
688 if not ctrl_down:
690 if not ctrl_down:
689 self._keep_cursor_in_buffer()
691 self._keep_cursor_in_buffer()
690
692
691 return intercepted
693 return intercepted
692
694
693 def _event_filter_page_keypress(self, event):
695 def _event_filter_page_keypress(self, event):
694 """ Filter key events for the paging widget to create console-like
696 """ Filter key events for the paging widget to create console-like
695 interface.
697 interface.
696 """
698 """
697 key = event.key()
699 key = event.key()
698
700
699 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
701 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
700 if self._splitter:
702 if self._splitter:
701 self._page_control.hide()
703 self._page_control.hide()
702 else:
704 else:
703 self.layout().setCurrentWidget(self._control)
705 self.layout().setCurrentWidget(self._control)
704 return True
706 return True
705
707
706 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
708 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
707 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
709 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
708 QtCore.Qt.Key_Down,
710 QtCore.Qt.Key_Down,
709 QtCore.Qt.NoModifier)
711 QtCore.Qt.NoModifier)
710 QtGui.qApp.sendEvent(self._page_control, new_event)
712 QtGui.qApp.sendEvent(self._page_control, new_event)
711 return True
713 return True
712
714
713 return False
715 return False
714
716
715 def _format_as_columns(self, items, separator=' '):
717 def _format_as_columns(self, items, separator=' '):
716 """ Transform a list of strings into a single string with columns.
718 """ Transform a list of strings into a single string with columns.
717
719
718 Parameters
720 Parameters
719 ----------
721 ----------
720 items : sequence of strings
722 items : sequence of strings
721 The strings to process.
723 The strings to process.
722
724
723 separator : str, optional [default is two spaces]
725 separator : str, optional [default is two spaces]
724 The string that separates columns.
726 The string that separates columns.
725
727
726 Returns
728 Returns
727 -------
729 -------
728 The formatted string.
730 The formatted string.
729 """
731 """
730 # Note: this code is adapted from columnize 0.3.2.
732 # Note: this code is adapted from columnize 0.3.2.
731 # See http://code.google.com/p/pycolumnize/
733 # See http://code.google.com/p/pycolumnize/
732
734
733 width = self._control.viewport().width()
735 width = self._control.viewport().width()
734 char_width = QtGui.QFontMetrics(self.font).width(' ')
736 char_width = QtGui.QFontMetrics(self.font).width(' ')
735 displaywidth = max(5, width / char_width)
737 displaywidth = max(5, width / char_width)
736
738
737 # Some degenerate cases.
739 # Some degenerate cases.
738 size = len(items)
740 size = len(items)
739 if size == 0:
741 if size == 0:
740 return '\n'
742 return '\n'
741 elif size == 1:
743 elif size == 1:
742 return '%s\n' % str(items[0])
744 return '%s\n' % str(items[0])
743
745
744 # Try every row count from 1 upwards
746 # Try every row count from 1 upwards
745 array_index = lambda nrows, row, col: nrows*col + row
747 array_index = lambda nrows, row, col: nrows*col + row
746 for nrows in range(1, size):
748 for nrows in range(1, size):
747 ncols = (size + nrows - 1) // nrows
749 ncols = (size + nrows - 1) // nrows
748 colwidths = []
750 colwidths = []
749 totwidth = -len(separator)
751 totwidth = -len(separator)
750 for col in range(ncols):
752 for col in range(ncols):
751 # Get max column width for this column
753 # Get max column width for this column
752 colwidth = 0
754 colwidth = 0
753 for row in range(nrows):
755 for row in range(nrows):
754 i = array_index(nrows, row, col)
756 i = array_index(nrows, row, col)
755 if i >= size: break
757 if i >= size: break
756 x = items[i]
758 x = items[i]
757 colwidth = max(colwidth, len(x))
759 colwidth = max(colwidth, len(x))
758 colwidths.append(colwidth)
760 colwidths.append(colwidth)
759 totwidth += colwidth + len(separator)
761 totwidth += colwidth + len(separator)
760 if totwidth > displaywidth:
762 if totwidth > displaywidth:
761 break
763 break
762 if totwidth <= displaywidth:
764 if totwidth <= displaywidth:
763 break
765 break
764
766
765 # The smallest number of rows computed and the max widths for each
767 # The smallest number of rows computed and the max widths for each
766 # column has been obtained. Now we just have to format each of the rows.
768 # column has been obtained. Now we just have to format each of the rows.
767 string = ''
769 string = ''
768 for row in range(nrows):
770 for row in range(nrows):
769 texts = []
771 texts = []
770 for col in range(ncols):
772 for col in range(ncols):
771 i = row + nrows*col
773 i = row + nrows*col
772 if i >= size:
774 if i >= size:
773 texts.append('')
775 texts.append('')
774 else:
776 else:
775 texts.append(items[i])
777 texts.append(items[i])
776 while texts and not texts[-1]:
778 while texts and not texts[-1]:
777 del texts[-1]
779 del texts[-1]
778 for col in range(len(texts)):
780 for col in range(len(texts)):
779 texts[col] = texts[col].ljust(colwidths[col])
781 texts[col] = texts[col].ljust(colwidths[col])
780 string += '%s\n' % str(separator.join(texts))
782 string += '%s\n' % str(separator.join(texts))
781 return string
783 return string
782
784
783 def _get_block_plain_text(self, block):
785 def _get_block_plain_text(self, block):
784 """ Given a QTextBlock, return its unformatted text.
786 """ Given a QTextBlock, return its unformatted text.
785 """
787 """
786 cursor = QtGui.QTextCursor(block)
788 cursor = QtGui.QTextCursor(block)
787 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
789 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
788 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
790 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
789 QtGui.QTextCursor.KeepAnchor)
791 QtGui.QTextCursor.KeepAnchor)
790 return str(cursor.selection().toPlainText())
792 return str(cursor.selection().toPlainText())
791
793
792 def _get_cursor(self):
794 def _get_cursor(self):
793 """ Convenience method that returns a cursor for the current position.
795 """ Convenience method that returns a cursor for the current position.
794 """
796 """
795 return self._control.textCursor()
797 return self._control.textCursor()
796
798
797 def _get_end_cursor(self):
799 def _get_end_cursor(self):
798 """ Convenience method that returns a cursor for the last character.
800 """ Convenience method that returns a cursor for the last character.
799 """
801 """
800 cursor = self._control.textCursor()
802 cursor = self._control.textCursor()
801 cursor.movePosition(QtGui.QTextCursor.End)
803 cursor.movePosition(QtGui.QTextCursor.End)
802 return cursor
804 return cursor
803
805
804 def _get_input_buffer_cursor_line(self):
806 def _get_input_buffer_cursor_line(self):
805 """ The text in the line of the input buffer in which the user's cursor
807 """ The text in the line of the input buffer in which the user's cursor
806 rests. Returns a string if there is such a line; otherwise, None.
808 rests. Returns a string if there is such a line; otherwise, None.
807 """
809 """
808 if self._executing:
810 if self._executing:
809 return None
811 return None
810 cursor = self._control.textCursor()
812 cursor = self._control.textCursor()
811 if cursor.position() >= self._prompt_pos:
813 if cursor.position() >= self._prompt_pos:
812 text = self._get_block_plain_text(cursor.block())
814 text = self._get_block_plain_text(cursor.block())
813 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
815 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
814 return text[len(self._prompt):]
816 return text[len(self._prompt):]
815 else:
817 else:
816 return text[len(self._continuation_prompt):]
818 return text[len(self._continuation_prompt):]
817 else:
819 else:
818 return None
820 return None
819
821
820 def _get_prompt_cursor(self):
822 def _get_prompt_cursor(self):
821 """ Convenience method that returns a cursor for the prompt position.
823 """ Convenience method that returns a cursor for the prompt position.
822 """
824 """
823 cursor = self._control.textCursor()
825 cursor = self._control.textCursor()
824 cursor.setPosition(self._prompt_pos)
826 cursor.setPosition(self._prompt_pos)
825 return cursor
827 return cursor
826
828
827 def _get_selection_cursor(self, start, end):
829 def _get_selection_cursor(self, start, end):
828 """ Convenience method that returns a cursor with text selected between
830 """ Convenience method that returns a cursor with text selected between
829 the positions 'start' and 'end'.
831 the positions 'start' and 'end'.
830 """
832 """
831 cursor = self._control.textCursor()
833 cursor = self._control.textCursor()
832 cursor.setPosition(start)
834 cursor.setPosition(start)
833 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
835 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
834 return cursor
836 return cursor
835
837
836 def _get_word_start_cursor(self, position):
838 def _get_word_start_cursor(self, position):
837 """ Find the start of the word to the left the given position. If a
839 """ Find the start of the word to the left the given position. If a
838 sequence of non-word characters precedes the first word, skip over
840 sequence of non-word characters precedes the first word, skip over
839 them. (This emulates the behavior of bash, emacs, etc.)
841 them. (This emulates the behavior of bash, emacs, etc.)
840 """
842 """
841 document = self._control.document()
843 document = self._control.document()
842 position -= 1
844 position -= 1
843 while position >= self._prompt_pos and \
845 while position >= self._prompt_pos and \
844 not document.characterAt(position).isLetterOrNumber():
846 not document.characterAt(position).isLetterOrNumber():
845 position -= 1
847 position -= 1
846 while position >= self._prompt_pos and \
848 while position >= self._prompt_pos and \
847 document.characterAt(position).isLetterOrNumber():
849 document.characterAt(position).isLetterOrNumber():
848 position -= 1
850 position -= 1
849 cursor = self._control.textCursor()
851 cursor = self._control.textCursor()
850 cursor.setPosition(position + 1)
852 cursor.setPosition(position + 1)
851 return cursor
853 return cursor
852
854
853 def _get_word_end_cursor(self, position):
855 def _get_word_end_cursor(self, position):
854 """ Find the end of the word to the right the given position. If a
856 """ Find the end of the word to the right the given position. If a
855 sequence of non-word characters precedes the first word, skip over
857 sequence of non-word characters precedes the first word, skip over
856 them. (This emulates the behavior of bash, emacs, etc.)
858 them. (This emulates the behavior of bash, emacs, etc.)
857 """
859 """
858 document = self._control.document()
860 document = self._control.document()
859 end = self._get_end_cursor().position()
861 end = self._get_end_cursor().position()
860 while position < end and \
862 while position < end and \
861 not document.characterAt(position).isLetterOrNumber():
863 not document.characterAt(position).isLetterOrNumber():
862 position += 1
864 position += 1
863 while position < end and \
865 while position < end and \
864 document.characterAt(position).isLetterOrNumber():
866 document.characterAt(position).isLetterOrNumber():
865 position += 1
867 position += 1
866 cursor = self._control.textCursor()
868 cursor = self._control.textCursor()
867 cursor.setPosition(position)
869 cursor.setPosition(position)
868 return cursor
870 return cursor
869
871
870 def _insert_html(self, cursor, html):
872 def _insert_html(self, cursor, html):
871 """ Insert HTML using the specified cursor in such a way that future
873 """ Insert HTML using the specified cursor in such a way that future
872 formatting is unaffected.
874 formatting is unaffected.
873 """
875 """
874 cursor.beginEditBlock()
876 cursor.beginEditBlock()
875 cursor.insertHtml(html)
877 cursor.insertHtml(html)
876
878
877 # After inserting HTML, the text document "remembers" it's in "html
879 # After inserting HTML, the text document "remembers" it's in "html
878 # mode", which means that subsequent calls adding plain text will result
880 # mode", which means that subsequent calls adding plain text will result
879 # in unwanted formatting, lost tab characters, etc. The following code
881 # in unwanted formatting, lost tab characters, etc. The following code
880 # hacks around this behavior, which I consider to be a bug in Qt.
882 # hacks around this behavior, which I consider to be a bug in Qt.
881 cursor.movePosition(QtGui.QTextCursor.Left,
883 cursor.movePosition(QtGui.QTextCursor.Left,
882 QtGui.QTextCursor.KeepAnchor)
884 QtGui.QTextCursor.KeepAnchor)
883 if cursor.selection().toPlainText() == ' ':
885 if cursor.selection().toPlainText() == ' ':
884 cursor.removeSelectedText()
886 cursor.removeSelectedText()
885 cursor.movePosition(QtGui.QTextCursor.Right)
887 cursor.movePosition(QtGui.QTextCursor.Right)
886 cursor.insertText(' ', QtGui.QTextCharFormat())
888 cursor.insertText(' ', QtGui.QTextCharFormat())
887 cursor.endEditBlock()
889 cursor.endEditBlock()
888
890
889 def _insert_plain_text(self, cursor, text):
891 def _insert_plain_text(self, cursor, text):
890 """ Inserts plain text using the specified cursor, processing ANSI codes
892 """ Inserts plain text using the specified cursor, processing ANSI codes
891 if enabled.
893 if enabled.
892 """
894 """
893 cursor.beginEditBlock()
895 cursor.beginEditBlock()
894 if self.ansi_codes:
896 if self.ansi_codes:
895 for substring in self._ansi_processor.split_string(text):
897 for substring in self._ansi_processor.split_string(text):
896 format = self._ansi_processor.get_format()
898 format = self._ansi_processor.get_format()
897 cursor.insertText(substring, format)
899 cursor.insertText(substring, format)
898 else:
900 else:
899 cursor.insertText(text)
901 cursor.insertText(text)
900 cursor.endEditBlock()
902 cursor.endEditBlock()
901
903
902 def _insert_into_buffer(self, text):
904 def _insert_into_buffer(self, text):
903 """ Inserts text into the input buffer at the current cursor position,
905 """ Inserts text into the input buffer at the current cursor position,
904 ensuring that continuation prompts are inserted as necessary.
906 ensuring that continuation prompts are inserted as necessary.
905 """
907 """
906 lines = str(text).splitlines(True)
908 lines = str(text).splitlines(True)
907 if lines:
909 if lines:
908 self._keep_cursor_in_buffer()
910 self._keep_cursor_in_buffer()
909 cursor = self._control.textCursor()
911 cursor = self._control.textCursor()
910 cursor.beginEditBlock()
912 cursor.beginEditBlock()
911 cursor.insertText(lines[0])
913 cursor.insertText(lines[0])
912 for line in lines[1:]:
914 for line in lines[1:]:
913 if self._continuation_prompt_html is None:
915 if self._continuation_prompt_html is None:
914 cursor.insertText(self._continuation_prompt)
916 cursor.insertText(self._continuation_prompt)
915 else:
917 else:
916 self._insert_html(cursor, self._continuation_prompt_html)
918 self._insert_html(cursor, self._continuation_prompt_html)
917 cursor.insertText(line)
919 cursor.insertText(line)
918 cursor.endEditBlock()
920 cursor.endEditBlock()
919 self._control.setTextCursor(cursor)
921 self._control.setTextCursor(cursor)
920
922
921 def _in_buffer(self, position):
923 def _in_buffer(self, position):
922 """ Returns whether the given position is inside the editing region.
924 """ Returns whether the given position is inside the editing region.
923 """
925 """
924 cursor = self._control.textCursor()
926 cursor = self._control.textCursor()
925 cursor.setPosition(position)
927 cursor.setPosition(position)
926 line = cursor.blockNumber()
928 line = cursor.blockNumber()
927 prompt_line = self._get_prompt_cursor().blockNumber()
929 prompt_line = self._get_prompt_cursor().blockNumber()
928 if line == prompt_line:
930 if line == prompt_line:
929 return position >= self._prompt_pos
931 return position >= self._prompt_pos
930 elif line > prompt_line:
932 elif line > prompt_line:
931 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
933 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
932 prompt_pos = cursor.position() + len(self._continuation_prompt)
934 prompt_pos = cursor.position() + len(self._continuation_prompt)
933 return position >= prompt_pos
935 return position >= prompt_pos
934 return False
936 return False
935
937
936 def _keep_cursor_in_buffer(self):
938 def _keep_cursor_in_buffer(self):
937 """ Ensures that the cursor is inside the editing region. Returns
939 """ Ensures that the cursor is inside the editing region. Returns
938 whether the cursor was moved.
940 whether the cursor was moved.
939 """
941 """
940 cursor = self._control.textCursor()
942 cursor = self._control.textCursor()
941 if self._in_buffer(cursor.position()):
943 if self._in_buffer(cursor.position()):
942 return False
944 return False
943 else:
945 else:
944 cursor.movePosition(QtGui.QTextCursor.End)
946 cursor.movePosition(QtGui.QTextCursor.End)
945 self._control.setTextCursor(cursor)
947 self._control.setTextCursor(cursor)
946 return True
948 return True
947
949
948 def _page(self, text):
950 def _page(self, text):
949 """ Displays text using the pager.
951 """ Displays text using the pager.
950 """
952 """
951 if self._page_style == 'custom':
953 if self._page_style == 'custom':
952 self.custom_page_requested.emit(text)
954 self.custom_page_requested.emit(text)
953 elif self._page_style == 'none':
955 elif self._page_style == 'none':
954 self._append_plain_text(text)
956 self._append_plain_text(text)
955 else:
957 else:
956 self._page_control.clear()
958 self._page_control.clear()
957 cursor = self._page_control.textCursor()
959 cursor = self._page_control.textCursor()
958 self._insert_plain_text(cursor, text)
960 self._insert_plain_text(cursor, text)
959 self._page_control.moveCursor(QtGui.QTextCursor.Start)
961 self._page_control.moveCursor(QtGui.QTextCursor.Start)
960
962
961 self._page_control.viewport().resize(self._control.size())
963 self._page_control.viewport().resize(self._control.size())
962 if self._splitter:
964 if self._splitter:
963 self._page_control.show()
965 self._page_control.show()
964 self._page_control.setFocus()
966 self._page_control.setFocus()
965 else:
967 else:
966 self.layout().setCurrentWidget(self._page_control)
968 self.layout().setCurrentWidget(self._page_control)
967
969
968 def _prompt_started(self):
970 def _prompt_started(self):
969 """ Called immediately after a new prompt is displayed.
971 """ Called immediately after a new prompt is displayed.
970 """
972 """
971 # Temporarily disable the maximum block count to permit undo/redo and
973 # Temporarily disable the maximum block count to permit undo/redo and
972 # to ensure that the prompt position does not change due to truncation.
974 # to ensure that the prompt position does not change due to truncation.
973 self._control.document().setMaximumBlockCount(0)
975 self._control.document().setMaximumBlockCount(0)
974 self._control.setUndoRedoEnabled(True)
976 self._control.setUndoRedoEnabled(True)
975
977
976 self._control.setReadOnly(False)
978 self._control.setReadOnly(False)
977 self._control.moveCursor(QtGui.QTextCursor.End)
979 self._control.moveCursor(QtGui.QTextCursor.End)
978
980
979 self._executing = False
981 self._executing = False
980 self._prompt_started_hook()
982 self._prompt_started_hook()
981
983
982 def _prompt_finished(self):
984 def _prompt_finished(self):
983 """ Called immediately after a prompt is finished, i.e. when some input
985 """ Called immediately after a prompt is finished, i.e. when some input
984 will be processed and a new prompt displayed.
986 will be processed and a new prompt displayed.
985 """
987 """
986 self._control.setUndoRedoEnabled(False)
988 self._control.setUndoRedoEnabled(False)
987 self._control.setReadOnly(True)
989 self._control.setReadOnly(True)
988 self._prompt_finished_hook()
990 self._prompt_finished_hook()
989
991
990 def _readline(self, prompt='', callback=None):
992 def _readline(self, prompt='', callback=None):
991 """ Reads one line of input from the user.
993 """ Reads one line of input from the user.
992
994
993 Parameters
995 Parameters
994 ----------
996 ----------
995 prompt : str, optional
997 prompt : str, optional
996 The prompt to print before reading the line.
998 The prompt to print before reading the line.
997
999
998 callback : callable, optional
1000 callback : callable, optional
999 A callback to execute with the read line. If not specified, input is
1001 A callback to execute with the read line. If not specified, input is
1000 read *synchronously* and this method does not return until it has
1002 read *synchronously* and this method does not return until it has
1001 been read.
1003 been read.
1002
1004
1003 Returns
1005 Returns
1004 -------
1006 -------
1005 If a callback is specified, returns nothing. Otherwise, returns the
1007 If a callback is specified, returns nothing. Otherwise, returns the
1006 input string with the trailing newline stripped.
1008 input string with the trailing newline stripped.
1007 """
1009 """
1008 if self._reading:
1010 if self._reading:
1009 raise RuntimeError('Cannot read a line. Widget is already reading.')
1011 raise RuntimeError('Cannot read a line. Widget is already reading.')
1010
1012
1011 if not callback and not self.isVisible():
1013 if not callback and not self.isVisible():
1012 # If the user cannot see the widget, this function cannot return.
1014 # If the user cannot see the widget, this function cannot return.
1013 raise RuntimeError('Cannot synchronously read a line if the widget'
1015 raise RuntimeError('Cannot synchronously read a line if the widget'
1014 'is not visible!')
1016 'is not visible!')
1015
1017
1016 self._reading = True
1018 self._reading = True
1017 self._show_prompt(prompt, newline=False)
1019 self._show_prompt(prompt, newline=False)
1018
1020
1019 if callback is None:
1021 if callback is None:
1020 self._reading_callback = None
1022 self._reading_callback = None
1021 while self._reading:
1023 while self._reading:
1022 QtCore.QCoreApplication.processEvents()
1024 QtCore.QCoreApplication.processEvents()
1023 return self.input_buffer.rstrip('\n')
1025 return self.input_buffer.rstrip('\n')
1024
1026
1025 else:
1027 else:
1026 self._reading_callback = lambda: \
1028 self._reading_callback = lambda: \
1027 callback(self.input_buffer.rstrip('\n'))
1029 callback(self.input_buffer.rstrip('\n'))
1028
1030
1029 def _reset(self):
1031 def _reset(self):
1030 """ Clears the console and resets internal state variables.
1032 """ Clears the console and resets internal state variables.
1031 """
1033 """
1032 self._control.clear()
1034 self._control.clear()
1033 self._executing = self._reading = False
1035 self._executing = self._reading = False
1034
1036
1035 def _set_continuation_prompt(self, prompt, html=False):
1037 def _set_continuation_prompt(self, prompt, html=False):
1036 """ Sets the continuation prompt.
1038 """ Sets the continuation prompt.
1037
1039
1038 Parameters
1040 Parameters
1039 ----------
1041 ----------
1040 prompt : str
1042 prompt : str
1041 The prompt to show when more input is needed.
1043 The prompt to show when more input is needed.
1042
1044
1043 html : bool, optional (default False)
1045 html : bool, optional (default False)
1044 If set, the prompt will be inserted as formatted HTML. Otherwise,
1046 If set, the prompt will be inserted as formatted HTML. Otherwise,
1045 the prompt will be treated as plain text, though ANSI color codes
1047 the prompt will be treated as plain text, though ANSI color codes
1046 will be handled.
1048 will be handled.
1047 """
1049 """
1048 if html:
1050 if html:
1049 self._continuation_prompt_html = prompt
1051 self._continuation_prompt_html = prompt
1050 else:
1052 else:
1051 self._continuation_prompt = prompt
1053 self._continuation_prompt = prompt
1052 self._continuation_prompt_html = None
1054 self._continuation_prompt_html = None
1053
1055
1054 def _set_cursor(self, cursor):
1056 def _set_cursor(self, cursor):
1055 """ Convenience method to set the current cursor.
1057 """ Convenience method to set the current cursor.
1056 """
1058 """
1057 self._control.setTextCursor(cursor)
1059 self._control.setTextCursor(cursor)
1058
1060
1059 def _set_position(self, position):
1061 def _set_position(self, position):
1060 """ Convenience method to set the position of the cursor.
1062 """ Convenience method to set the position of the cursor.
1061 """
1063 """
1062 cursor = self._control.textCursor()
1064 cursor = self._control.textCursor()
1063 cursor.setPosition(position)
1065 cursor.setPosition(position)
1064 self._control.setTextCursor(cursor)
1066 self._control.setTextCursor(cursor)
1065
1067
1066 def _set_selection(self, start, end):
1068 def _set_selection(self, start, end):
1067 """ Convenience method to set the current selected text.
1069 """ Convenience method to set the current selected text.
1068 """
1070 """
1069 self._control.setTextCursor(self._get_selection_cursor(start, end))
1071 self._control.setTextCursor(self._get_selection_cursor(start, end))
1070
1072
1071 def _show_context_menu(self, pos):
1073 def _show_context_menu(self, pos):
1072 """ Shows a context menu at the given QPoint (in widget coordinates).
1074 """ Shows a context menu at the given QPoint (in widget coordinates).
1073 """
1075 """
1074 menu = QtGui.QMenu()
1076 menu = QtGui.QMenu()
1075
1077
1076 copy_action = menu.addAction('Copy', self.copy)
1078 copy_action = menu.addAction('Copy', self.copy)
1077 copy_action.setEnabled(self._get_cursor().hasSelection())
1079 copy_action.setEnabled(self._get_cursor().hasSelection())
1078 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1080 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1079
1081
1080 paste_action = menu.addAction('Paste', self.paste)
1082 paste_action = menu.addAction('Paste', self.paste)
1081 paste_action.setEnabled(self.can_paste())
1083 paste_action.setEnabled(self.can_paste())
1082 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1084 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1083
1085
1084 menu.addSeparator()
1086 menu.addSeparator()
1085 menu.addAction('Select All', self.select_all)
1087 menu.addAction('Select All', self.select_all)
1086
1088
1087 menu.exec_(self._control.mapToGlobal(pos))
1089 menu.exec_(self._control.mapToGlobal(pos))
1088
1090
1089 def _show_prompt(self, prompt=None, html=False, newline=True):
1091 def _show_prompt(self, prompt=None, html=False, newline=True):
1090 """ Writes a new prompt at the end of the buffer.
1092 """ Writes a new prompt at the end of the buffer.
1091
1093
1092 Parameters
1094 Parameters
1093 ----------
1095 ----------
1094 prompt : str, optional
1096 prompt : str, optional
1095 The prompt to show. If not specified, the previous prompt is used.
1097 The prompt to show. If not specified, the previous prompt is used.
1096
1098
1097 html : bool, optional (default False)
1099 html : bool, optional (default False)
1098 Only relevant when a prompt is specified. If set, the prompt will
1100 Only relevant when a prompt is specified. If set, the prompt will
1099 be inserted as formatted HTML. Otherwise, the prompt will be treated
1101 be inserted as formatted HTML. Otherwise, the prompt will be treated
1100 as plain text, though ANSI color codes will be handled.
1102 as plain text, though ANSI color codes will be handled.
1101
1103
1102 newline : bool, optional (default True)
1104 newline : bool, optional (default True)
1103 If set, a new line will be written before showing the prompt if
1105 If set, a new line will be written before showing the prompt if
1104 there is not already a newline at the end of the buffer.
1106 there is not already a newline at the end of the buffer.
1105 """
1107 """
1106 # Insert a preliminary newline, if necessary.
1108 # Insert a preliminary newline, if necessary.
1107 if newline:
1109 if newline:
1108 cursor = self._get_end_cursor()
1110 cursor = self._get_end_cursor()
1109 if cursor.position() > 0:
1111 if cursor.position() > 0:
1110 cursor.movePosition(QtGui.QTextCursor.Left,
1112 cursor.movePosition(QtGui.QTextCursor.Left,
1111 QtGui.QTextCursor.KeepAnchor)
1113 QtGui.QTextCursor.KeepAnchor)
1112 if str(cursor.selection().toPlainText()) != '\n':
1114 if str(cursor.selection().toPlainText()) != '\n':
1113 self._append_plain_text('\n')
1115 self._append_plain_text('\n')
1114
1116
1115 # Write the prompt.
1117 # Write the prompt.
1116 if prompt is None:
1118 if prompt is None:
1117 if self._prompt_html is None:
1119 if self._prompt_html is None:
1118 self._append_plain_text(self._prompt)
1120 self._append_plain_text(self._prompt)
1119 else:
1121 else:
1120 self._append_html(self._prompt_html)
1122 self._append_html(self._prompt_html)
1121 else:
1123 else:
1122 if html:
1124 if html:
1123 self._prompt = self._append_html_fetching_plain_text(prompt)
1125 self._prompt = self._append_html_fetching_plain_text(prompt)
1124 self._prompt_html = prompt
1126 self._prompt_html = prompt
1125 else:
1127 else:
1126 self._append_plain_text(prompt)
1128 self._append_plain_text(prompt)
1127 self._prompt = prompt
1129 self._prompt = prompt
1128 self._prompt_html = None
1130 self._prompt_html = None
1129
1131
1130 self._prompt_pos = self._get_end_cursor().position()
1132 self._prompt_pos = self._get_end_cursor().position()
1131 self._prompt_started()
1133 self._prompt_started()
1132
1134
1133 def _show_continuation_prompt(self):
1135 def _show_continuation_prompt(self):
1134 """ Writes a new continuation prompt at the end of the buffer.
1136 """ Writes a new continuation prompt at the end of the buffer.
1135 """
1137 """
1136 if self._continuation_prompt_html is None:
1138 if self._continuation_prompt_html is None:
1137 self._append_plain_text(self._continuation_prompt)
1139 self._append_plain_text(self._continuation_prompt)
1138 else:
1140 else:
1139 self._continuation_prompt = self._append_html_fetching_plain_text(
1141 self._continuation_prompt = self._append_html_fetching_plain_text(
1140 self._continuation_prompt_html)
1142 self._continuation_prompt_html)
1141
1143
1142 self._prompt_started()
1144 self._prompt_started()
1143
1145
1144
1146
1145 class HistoryConsoleWidget(ConsoleWidget):
1147 class HistoryConsoleWidget(ConsoleWidget):
1146 """ A ConsoleWidget that keeps a history of the commands that have been
1148 """ A ConsoleWidget that keeps a history of the commands that have been
1147 executed.
1149 executed.
1148 """
1150 """
1149
1151
1150 #---------------------------------------------------------------------------
1152 #---------------------------------------------------------------------------
1151 # 'object' interface
1153 # 'object' interface
1152 #---------------------------------------------------------------------------
1154 #---------------------------------------------------------------------------
1153
1155
1154 def __init__(self, *args, **kw):
1156 def __init__(self, *args, **kw):
1155 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1157 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1156 self._history = []
1158 self._history = []
1157 self._history_index = 0
1159 self._history_index = 0
1158
1160
1159 #---------------------------------------------------------------------------
1161 #---------------------------------------------------------------------------
1160 # 'ConsoleWidget' public interface
1162 # 'ConsoleWidget' public interface
1161 #---------------------------------------------------------------------------
1163 #---------------------------------------------------------------------------
1162
1164
1163 def execute(self, source=None, hidden=False, interactive=False):
1165 def execute(self, source=None, hidden=False, interactive=False):
1164 """ Reimplemented to the store history.
1166 """ Reimplemented to the store history.
1165 """
1167 """
1166 if not hidden:
1168 if not hidden:
1167 history = self.input_buffer if source is None else source
1169 history = self.input_buffer if source is None else source
1168
1170
1169 executed = super(HistoryConsoleWidget, self).execute(
1171 executed = super(HistoryConsoleWidget, self).execute(
1170 source, hidden, interactive)
1172 source, hidden, interactive)
1171
1173
1172 if executed and not hidden:
1174 if executed and not hidden:
1173 self._history.append(history.rstrip())
1175 self._history.append(history.rstrip())
1174 self._history_index = len(self._history)
1176 self._history_index = len(self._history)
1175
1177
1176 return executed
1178 return executed
1177
1179
1178 #---------------------------------------------------------------------------
1180 #---------------------------------------------------------------------------
1179 # 'ConsoleWidget' abstract interface
1181 # 'ConsoleWidget' abstract interface
1180 #---------------------------------------------------------------------------
1182 #---------------------------------------------------------------------------
1181
1183
1182 def _up_pressed(self):
1184 def _up_pressed(self):
1183 """ Called when the up key is pressed. Returns whether to continue
1185 """ Called when the up key is pressed. Returns whether to continue
1184 processing the event.
1186 processing the event.
1185 """
1187 """
1186 prompt_cursor = self._get_prompt_cursor()
1188 prompt_cursor = self._get_prompt_cursor()
1187 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1189 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1188 self.history_previous()
1190 self.history_previous()
1189
1191
1190 # Go to the first line of prompt for seemless history scrolling.
1192 # Go to the first line of prompt for seemless history scrolling.
1191 cursor = self._get_prompt_cursor()
1193 cursor = self._get_prompt_cursor()
1192 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1194 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1193 self._set_cursor(cursor)
1195 self._set_cursor(cursor)
1194
1196
1195 return False
1197 return False
1196 return True
1198 return True
1197
1199
1198 def _down_pressed(self):
1200 def _down_pressed(self):
1199 """ Called when the down key is pressed. Returns whether to continue
1201 """ Called when the down key is pressed. Returns whether to continue
1200 processing the event.
1202 processing the event.
1201 """
1203 """
1202 end_cursor = self._get_end_cursor()
1204 end_cursor = self._get_end_cursor()
1203 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1205 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1204 self.history_next()
1206 self.history_next()
1205 return False
1207 return False
1206 return True
1208 return True
1207
1209
1208 #---------------------------------------------------------------------------
1210 #---------------------------------------------------------------------------
1209 # 'HistoryConsoleWidget' interface
1211 # 'HistoryConsoleWidget' interface
1210 #---------------------------------------------------------------------------
1212 #---------------------------------------------------------------------------
1211
1213
1212 def history_previous(self):
1214 def history_previous(self):
1213 """ If possible, set the input buffer to the previous item in the
1215 """ If possible, set the input buffer to the previous item in the
1214 history.
1216 history.
1215 """
1217 """
1216 if self._history_index > 0:
1218 if self._history_index > 0:
1217 self._history_index -= 1
1219 self._history_index -= 1
1218 self.input_buffer = self._history[self._history_index]
1220 self.input_buffer = self._history[self._history_index]
1219
1221
1220 def history_next(self):
1222 def history_next(self):
1221 """ Set the input buffer to the next item in the history, or a blank
1223 """ Set the input buffer to the next item in the history, or a blank
1222 line if there is no subsequent item.
1224 line if there is no subsequent item.
1223 """
1225 """
1224 if self._history_index < len(self._history):
1226 if self._history_index < len(self._history):
1225 self._history_index += 1
1227 self._history_index += 1
1226 if self._history_index < len(self._history):
1228 if self._history_index < len(self._history):
1227 self.input_buffer = self._history[self._history_index]
1229 self.input_buffer = self._history[self._history_index]
1228 else:
1230 else:
1229 self.input_buffer = ''
1231 self.input_buffer = ''
@@ -1,186 +1,188 b''
1 # System library imports
1 # System library imports
2 from PyQt4 import QtCore, QtGui
2 from PyQt4 import QtCore, QtGui
3
3
4 # Local imports
4 # Local imports
5 from IPython.core.usage import default_banner
5 from IPython.core.usage import default_banner
6 from frontend_widget import FrontendWidget
6 from frontend_widget import FrontendWidget
7
7
8
8
9 class IPythonWidget(FrontendWidget):
9 class IPythonWidget(FrontendWidget):
10 """ A FrontendWidget for an IPython kernel.
10 """ A FrontendWidget for an IPython kernel.
11 """
11 """
12
12
13 # The default stylesheet: black text on a white background.
13 # The default stylesheet: black text on a white background.
14 default_stylesheet = """
14 default_stylesheet = """
15 .error { color: red; }
15 .error { color: red; }
16 .in-prompt { color: navy; }
16 .in-prompt { color: navy; }
17 .in-prompt-number { font-weight: bold; }
17 .in-prompt-number { font-weight: bold; }
18 .out-prompt { color: darkred; }
18 .out-prompt { color: darkred; }
19 .out-prompt-number { font-weight: bold; }
19 .out-prompt-number { font-weight: bold; }
20 """
20 """
21
21
22 # A dark stylesheet: white text on a black background.
22 # A dark stylesheet: white text on a black background.
23 dark_stylesheet = """
23 dark_stylesheet = """
24 QPlainTextEdit { background-color: black; color: white }
24 QPlainTextEdit { background-color: black; color: white }
25 QFrame { border: 1px solid grey; }
25 QFrame { border: 1px solid grey; }
26 .error { color: red; }
26 .error { color: red; }
27 .in-prompt { color: lime; }
27 .in-prompt { color: lime; }
28 .in-prompt-number { color: lime; font-weight: bold; }
28 .in-prompt-number { color: lime; font-weight: bold; }
29 .out-prompt { color: red; }
29 .out-prompt { color: red; }
30 .out-prompt-number { color: red; font-weight: bold; }
30 .out-prompt-number { color: red; font-weight: bold; }
31 """
31 """
32
32
33 # Default prompts.
33 # Default prompts.
34 in_prompt = '<br/>In [<span class="in-prompt-number">%i</span>]: '
34 in_prompt = '<br/>In [<span class="in-prompt-number">%i</span>]: '
35 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
35 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36
36
37 #---------------------------------------------------------------------------
37 #---------------------------------------------------------------------------
38 # 'object' interface
38 # 'object' interface
39 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
40
40
41 def __init__(self, *args, **kw):
41 def __init__(self, *args, **kw):
42 super(IPythonWidget, self).__init__(*args, **kw)
42 super(IPythonWidget, self).__init__(*args, **kw)
43
43
44 # Initialize protected variables.
44 # Initialize protected variables.
45 self._previous_prompt_blocks = []
45 self._previous_prompt_blocks = []
46 self._prompt_count = 0
46 self._prompt_count = 0
47
47
48 # Set a default stylesheet.
48 # Set a default stylesheet.
49 self.reset_styling()
49 self.reset_styling()
50
50
51 #---------------------------------------------------------------------------
51 #---------------------------------------------------------------------------
52 # 'BaseFrontendMixin' abstract interface
52 # 'BaseFrontendMixin' abstract interface
53 #---------------------------------------------------------------------------
53 #---------------------------------------------------------------------------
54
54
55 def _handle_pyout(self, msg):
55 def _handle_pyout(self, msg):
56 """ Reimplemented for IPython-style "display hook".
56 """ Reimplemented for IPython-style "display hook".
57 """
57 """
58 self._append_html(self._make_out_prompt(self._prompt_count))
58 self._append_html(self._make_out_prompt(self._prompt_count))
59 self._save_prompt_block()
59 self._save_prompt_block()
60
60
61 self._append_plain_text(msg['content']['data'] + '\n')
61 self._append_plain_text(msg['content']['data'] + '\n')
62
62
63 #---------------------------------------------------------------------------
63 #---------------------------------------------------------------------------
64 # 'FrontendWidget' interface
64 # 'FrontendWidget' interface
65 #---------------------------------------------------------------------------
65 #---------------------------------------------------------------------------
66
66
67 def execute_file(self, path, hidden=False):
67 def execute_file(self, path, hidden=False):
68 """ Reimplemented to use the 'run' magic.
68 """ Reimplemented to use the 'run' magic.
69 """
69 """
70 self.execute('run %s' % path, hidden=hidden)
70 self.execute('run %s' % path, hidden=hidden)
71
71
72 #---------------------------------------------------------------------------
72 #---------------------------------------------------------------------------
73 # 'FrontendWidget' protected interface
73 # 'FrontendWidget' protected interface
74 #---------------------------------------------------------------------------
74 #---------------------------------------------------------------------------
75
75
76 def _get_banner(self):
76 def _get_banner(self):
77 """ Reimplemented to return IPython's default banner.
77 """ Reimplemented to return IPython's default banner.
78 """
78 """
79 return default_banner
79 return default_banner
80
80
81 def _process_execute_error(self, msg):
81 def _process_execute_error(self, msg):
82 """ Reimplemented for IPython-style traceback formatting.
82 """ Reimplemented for IPython-style traceback formatting.
83 """
83 """
84 content = msg['content']
84 content = msg['content']
85 traceback_lines = content['traceback'][:]
85 traceback_lines = content['traceback'][:]
86 traceback = ''.join(traceback_lines)
86 traceback = ''.join(traceback_lines)
87 traceback = traceback.replace(' ', '&nbsp;')
87 traceback = traceback.replace(' ', '&nbsp;')
88 traceback = traceback.replace('\n', '<br/>')
88 traceback = traceback.replace('\n', '<br/>')
89
89
90 ename = content['ename']
90 ename = content['ename']
91 ename_styled = '<span class="error">%s</span>' % ename
91 ename_styled = '<span class="error">%s</span>' % ename
92 traceback = traceback.replace(ename, ename_styled)
92 traceback = traceback.replace(ename, ename_styled)
93
93
94 self._append_html(traceback)
94 self._append_html(traceback)
95
95
96 def _show_interpreter_prompt(self):
96 def _show_interpreter_prompt(self):
97 """ Reimplemented for IPython-style prompts.
97 """ Reimplemented for IPython-style prompts.
98 """
98 """
99 # Update old prompt numbers if necessary.
99 # Update old prompt numbers if necessary.
100 previous_prompt_number = self._prompt_count
100 previous_prompt_number = self._prompt_count
101 if previous_prompt_number != self._prompt_count:
101 if previous_prompt_number != self._prompt_count:
102 for i, (block, length) in enumerate(self._previous_prompt_blocks):
102 for i, (block, length) in enumerate(self._previous_prompt_blocks):
103 if block.isValid():
103 if block.isValid():
104 cursor = QtGui.QTextCursor(block)
104 cursor = QtGui.QTextCursor(block)
105 cursor.movePosition(QtGui.QTextCursor.Right,
105 cursor.movePosition(QtGui.QTextCursor.Right,
106 QtGui.QTextCursor.KeepAnchor, length-1)
106 QtGui.QTextCursor.KeepAnchor, length-1)
107 if i == 0:
107 if i == 0:
108 prompt = self._make_in_prompt(previous_prompt_number)
108 prompt = self._make_in_prompt(previous_prompt_number)
109 else:
109 else:
110 prompt = self._make_out_prompt(previous_prompt_number)
110 prompt = self._make_out_prompt(previous_prompt_number)
111 self._insert_html(cursor, prompt)
111 self._insert_html(cursor, prompt)
112 self._previous_prompt_blocks = []
112 self._previous_prompt_blocks = []
113
113
114 # Show a new prompt.
114 # Show a new prompt.
115 self._prompt_count += 1
115 self._prompt_count += 1
116 self._show_prompt(self._make_in_prompt(self._prompt_count), html=True)
116 self._show_prompt(self._make_in_prompt(self._prompt_count), html=True)
117 self._save_prompt_block()
117 self._save_prompt_block()
118
118
119 # Update continuation prompt to reflect (possibly) new prompt length.
119 # Update continuation prompt to reflect (possibly) new prompt length.
120 self._set_continuation_prompt(
120 self._set_continuation_prompt(
121 self._make_continuation_prompt(self._prompt), html=True)
121 self._make_continuation_prompt(self._prompt), html=True)
122
122
123 #---------------------------------------------------------------------------
123 #---------------------------------------------------------------------------
124 # 'IPythonWidget' interface
124 # 'IPythonWidget' interface
125 #---------------------------------------------------------------------------
125 #---------------------------------------------------------------------------
126
126
127 def reset_styling(self):
127 def reset_styling(self):
128 """ Restores the default IPythonWidget styling.
128 """ Restores the default IPythonWidget styling.
129 """
129 """
130 self.set_styling(self.default_stylesheet, syntax_style='default')
130 self.set_styling(self.default_stylesheet, syntax_style='default')
131 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
131 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
132
132
133 def set_styling(self, stylesheet, syntax_style=None):
133 def set_styling(self, stylesheet, syntax_style=None):
134 """ Sets the IPythonWidget styling.
134 """ Sets the IPythonWidget styling.
135
135
136 Parameters:
136 Parameters:
137 -----------
137 -----------
138 stylesheet : str
138 stylesheet : str
139 A CSS stylesheet. The stylesheet can contain classes for:
139 A CSS stylesheet. The stylesheet can contain classes for:
140 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
140 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
141 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
141 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
142 3. IPython: .error, .in-prompt, .out-prompt, etc.
142 3. IPython: .error, .in-prompt, .out-prompt, etc.
143
143
144 syntax_style : str or None [default None]
144 syntax_style : str or None [default None]
145 If specified, use the Pygments style with given name. Otherwise,
145 If specified, use the Pygments style with given name. Otherwise,
146 the stylesheet is queried for Pygments style information.
146 the stylesheet is queried for Pygments style information.
147 """
147 """
148 self.setStyleSheet(stylesheet)
148 self.setStyleSheet(stylesheet)
149 self._control.document().setDefaultStyleSheet(stylesheet)
149 self._control.document().setDefaultStyleSheet(stylesheet)
150 if self._page_control:
151 self._page_control.document().setDefaultStyleSheet(stylesheet)
150
152
151 if syntax_style is None:
153 if syntax_style is None:
152 self._highlighter.set_style_sheet(stylesheet)
154 self._highlighter.set_style_sheet(stylesheet)
153 else:
155 else:
154 self._highlighter.set_style(syntax_style)
156 self._highlighter.set_style(syntax_style)
155
157
156 #---------------------------------------------------------------------------
158 #---------------------------------------------------------------------------
157 # 'IPythonWidget' protected interface
159 # 'IPythonWidget' protected interface
158 #---------------------------------------------------------------------------
160 #---------------------------------------------------------------------------
159
161
160 def _make_in_prompt(self, number):
162 def _make_in_prompt(self, number):
161 """ Given a prompt number, returns an HTML In prompt.
163 """ Given a prompt number, returns an HTML In prompt.
162 """
164 """
163 body = self.in_prompt % number
165 body = self.in_prompt % number
164 return '<span class="in-prompt">%s</span>' % body
166 return '<span class="in-prompt">%s</span>' % body
165
167
166 def _make_continuation_prompt(self, prompt):
168 def _make_continuation_prompt(self, prompt):
167 """ Given a plain text version of an In prompt, returns an HTML
169 """ Given a plain text version of an In prompt, returns an HTML
168 continuation prompt.
170 continuation prompt.
169 """
171 """
170 end_chars = '...: '
172 end_chars = '...: '
171 space_count = len(prompt.lstrip('\n')) - len(end_chars)
173 space_count = len(prompt.lstrip('\n')) - len(end_chars)
172 body = '&nbsp;' * space_count + end_chars
174 body = '&nbsp;' * space_count + end_chars
173 return '<span class="in-prompt">%s</span>' % body
175 return '<span class="in-prompt">%s</span>' % body
174
176
175 def _make_out_prompt(self, number):
177 def _make_out_prompt(self, number):
176 """ Given a prompt number, returns an HTML Out prompt.
178 """ Given a prompt number, returns an HTML Out prompt.
177 """
179 """
178 body = self.out_prompt % number
180 body = self.out_prompt % number
179 return '<span class="out-prompt">%s</span>' % body
181 return '<span class="out-prompt">%s</span>' % body
180
182
181 def _save_prompt_block(self):
183 def _save_prompt_block(self):
182 """ Assuming a prompt has just been written at the end of the buffer,
184 """ Assuming a prompt has just been written at the end of the buffer,
183 store the QTextBlock that contains it and its length.
185 store the QTextBlock that contains it and its length.
184 """
186 """
185 block = self._control.document().lastBlock()
187 block = self._control.document().lastBlock()
186 self._previous_prompt_blocks.append((block, block.length()))
188 self._previous_prompt_blocks.append((block, block.length()))
General Comments 0
You need to be logged in to leave comments. Login now