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