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