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