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