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