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