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