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