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