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