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