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