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