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