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