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