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