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