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