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