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