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