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