##// END OF EJS Templates
Merge pull request #7487 from takluyver/i7485...
Min RK -
r19977:8a53f005 merge
parent child Browse files
Show More
@@ -1,2163 +1,2167 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._on_flush_pending_stream_timer)
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 include_other_output = Bool(False, config=True,
523 include_other_output = Bool(False, config=True,
524 help="""Whether to include output from clients
524 help="""Whether to include output from clients
525 other than this one sharing the same kernel.
525 other than this one sharing the same kernel.
526
526
527 Outputs are not displayed until enter is pressed.
527 Outputs are not displayed until enter is pressed.
528 """
528 """
529 )
529 )
530
530
531 def can_copy(self):
531 def can_copy(self):
532 """ Returns whether text can be copied to the clipboard.
532 """ Returns whether text can be copied to the clipboard.
533 """
533 """
534 return self._control.textCursor().hasSelection()
534 return self._control.textCursor().hasSelection()
535
535
536 def can_cut(self):
536 def can_cut(self):
537 """ Returns whether text can be cut to the clipboard.
537 """ Returns whether text can be cut to the clipboard.
538 """
538 """
539 cursor = self._control.textCursor()
539 cursor = self._control.textCursor()
540 return (cursor.hasSelection() and
540 return (cursor.hasSelection() and
541 self._in_buffer(cursor.anchor()) and
541 self._in_buffer(cursor.anchor()) and
542 self._in_buffer(cursor.position()))
542 self._in_buffer(cursor.position()))
543
543
544 def can_paste(self):
544 def can_paste(self):
545 """ Returns whether text can be pasted from the clipboard.
545 """ Returns whether text can be pasted from the clipboard.
546 """
546 """
547 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
547 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
548 return bool(QtGui.QApplication.clipboard().text())
548 return bool(QtGui.QApplication.clipboard().text())
549 return False
549 return False
550
550
551 def clear(self, keep_input=True):
551 def clear(self, keep_input=True):
552 """ Clear the console.
552 """ Clear the console.
553
553
554 Parameters
554 Parameters
555 ----------
555 ----------
556 keep_input : bool, optional (default True)
556 keep_input : bool, optional (default True)
557 If set, restores the old input buffer if a new prompt is written.
557 If set, restores the old input buffer if a new prompt is written.
558 """
558 """
559 if self._executing:
559 if self._executing:
560 self._control.clear()
560 self._control.clear()
561 else:
561 else:
562 if keep_input:
562 if keep_input:
563 input_buffer = self.input_buffer
563 input_buffer = self.input_buffer
564 self._control.clear()
564 self._control.clear()
565 self._show_prompt()
565 self._show_prompt()
566 if keep_input:
566 if keep_input:
567 self.input_buffer = input_buffer
567 self.input_buffer = input_buffer
568
568
569 def copy(self):
569 def copy(self):
570 """ Copy the currently selected text to the clipboard.
570 """ Copy the currently selected text to the clipboard.
571 """
571 """
572 self.layout().currentWidget().copy()
572 self.layout().currentWidget().copy()
573
573
574 def copy_anchor(self, anchor):
574 def copy_anchor(self, anchor):
575 """ Copy anchor text to the clipboard
575 """ Copy anchor text to the clipboard
576 """
576 """
577 QtGui.QApplication.clipboard().setText(anchor)
577 QtGui.QApplication.clipboard().setText(anchor)
578
578
579 def cut(self):
579 def cut(self):
580 """ Copy the currently selected text to the clipboard and delete it
580 """ Copy the currently selected text to the clipboard and delete it
581 if it's inside the input buffer.
581 if it's inside the input buffer.
582 """
582 """
583 self.copy()
583 self.copy()
584 if self.can_cut():
584 if self.can_cut():
585 self._control.textCursor().removeSelectedText()
585 self._control.textCursor().removeSelectedText()
586
586
587 def execute(self, source=None, hidden=False, interactive=False):
587 def execute(self, source=None, hidden=False, interactive=False):
588 """ Executes source or the input buffer, possibly prompting for more
588 """ Executes source or the input buffer, possibly prompting for more
589 input.
589 input.
590
590
591 Parameters
591 Parameters
592 ----------
592 ----------
593 source : str, optional
593 source : str, optional
594
594
595 The source to execute. If not specified, the input buffer will be
595 The source to execute. If not specified, the input buffer will be
596 used. If specified and 'hidden' is False, the input buffer will be
596 used. If specified and 'hidden' is False, the input buffer will be
597 replaced with the source before execution.
597 replaced with the source before execution.
598
598
599 hidden : bool, optional (default False)
599 hidden : bool, optional (default False)
600
600
601 If set, no output will be shown and the prompt will not be modified.
601 If set, no output will be shown and the prompt will not be modified.
602 In other words, it will be completely invisible to the user that
602 In other words, it will be completely invisible to the user that
603 an execution has occurred.
603 an execution has occurred.
604
604
605 interactive : bool, optional (default False)
605 interactive : bool, optional (default False)
606
606
607 Whether the console is to treat the source as having been manually
607 Whether the console is to treat the source as having been manually
608 entered by the user. The effect of this parameter depends on the
608 entered by the user. The effect of this parameter depends on the
609 subclass implementation.
609 subclass implementation.
610
610
611 Raises
611 Raises
612 ------
612 ------
613 RuntimeError
613 RuntimeError
614 If incomplete input is given and 'hidden' is True. In this case,
614 If incomplete input is given and 'hidden' is True. In this case,
615 it is not possible to prompt for more input.
615 it is not possible to prompt for more input.
616
616
617 Returns
617 Returns
618 -------
618 -------
619 A boolean indicating whether the source was executed.
619 A boolean indicating whether the source was executed.
620 """
620 """
621 # WARNING: The order in which things happen here is very particular, in
621 # WARNING: The order in which things happen here is very particular, in
622 # large part because our syntax highlighting is fragile. If you change
622 # large part because our syntax highlighting is fragile. If you change
623 # something, test carefully!
623 # something, test carefully!
624
624
625 # Decide what to execute.
625 # Decide what to execute.
626 if source is None:
626 if source is None:
627 source = self.input_buffer
627 source = self.input_buffer
628 if not hidden:
628 if not hidden:
629 # A newline is appended later, but it should be considered part
629 # A newline is appended later, but it should be considered part
630 # of the input buffer.
630 # of the input buffer.
631 source += '\n'
631 source += '\n'
632 elif not hidden:
632 elif not hidden:
633 self.input_buffer = source
633 self.input_buffer = source
634
634
635 # Execute the source or show a continuation prompt if it is incomplete.
635 # Execute the source or show a continuation prompt if it is incomplete.
636 if self.execute_on_complete_input:
636 if self.execute_on_complete_input:
637 complete = self._is_complete(source, interactive)
637 complete = self._is_complete(source, interactive)
638 else:
638 else:
639 complete = not interactive
639 complete = not interactive
640 if hidden:
640 if hidden:
641 if complete or not self.execute_on_complete_input:
641 if complete or not self.execute_on_complete_input:
642 self._execute(source, hidden)
642 self._execute(source, hidden)
643 else:
643 else:
644 error = 'Incomplete noninteractive input: "%s"'
644 error = 'Incomplete noninteractive input: "%s"'
645 raise RuntimeError(error % source)
645 raise RuntimeError(error % source)
646 else:
646 else:
647 if complete:
647 if complete:
648 self._append_plain_text('\n')
648 self._append_plain_text('\n')
649 self._input_buffer_executing = self.input_buffer
649 self._input_buffer_executing = self.input_buffer
650 self._executing = True
650 self._executing = True
651 self._prompt_finished()
651 self._prompt_finished()
652
652
653 # The maximum block count is only in effect during execution.
653 # The maximum block count is only in effect during execution.
654 # This ensures that _prompt_pos does not become invalid due to
654 # This ensures that _prompt_pos does not become invalid due to
655 # text truncation.
655 # text truncation.
656 self._control.document().setMaximumBlockCount(self.buffer_size)
656 self._control.document().setMaximumBlockCount(self.buffer_size)
657
657
658 # Setting a positive maximum block count will automatically
658 # Setting a positive maximum block count will automatically
659 # disable the undo/redo history, but just to be safe:
659 # disable the undo/redo history, but just to be safe:
660 self._control.setUndoRedoEnabled(False)
660 self._control.setUndoRedoEnabled(False)
661
661
662 # Perform actual execution.
662 # Perform actual execution.
663 self._execute(source, hidden)
663 self._execute(source, hidden)
664
664
665 else:
665 else:
666 # Do this inside an edit block so continuation prompts are
666 # Do this inside an edit block so continuation prompts are
667 # removed seamlessly via undo/redo.
667 # removed seamlessly via undo/redo.
668 cursor = self._get_end_cursor()
668 cursor = self._get_end_cursor()
669 cursor.beginEditBlock()
669 cursor.beginEditBlock()
670 cursor.insertText('\n')
670 cursor.insertText('\n')
671 self._insert_continuation_prompt(cursor)
671 self._insert_continuation_prompt(cursor)
672 cursor.endEditBlock()
672 cursor.endEditBlock()
673
673
674 # Do not do this inside the edit block. It works as expected
674 # Do not do this inside the edit block. It works as expected
675 # when using a QPlainTextEdit control, but does not have an
675 # when using a QPlainTextEdit control, but does not have an
676 # effect when using a QTextEdit. I believe this is a Qt bug.
676 # effect when using a QTextEdit. I believe this is a Qt bug.
677 self._control.moveCursor(QtGui.QTextCursor.End)
677 self._control.moveCursor(QtGui.QTextCursor.End)
678
678
679 return complete
679 return complete
680
680
681 def export_html(self):
681 def export_html(self):
682 """ Shows a dialog to export HTML/XML in various formats.
682 """ Shows a dialog to export HTML/XML in various formats.
683 """
683 """
684 self._html_exporter.export()
684 self._html_exporter.export()
685
685
686 def _get_input_buffer(self, force=False):
686 def _get_input_buffer(self, force=False):
687 """ The text that the user has entered entered at the current prompt.
687 """ The text that the user has entered entered at the current prompt.
688
688
689 If the console is currently executing, the text that is executing will
689 If the console is currently executing, the text that is executing will
690 always be returned.
690 always be returned.
691 """
691 """
692 # If we're executing, the input buffer may not even exist anymore due to
692 # If we're executing, the input buffer may not even exist anymore due to
693 # the limit imposed by 'buffer_size'. Therefore, we store it.
693 # the limit imposed by 'buffer_size'. Therefore, we store it.
694 if self._executing and not force:
694 if self._executing and not force:
695 return self._input_buffer_executing
695 return self._input_buffer_executing
696
696
697 cursor = self._get_end_cursor()
697 cursor = self._get_end_cursor()
698 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
698 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
699 input_buffer = cursor.selection().toPlainText()
699 input_buffer = cursor.selection().toPlainText()
700
700
701 # Strip out continuation prompts.
701 # Strip out continuation prompts.
702 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
702 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
703
703
704 def _set_input_buffer(self, string):
704 def _set_input_buffer(self, string):
705 """ Sets the text in the input buffer.
705 """ Sets the text in the input buffer.
706
706
707 If the console is currently executing, this call has no *immediate*
707 If the console is currently executing, this call has no *immediate*
708 effect. When the execution is finished, the input buffer will be updated
708 effect. When the execution is finished, the input buffer will be updated
709 appropriately.
709 appropriately.
710 """
710 """
711 # If we're executing, store the text for later.
711 # If we're executing, store the text for later.
712 if self._executing:
712 if self._executing:
713 self._input_buffer_pending = string
713 self._input_buffer_pending = string
714 return
714 return
715
715
716 # Remove old text.
716 # Remove old text.
717 cursor = self._get_end_cursor()
717 cursor = self._get_end_cursor()
718 cursor.beginEditBlock()
718 cursor.beginEditBlock()
719 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
719 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
720 cursor.removeSelectedText()
720 cursor.removeSelectedText()
721
721
722 # Insert new text with continuation prompts.
722 # Insert new text with continuation prompts.
723 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
723 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
724 cursor.endEditBlock()
724 cursor.endEditBlock()
725 self._control.moveCursor(QtGui.QTextCursor.End)
725 self._control.moveCursor(QtGui.QTextCursor.End)
726
726
727 input_buffer = property(_get_input_buffer, _set_input_buffer)
727 input_buffer = property(_get_input_buffer, _set_input_buffer)
728
728
729 def _get_font(self):
729 def _get_font(self):
730 """ The base font being used by the ConsoleWidget.
730 """ The base font being used by the ConsoleWidget.
731 """
731 """
732 return self._control.document().defaultFont()
732 return self._control.document().defaultFont()
733
733
734 def _set_font(self, font):
734 def _set_font(self, font):
735 """ Sets the base font for the ConsoleWidget to the specified QFont.
735 """ Sets the base font for the ConsoleWidget to the specified QFont.
736 """
736 """
737 font_metrics = QtGui.QFontMetrics(font)
737 font_metrics = QtGui.QFontMetrics(font)
738 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
738 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
739
739
740 self._completion_widget.setFont(font)
740 self._completion_widget.setFont(font)
741 self._control.document().setDefaultFont(font)
741 self._control.document().setDefaultFont(font)
742 if self._page_control:
742 if self._page_control:
743 self._page_control.document().setDefaultFont(font)
743 self._page_control.document().setDefaultFont(font)
744
744
745 self.font_changed.emit(font)
745 self.font_changed.emit(font)
746
746
747 font = property(_get_font, _set_font)
747 font = property(_get_font, _set_font)
748
748
749 def open_anchor(self, anchor):
749 def open_anchor(self, anchor):
750 """ Open selected anchor in the default webbrowser
750 """ Open selected anchor in the default webbrowser
751 """
751 """
752 webbrowser.open( anchor )
752 webbrowser.open( anchor )
753
753
754 def paste(self, mode=QtGui.QClipboard.Clipboard):
754 def paste(self, mode=QtGui.QClipboard.Clipboard):
755 """ Paste the contents of the clipboard into the input region.
755 """ Paste the contents of the clipboard into the input region.
756
756
757 Parameters
757 Parameters
758 ----------
758 ----------
759 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
759 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
760
760
761 Controls which part of the system clipboard is used. This can be
761 Controls which part of the system clipboard is used. This can be
762 used to access the selection clipboard in X11 and the Find buffer
762 used to access the selection clipboard in X11 and the Find buffer
763 in Mac OS. By default, the regular clipboard is used.
763 in Mac OS. By default, the regular clipboard is used.
764 """
764 """
765 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
765 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
766 # Make sure the paste is safe.
766 # Make sure the paste is safe.
767 self._keep_cursor_in_buffer()
767 self._keep_cursor_in_buffer()
768 cursor = self._control.textCursor()
768 cursor = self._control.textCursor()
769
769
770 # Remove any trailing newline, which confuses the GUI and forces the
770 # Remove any trailing newline, which confuses the GUI and forces the
771 # user to backspace.
771 # user to backspace.
772 text = QtGui.QApplication.clipboard().text(mode).rstrip()
772 text = QtGui.QApplication.clipboard().text(mode).rstrip()
773 self._insert_plain_text_into_buffer(cursor, dedent(text))
773 self._insert_plain_text_into_buffer(cursor, dedent(text))
774
774
775 def print_(self, printer = None):
775 def print_(self, printer = None):
776 """ Print the contents of the ConsoleWidget to the specified QPrinter.
776 """ Print the contents of the ConsoleWidget to the specified QPrinter.
777 """
777 """
778 if (not printer):
778 if (not printer):
779 printer = QtGui.QPrinter()
779 printer = QtGui.QPrinter()
780 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
780 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
781 return
781 return
782 self._control.print_(printer)
782 self._control.print_(printer)
783
783
784 def prompt_to_top(self):
784 def prompt_to_top(self):
785 """ Moves the prompt to the top of the viewport.
785 """ Moves the prompt to the top of the viewport.
786 """
786 """
787 if not self._executing:
787 if not self._executing:
788 prompt_cursor = self._get_prompt_cursor()
788 prompt_cursor = self._get_prompt_cursor()
789 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
789 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
790 self._set_cursor(prompt_cursor)
790 self._set_cursor(prompt_cursor)
791 self._set_top_cursor(prompt_cursor)
791 self._set_top_cursor(prompt_cursor)
792
792
793 def redo(self):
793 def redo(self):
794 """ Redo the last operation. If there is no operation to redo, nothing
794 """ Redo the last operation. If there is no operation to redo, nothing
795 happens.
795 happens.
796 """
796 """
797 self._control.redo()
797 self._control.redo()
798
798
799 def reset_font(self):
799 def reset_font(self):
800 """ Sets the font to the default fixed-width font for this platform.
800 """ Sets the font to the default fixed-width font for this platform.
801 """
801 """
802 if sys.platform == 'win32':
802 if sys.platform == 'win32':
803 # Consolas ships with Vista/Win7, fallback to Courier if needed
803 # Consolas ships with Vista/Win7, fallback to Courier if needed
804 fallback = 'Courier'
804 fallback = 'Courier'
805 elif sys.platform == 'darwin':
805 elif sys.platform == 'darwin':
806 # OSX always has Monaco
806 # OSX always has Monaco
807 fallback = 'Monaco'
807 fallback = 'Monaco'
808 else:
808 else:
809 # Monospace should always exist
809 # Monospace should always exist
810 fallback = 'Monospace'
810 fallback = 'Monospace'
811 font = get_font(self.font_family, fallback)
811 font = get_font(self.font_family, fallback)
812 if self.font_size:
812 if self.font_size:
813 font.setPointSize(self.font_size)
813 font.setPointSize(self.font_size)
814 else:
814 else:
815 font.setPointSize(QtGui.qApp.font().pointSize())
815 font.setPointSize(QtGui.qApp.font().pointSize())
816 font.setStyleHint(QtGui.QFont.TypeWriter)
816 font.setStyleHint(QtGui.QFont.TypeWriter)
817 self._set_font(font)
817 self._set_font(font)
818
818
819 def change_font_size(self, delta):
819 def change_font_size(self, delta):
820 """Change the font size by the specified amount (in points).
820 """Change the font size by the specified amount (in points).
821 """
821 """
822 font = self.font
822 font = self.font
823 size = max(font.pointSize() + delta, 1) # minimum 1 point
823 size = max(font.pointSize() + delta, 1) # minimum 1 point
824 font.setPointSize(size)
824 font.setPointSize(size)
825 self._set_font(font)
825 self._set_font(font)
826
826
827 def _increase_font_size(self):
827 def _increase_font_size(self):
828 self.change_font_size(1)
828 self.change_font_size(1)
829
829
830 def _decrease_font_size(self):
830 def _decrease_font_size(self):
831 self.change_font_size(-1)
831 self.change_font_size(-1)
832
832
833 def select_all(self):
833 def select_all(self):
834 """ Selects all the text in the buffer.
834 """ Selects all the text in the buffer.
835 """
835 """
836 self._control.selectAll()
836 self._control.selectAll()
837
837
838 def _get_tab_width(self):
838 def _get_tab_width(self):
839 """ The width (in terms of space characters) for tab characters.
839 """ The width (in terms of space characters) for tab characters.
840 """
840 """
841 return self._tab_width
841 return self._tab_width
842
842
843 def _set_tab_width(self, tab_width):
843 def _set_tab_width(self, tab_width):
844 """ Sets the width (in terms of space characters) for tab characters.
844 """ Sets the width (in terms of space characters) for tab characters.
845 """
845 """
846 font_metrics = QtGui.QFontMetrics(self.font)
846 font_metrics = QtGui.QFontMetrics(self.font)
847 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
847 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
848
848
849 self._tab_width = tab_width
849 self._tab_width = tab_width
850
850
851 tab_width = property(_get_tab_width, _set_tab_width)
851 tab_width = property(_get_tab_width, _set_tab_width)
852
852
853 def undo(self):
853 def undo(self):
854 """ Undo the last operation. If there is no operation to undo, nothing
854 """ Undo the last operation. If there is no operation to undo, nothing
855 happens.
855 happens.
856 """
856 """
857 self._control.undo()
857 self._control.undo()
858
858
859 #---------------------------------------------------------------------------
859 #---------------------------------------------------------------------------
860 # 'ConsoleWidget' abstract interface
860 # 'ConsoleWidget' abstract interface
861 #---------------------------------------------------------------------------
861 #---------------------------------------------------------------------------
862
862
863 def _is_complete(self, source, interactive):
863 def _is_complete(self, source, interactive):
864 """ Returns whether 'source' can be executed. When triggered by an
864 """ Returns whether 'source' can be executed. When triggered by an
865 Enter/Return key press, 'interactive' is True; otherwise, it is
865 Enter/Return key press, 'interactive' is True; otherwise, it is
866 False.
866 False.
867 """
867 """
868 raise NotImplementedError
868 raise NotImplementedError
869
869
870 def _execute(self, source, hidden):
870 def _execute(self, source, hidden):
871 """ Execute 'source'. If 'hidden', do not show any output.
871 """ Execute 'source'. If 'hidden', do not show any output.
872 """
872 """
873 raise NotImplementedError
873 raise NotImplementedError
874
874
875 def _prompt_started_hook(self):
875 def _prompt_started_hook(self):
876 """ Called immediately after a new prompt is displayed.
876 """ Called immediately after a new prompt is displayed.
877 """
877 """
878 pass
878 pass
879
879
880 def _prompt_finished_hook(self):
880 def _prompt_finished_hook(self):
881 """ Called immediately after a prompt is finished, i.e. when some input
881 """ Called immediately after a prompt is finished, i.e. when some input
882 will be processed and a new prompt displayed.
882 will be processed and a new prompt displayed.
883 """
883 """
884 pass
884 pass
885
885
886 def _up_pressed(self, shift_modifier):
886 def _up_pressed(self, shift_modifier):
887 """ Called when the up key is pressed. Returns whether to continue
887 """ Called when the up key is pressed. Returns whether to continue
888 processing the event.
888 processing the event.
889 """
889 """
890 return True
890 return True
891
891
892 def _down_pressed(self, shift_modifier):
892 def _down_pressed(self, shift_modifier):
893 """ Called when the down key is pressed. Returns whether to continue
893 """ Called when the down key is pressed. Returns whether to continue
894 processing the event.
894 processing the event.
895 """
895 """
896 return True
896 return True
897
897
898 def _tab_pressed(self):
898 def _tab_pressed(self):
899 """ Called when the tab key is pressed. Returns whether to continue
899 """ Called when the tab key is pressed. Returns whether to continue
900 processing the event.
900 processing the event.
901 """
901 """
902 return False
902 return False
903
903
904 #--------------------------------------------------------------------------
904 #--------------------------------------------------------------------------
905 # 'ConsoleWidget' protected interface
905 # 'ConsoleWidget' protected interface
906 #--------------------------------------------------------------------------
906 #--------------------------------------------------------------------------
907
907
908 def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
908 def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
909 """ A low-level method for appending content to the end of the buffer.
909 """ A low-level method for appending content to the end of the buffer.
910
910
911 If 'before_prompt' is enabled, the content will be inserted before the
911 If 'before_prompt' is enabled, the content will be inserted before the
912 current prompt, if there is one.
912 current prompt, if there is one.
913 """
913 """
914 # Determine where to insert the content.
914 # Determine where to insert the content.
915 cursor = self._control.textCursor()
915 cursor = self._control.textCursor()
916 if before_prompt and (self._reading or not self._executing):
916 if before_prompt and (self._reading or not self._executing):
917 self._flush_pending_stream()
917 self._flush_pending_stream()
918 cursor.setPosition(self._append_before_prompt_pos)
918 cursor.setPosition(self._append_before_prompt_pos)
919 else:
919 else:
920 if insert != self._insert_plain_text:
920 if insert != self._insert_plain_text:
921 self._flush_pending_stream()
921 self._flush_pending_stream()
922 cursor.movePosition(QtGui.QTextCursor.End)
922 cursor.movePosition(QtGui.QTextCursor.End)
923 start_pos = cursor.position()
923 start_pos = cursor.position()
924
924
925 # Perform the insertion.
925 # Perform the insertion.
926 result = insert(cursor, input, *args, **kwargs)
926 result = insert(cursor, input, *args, **kwargs)
927
927
928 # Adjust the prompt position if we have inserted before it. This is safe
928 # Adjust the prompt position if we have inserted before it. This is safe
929 # because buffer truncation is disabled when not executing.
929 # because buffer truncation is disabled when not executing.
930 if before_prompt and (self._reading or not self._executing):
930 if before_prompt and (self._reading or not self._executing):
931 diff = cursor.position() - start_pos
931 diff = cursor.position() - start_pos
932 self._append_before_prompt_pos += diff
932 self._append_before_prompt_pos += diff
933 self._prompt_pos += diff
933 self._prompt_pos += diff
934
934
935 return result
935 return result
936
936
937 def _append_block(self, block_format=None, before_prompt=False):
937 def _append_block(self, block_format=None, before_prompt=False):
938 """ Appends an new QTextBlock to the end of the console buffer.
938 """ Appends an new QTextBlock to the end of the console buffer.
939 """
939 """
940 self._append_custom(self._insert_block, block_format, before_prompt)
940 self._append_custom(self._insert_block, block_format, before_prompt)
941
941
942 def _append_html(self, html, before_prompt=False):
942 def _append_html(self, html, before_prompt=False):
943 """ Appends HTML at the end of the console buffer.
943 """ Appends HTML at the end of the console buffer.
944 """
944 """
945 self._append_custom(self._insert_html, html, before_prompt)
945 self._append_custom(self._insert_html, html, before_prompt)
946
946
947 def _append_html_fetching_plain_text(self, html, before_prompt=False):
947 def _append_html_fetching_plain_text(self, html, before_prompt=False):
948 """ Appends HTML, then returns the plain text version of it.
948 """ Appends HTML, then returns the plain text version of it.
949 """
949 """
950 return self._append_custom(self._insert_html_fetching_plain_text,
950 return self._append_custom(self._insert_html_fetching_plain_text,
951 html, before_prompt)
951 html, before_prompt)
952
952
953 def _append_plain_text(self, text, before_prompt=False):
953 def _append_plain_text(self, text, before_prompt=False):
954 """ Appends plain text, processing ANSI codes if enabled.
954 """ Appends plain text, processing ANSI codes if enabled.
955 """
955 """
956 self._append_custom(self._insert_plain_text, text, before_prompt)
956 self._append_custom(self._insert_plain_text, text, before_prompt)
957
957
958 def _cancel_completion(self):
958 def _cancel_completion(self):
959 """ If text completion is progress, cancel it.
959 """ If text completion is progress, cancel it.
960 """
960 """
961 self._completion_widget.cancel_completion()
961 self._completion_widget.cancel_completion()
962
962
963 def _clear_temporary_buffer(self):
963 def _clear_temporary_buffer(self):
964 """ Clears the "temporary text" buffer, i.e. all the text following
964 """ Clears the "temporary text" buffer, i.e. all the text following
965 the prompt region.
965 the prompt region.
966 """
966 """
967 # Select and remove all text below the input buffer.
967 # Select and remove all text below the input buffer.
968 cursor = self._get_prompt_cursor()
968 cursor = self._get_prompt_cursor()
969 prompt = self._continuation_prompt.lstrip()
969 prompt = self._continuation_prompt.lstrip()
970 if(self._temp_buffer_filled):
970 if(self._temp_buffer_filled):
971 self._temp_buffer_filled = False
971 self._temp_buffer_filled = False
972 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
972 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
973 temp_cursor = QtGui.QTextCursor(cursor)
973 temp_cursor = QtGui.QTextCursor(cursor)
974 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
974 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
975 text = temp_cursor.selection().toPlainText().lstrip()
975 text = temp_cursor.selection().toPlainText().lstrip()
976 if not text.startswith(prompt):
976 if not text.startswith(prompt):
977 break
977 break
978 else:
978 else:
979 # We've reached the end of the input buffer and no text follows.
979 # We've reached the end of the input buffer and no text follows.
980 return
980 return
981 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
981 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
982 cursor.movePosition(QtGui.QTextCursor.End,
982 cursor.movePosition(QtGui.QTextCursor.End,
983 QtGui.QTextCursor.KeepAnchor)
983 QtGui.QTextCursor.KeepAnchor)
984 cursor.removeSelectedText()
984 cursor.removeSelectedText()
985
985
986 # After doing this, we have no choice but to clear the undo/redo
986 # After doing this, we have no choice but to clear the undo/redo
987 # history. Otherwise, the text is not "temporary" at all, because it
987 # history. Otherwise, the text is not "temporary" at all, because it
988 # can be recalled with undo/redo. Unfortunately, Qt does not expose
988 # can be recalled with undo/redo. Unfortunately, Qt does not expose
989 # fine-grained control to the undo/redo system.
989 # fine-grained control to the undo/redo system.
990 if self._control.isUndoRedoEnabled():
990 if self._control.isUndoRedoEnabled():
991 self._control.setUndoRedoEnabled(False)
991 self._control.setUndoRedoEnabled(False)
992 self._control.setUndoRedoEnabled(True)
992 self._control.setUndoRedoEnabled(True)
993
993
994 def _complete_with_items(self, cursor, items):
994 def _complete_with_items(self, cursor, items):
995 """ Performs completion with 'items' at the specified cursor location.
995 """ Performs completion with 'items' at the specified cursor location.
996 """
996 """
997 self._cancel_completion()
997 self._cancel_completion()
998
998
999 if len(items) == 1:
999 if len(items) == 1:
1000 cursor.setPosition(self._control.textCursor().position(),
1000 cursor.setPosition(self._control.textCursor().position(),
1001 QtGui.QTextCursor.KeepAnchor)
1001 QtGui.QTextCursor.KeepAnchor)
1002 cursor.insertText(items[0])
1002 cursor.insertText(items[0])
1003
1003
1004 elif len(items) > 1:
1004 elif len(items) > 1:
1005 current_pos = self._control.textCursor().position()
1005 current_pos = self._control.textCursor().position()
1006 prefix = commonprefix(items)
1006 prefix = commonprefix(items)
1007 if prefix:
1007 if prefix:
1008 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
1008 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
1009 cursor.insertText(prefix)
1009 cursor.insertText(prefix)
1010 current_pos = cursor.position()
1010 current_pos = cursor.position()
1011
1011
1012 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
1012 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
1013 self._completion_widget.show_items(cursor, items)
1013 self._completion_widget.show_items(cursor, items)
1014
1014
1015
1015
1016 def _fill_temporary_buffer(self, cursor, text, html=False):
1016 def _fill_temporary_buffer(self, cursor, text, html=False):
1017 """fill the area below the active editting zone with text"""
1017 """fill the area below the active editting zone with text"""
1018
1018
1019 current_pos = self._control.textCursor().position()
1019 current_pos = self._control.textCursor().position()
1020
1020
1021 cursor.beginEditBlock()
1021 cursor.beginEditBlock()
1022 self._append_plain_text('\n')
1022 self._append_plain_text('\n')
1023 self._page(text, html=html)
1023 self._page(text, html=html)
1024 cursor.endEditBlock()
1024 cursor.endEditBlock()
1025
1025
1026 cursor.setPosition(current_pos)
1026 cursor.setPosition(current_pos)
1027 self._control.moveCursor(QtGui.QTextCursor.End)
1027 self._control.moveCursor(QtGui.QTextCursor.End)
1028 self._control.setTextCursor(cursor)
1028 self._control.setTextCursor(cursor)
1029
1029
1030 self._temp_buffer_filled = True
1030 self._temp_buffer_filled = True
1031
1031
1032
1032
1033 def _context_menu_make(self, pos):
1033 def _context_menu_make(self, pos):
1034 """ Creates a context menu for the given QPoint (in widget coordinates).
1034 """ Creates a context menu for the given QPoint (in widget coordinates).
1035 """
1035 """
1036 menu = QtGui.QMenu(self)
1036 menu = QtGui.QMenu(self)
1037
1037
1038 self.cut_action = menu.addAction('Cut', self.cut)
1038 self.cut_action = menu.addAction('Cut', self.cut)
1039 self.cut_action.setEnabled(self.can_cut())
1039 self.cut_action.setEnabled(self.can_cut())
1040 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1040 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1041
1041
1042 self.copy_action = menu.addAction('Copy', self.copy)
1042 self.copy_action = menu.addAction('Copy', self.copy)
1043 self.copy_action.setEnabled(self.can_copy())
1043 self.copy_action.setEnabled(self.can_copy())
1044 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1044 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1045
1045
1046 self.paste_action = menu.addAction('Paste', self.paste)
1046 self.paste_action = menu.addAction('Paste', self.paste)
1047 self.paste_action.setEnabled(self.can_paste())
1047 self.paste_action.setEnabled(self.can_paste())
1048 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1048 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1049
1049
1050 anchor = self._control.anchorAt(pos)
1050 anchor = self._control.anchorAt(pos)
1051 if anchor:
1051 if anchor:
1052 menu.addSeparator()
1052 menu.addSeparator()
1053 self.copy_link_action = menu.addAction(
1053 self.copy_link_action = menu.addAction(
1054 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1054 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1055 self.open_link_action = menu.addAction(
1055 self.open_link_action = menu.addAction(
1056 'Open Link', lambda: self.open_anchor(anchor=anchor))
1056 'Open Link', lambda: self.open_anchor(anchor=anchor))
1057
1057
1058 menu.addSeparator()
1058 menu.addSeparator()
1059 menu.addAction(self.select_all_action)
1059 menu.addAction(self.select_all_action)
1060
1060
1061 menu.addSeparator()
1061 menu.addSeparator()
1062 menu.addAction(self.export_action)
1062 menu.addAction(self.export_action)
1063 menu.addAction(self.print_action)
1063 menu.addAction(self.print_action)
1064
1064
1065 return menu
1065 return menu
1066
1066
1067 def _control_key_down(self, modifiers, include_command=False):
1067 def _control_key_down(self, modifiers, include_command=False):
1068 """ Given a KeyboardModifiers flags object, return whether the Control
1068 """ Given a KeyboardModifiers flags object, return whether the Control
1069 key is down.
1069 key is down.
1070
1070
1071 Parameters
1071 Parameters
1072 ----------
1072 ----------
1073 include_command : bool, optional (default True)
1073 include_command : bool, optional (default True)
1074 Whether to treat the Command key as a (mutually exclusive) synonym
1074 Whether to treat the Command key as a (mutually exclusive) synonym
1075 for Control when in Mac OS.
1075 for Control when in Mac OS.
1076 """
1076 """
1077 # Note that on Mac OS, ControlModifier corresponds to the Command key
1077 # Note that on Mac OS, ControlModifier corresponds to the Command key
1078 # while MetaModifier corresponds to the Control key.
1078 # while MetaModifier corresponds to the Control key.
1079 if sys.platform == 'darwin':
1079 if sys.platform == 'darwin':
1080 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1080 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1081 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1081 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1082 else:
1082 else:
1083 return bool(modifiers & QtCore.Qt.ControlModifier)
1083 return bool(modifiers & QtCore.Qt.ControlModifier)
1084
1084
1085 def _create_control(self):
1085 def _create_control(self):
1086 """ Creates and connects the underlying text widget.
1086 """ Creates and connects the underlying text widget.
1087 """
1087 """
1088 # Create the underlying control.
1088 # Create the underlying control.
1089 if self.custom_control:
1089 if self.custom_control:
1090 control = self.custom_control()
1090 control = self.custom_control()
1091 elif self.kind == 'plain':
1091 elif self.kind == 'plain':
1092 control = QtGui.QPlainTextEdit()
1092 control = QtGui.QPlainTextEdit()
1093 elif self.kind == 'rich':
1093 elif self.kind == 'rich':
1094 control = QtGui.QTextEdit()
1094 control = QtGui.QTextEdit()
1095 control.setAcceptRichText(False)
1095 control.setAcceptRichText(False)
1096 control.setMouseTracking(True)
1096 control.setMouseTracking(True)
1097
1097
1098 # Prevent the widget from handling drops, as we already provide
1098 # Prevent the widget from handling drops, as we already provide
1099 # the logic in this class.
1099 # the logic in this class.
1100 control.setAcceptDrops(False)
1100 control.setAcceptDrops(False)
1101
1101
1102 # Install event filters. The filter on the viewport is needed for
1102 # Install event filters. The filter on the viewport is needed for
1103 # mouse events.
1103 # mouse events.
1104 control.installEventFilter(self)
1104 control.installEventFilter(self)
1105 control.viewport().installEventFilter(self)
1105 control.viewport().installEventFilter(self)
1106
1106
1107 # Connect signals.
1107 # Connect signals.
1108 control.customContextMenuRequested.connect(
1108 control.customContextMenuRequested.connect(
1109 self._custom_context_menu_requested)
1109 self._custom_context_menu_requested)
1110 control.copyAvailable.connect(self.copy_available)
1110 control.copyAvailable.connect(self.copy_available)
1111 control.redoAvailable.connect(self.redo_available)
1111 control.redoAvailable.connect(self.redo_available)
1112 control.undoAvailable.connect(self.undo_available)
1112 control.undoAvailable.connect(self.undo_available)
1113
1113
1114 # Hijack the document size change signal to prevent Qt from adjusting
1114 # Hijack the document size change signal to prevent Qt from adjusting
1115 # the viewport's scrollbar. We are relying on an implementation detail
1115 # the viewport's scrollbar. We are relying on an implementation detail
1116 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1116 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1117 # this functionality we cannot create a nice terminal interface.
1117 # this functionality we cannot create a nice terminal interface.
1118 layout = control.document().documentLayout()
1118 layout = control.document().documentLayout()
1119 layout.documentSizeChanged.disconnect()
1119 layout.documentSizeChanged.disconnect()
1120 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1120 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1121
1121
1122 # Configure the control.
1122 # Configure the control.
1123 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1123 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1124 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1124 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1125 control.setReadOnly(True)
1125 control.setReadOnly(True)
1126 control.setUndoRedoEnabled(False)
1126 control.setUndoRedoEnabled(False)
1127 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1127 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1128 return control
1128 return control
1129
1129
1130 def _create_page_control(self):
1130 def _create_page_control(self):
1131 """ Creates and connects the underlying paging widget.
1131 """ Creates and connects the underlying paging widget.
1132 """
1132 """
1133 if self.custom_page_control:
1133 if self.custom_page_control:
1134 control = self.custom_page_control()
1134 control = self.custom_page_control()
1135 elif self.kind == 'plain':
1135 elif self.kind == 'plain':
1136 control = QtGui.QPlainTextEdit()
1136 control = QtGui.QPlainTextEdit()
1137 elif self.kind == 'rich':
1137 elif self.kind == 'rich':
1138 control = QtGui.QTextEdit()
1138 control = QtGui.QTextEdit()
1139 control.installEventFilter(self)
1139 control.installEventFilter(self)
1140 viewport = control.viewport()
1140 viewport = control.viewport()
1141 viewport.installEventFilter(self)
1141 viewport.installEventFilter(self)
1142 control.setReadOnly(True)
1142 control.setReadOnly(True)
1143 control.setUndoRedoEnabled(False)
1143 control.setUndoRedoEnabled(False)
1144 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1144 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1145 return control
1145 return control
1146
1146
1147 def _event_filter_console_keypress(self, event):
1147 def _event_filter_console_keypress(self, event):
1148 """ Filter key events for the underlying text widget to create a
1148 """ Filter key events for the underlying text widget to create a
1149 console-like interface.
1149 console-like interface.
1150 """
1150 """
1151 intercepted = False
1151 intercepted = False
1152 cursor = self._control.textCursor()
1152 cursor = self._control.textCursor()
1153 position = cursor.position()
1153 position = cursor.position()
1154 key = event.key()
1154 key = event.key()
1155 ctrl_down = self._control_key_down(event.modifiers())
1155 ctrl_down = self._control_key_down(event.modifiers())
1156 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1156 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1157 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1157 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1158
1158
1159 #------ Special sequences ----------------------------------------------
1159 #------ Special sequences ----------------------------------------------
1160
1160
1161 if event.matches(QtGui.QKeySequence.Copy):
1161 if event.matches(QtGui.QKeySequence.Copy):
1162 self.copy()
1162 self.copy()
1163 intercepted = True
1163 intercepted = True
1164
1164
1165 elif event.matches(QtGui.QKeySequence.Cut):
1165 elif event.matches(QtGui.QKeySequence.Cut):
1166 self.cut()
1166 self.cut()
1167 intercepted = True
1167 intercepted = True
1168
1168
1169 elif event.matches(QtGui.QKeySequence.Paste):
1169 elif event.matches(QtGui.QKeySequence.Paste):
1170 self.paste()
1170 self.paste()
1171 intercepted = True
1171 intercepted = True
1172
1172
1173 #------ Special modifier logic -----------------------------------------
1173 #------ Special modifier logic -----------------------------------------
1174
1174
1175 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1175 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1176 intercepted = True
1176 intercepted = True
1177
1177
1178 # Special handling when tab completing in text mode.
1178 # Special handling when tab completing in text mode.
1179 self._cancel_completion()
1179 self._cancel_completion()
1180
1180
1181 if self._in_buffer(position):
1181 if self._in_buffer(position):
1182 # Special handling when a reading a line of raw input.
1182 # Special handling when a reading a line of raw input.
1183 if self._reading:
1183 if self._reading:
1184 self._append_plain_text('\n')
1184 self._append_plain_text('\n')
1185 self._reading = False
1185 self._reading = False
1186 if self._reading_callback:
1186 if self._reading_callback:
1187 self._reading_callback()
1187 self._reading_callback()
1188
1188
1189 # If the input buffer is a single line or there is only
1189 # If the input buffer is a single line or there is only
1190 # whitespace after the cursor, execute. Otherwise, split the
1190 # whitespace after the cursor, execute. Otherwise, split the
1191 # line with a continuation prompt.
1191 # line with a continuation prompt.
1192 elif not self._executing:
1192 elif not self._executing:
1193 cursor.movePosition(QtGui.QTextCursor.End,
1193 cursor.movePosition(QtGui.QTextCursor.End,
1194 QtGui.QTextCursor.KeepAnchor)
1194 QtGui.QTextCursor.KeepAnchor)
1195 at_end = len(cursor.selectedText().strip()) == 0
1195 at_end = len(cursor.selectedText().strip()) == 0
1196 single_line = (self._get_end_cursor().blockNumber() ==
1196 single_line = (self._get_end_cursor().blockNumber() ==
1197 self._get_prompt_cursor().blockNumber())
1197 self._get_prompt_cursor().blockNumber())
1198 if (at_end or shift_down or single_line) and not ctrl_down:
1198 if (at_end or shift_down or single_line) and not ctrl_down:
1199 self.execute(interactive = not shift_down)
1199 self.execute(interactive = not shift_down)
1200 else:
1200 else:
1201 # Do this inside an edit block for clean undo/redo.
1201 # Do this inside an edit block for clean undo/redo.
1202 cursor.beginEditBlock()
1202 cursor.beginEditBlock()
1203 cursor.setPosition(position)
1203 cursor.setPosition(position)
1204 cursor.insertText('\n')
1204 cursor.insertText('\n')
1205 self._insert_continuation_prompt(cursor)
1205 self._insert_continuation_prompt(cursor)
1206 cursor.endEditBlock()
1206 cursor.endEditBlock()
1207
1207
1208 # Ensure that the whole input buffer is visible.
1208 # Ensure that the whole input buffer is visible.
1209 # FIXME: This will not be usable if the input buffer is
1209 # FIXME: This will not be usable if the input buffer is
1210 # taller than the console widget.
1210 # taller than the console widget.
1211 self._control.moveCursor(QtGui.QTextCursor.End)
1211 self._control.moveCursor(QtGui.QTextCursor.End)
1212 self._control.setTextCursor(cursor)
1212 self._control.setTextCursor(cursor)
1213
1213
1214 #------ Control/Cmd modifier -------------------------------------------
1214 #------ Control/Cmd modifier -------------------------------------------
1215
1215
1216 elif ctrl_down:
1216 elif ctrl_down:
1217 if key == QtCore.Qt.Key_G:
1217 if key == QtCore.Qt.Key_G:
1218 self._keyboard_quit()
1218 self._keyboard_quit()
1219 intercepted = True
1219 intercepted = True
1220
1220
1221 elif key == QtCore.Qt.Key_K:
1221 elif key == QtCore.Qt.Key_K:
1222 if self._in_buffer(position):
1222 if self._in_buffer(position):
1223 cursor.clearSelection()
1223 cursor.clearSelection()
1224 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1224 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1225 QtGui.QTextCursor.KeepAnchor)
1225 QtGui.QTextCursor.KeepAnchor)
1226 if not cursor.hasSelection():
1226 if not cursor.hasSelection():
1227 # Line deletion (remove continuation prompt)
1227 # Line deletion (remove continuation prompt)
1228 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1228 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1229 QtGui.QTextCursor.KeepAnchor)
1229 QtGui.QTextCursor.KeepAnchor)
1230 cursor.movePosition(QtGui.QTextCursor.Right,
1230 cursor.movePosition(QtGui.QTextCursor.Right,
1231 QtGui.QTextCursor.KeepAnchor,
1231 QtGui.QTextCursor.KeepAnchor,
1232 len(self._continuation_prompt))
1232 len(self._continuation_prompt))
1233 self._kill_ring.kill_cursor(cursor)
1233 self._kill_ring.kill_cursor(cursor)
1234 self._set_cursor(cursor)
1234 self._set_cursor(cursor)
1235 intercepted = True
1235 intercepted = True
1236
1236
1237 elif key == QtCore.Qt.Key_L:
1237 elif key == QtCore.Qt.Key_L:
1238 self.prompt_to_top()
1238 self.prompt_to_top()
1239 intercepted = True
1239 intercepted = True
1240
1240
1241 elif key == QtCore.Qt.Key_O:
1241 elif key == QtCore.Qt.Key_O:
1242 if self._page_control and self._page_control.isVisible():
1242 if self._page_control and self._page_control.isVisible():
1243 self._page_control.setFocus()
1243 self._page_control.setFocus()
1244 intercepted = True
1244 intercepted = True
1245
1245
1246 elif key == QtCore.Qt.Key_U:
1246 elif key == QtCore.Qt.Key_U:
1247 if self._in_buffer(position):
1247 if self._in_buffer(position):
1248 cursor.clearSelection()
1248 cursor.clearSelection()
1249 start_line = cursor.blockNumber()
1249 start_line = cursor.blockNumber()
1250 if start_line == self._get_prompt_cursor().blockNumber():
1250 if start_line == self._get_prompt_cursor().blockNumber():
1251 offset = len(self._prompt)
1251 offset = len(self._prompt)
1252 else:
1252 else:
1253 offset = len(self._continuation_prompt)
1253 offset = len(self._continuation_prompt)
1254 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1254 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1255 QtGui.QTextCursor.KeepAnchor)
1255 QtGui.QTextCursor.KeepAnchor)
1256 cursor.movePosition(QtGui.QTextCursor.Right,
1256 cursor.movePosition(QtGui.QTextCursor.Right,
1257 QtGui.QTextCursor.KeepAnchor, offset)
1257 QtGui.QTextCursor.KeepAnchor, offset)
1258 self._kill_ring.kill_cursor(cursor)
1258 self._kill_ring.kill_cursor(cursor)
1259 self._set_cursor(cursor)
1259 self._set_cursor(cursor)
1260 intercepted = True
1260 intercepted = True
1261
1261
1262 elif key == QtCore.Qt.Key_Y:
1262 elif key == QtCore.Qt.Key_Y:
1263 self._keep_cursor_in_buffer()
1263 self._keep_cursor_in_buffer()
1264 self._kill_ring.yank()
1264 self._kill_ring.yank()
1265 intercepted = True
1265 intercepted = True
1266
1266
1267 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1267 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1268 if key == QtCore.Qt.Key_Backspace:
1268 if key == QtCore.Qt.Key_Backspace:
1269 cursor = self._get_word_start_cursor(position)
1269 cursor = self._get_word_start_cursor(position)
1270 else: # key == QtCore.Qt.Key_Delete
1270 else: # key == QtCore.Qt.Key_Delete
1271 cursor = self._get_word_end_cursor(position)
1271 cursor = self._get_word_end_cursor(position)
1272 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1272 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1273 self._kill_ring.kill_cursor(cursor)
1273 self._kill_ring.kill_cursor(cursor)
1274 intercepted = True
1274 intercepted = True
1275
1275
1276 elif key == QtCore.Qt.Key_D:
1276 elif key == QtCore.Qt.Key_D:
1277 if len(self.input_buffer) == 0:
1277 if len(self.input_buffer) == 0:
1278 self.exit_requested.emit(self)
1278 self.exit_requested.emit(self)
1279 else:
1279 else:
1280 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1280 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1281 QtCore.Qt.Key_Delete,
1281 QtCore.Qt.Key_Delete,
1282 QtCore.Qt.NoModifier)
1282 QtCore.Qt.NoModifier)
1283 QtGui.qApp.sendEvent(self._control, new_event)
1283 QtGui.qApp.sendEvent(self._control, new_event)
1284 intercepted = True
1284 intercepted = True
1285
1285
1286 #------ Alt modifier ---------------------------------------------------
1286 #------ Alt modifier ---------------------------------------------------
1287
1287
1288 elif alt_down:
1288 elif alt_down:
1289 if key == QtCore.Qt.Key_B:
1289 if key == QtCore.Qt.Key_B:
1290 self._set_cursor(self._get_word_start_cursor(position))
1290 self._set_cursor(self._get_word_start_cursor(position))
1291 intercepted = True
1291 intercepted = True
1292
1292
1293 elif key == QtCore.Qt.Key_F:
1293 elif key == QtCore.Qt.Key_F:
1294 self._set_cursor(self._get_word_end_cursor(position))
1294 self._set_cursor(self._get_word_end_cursor(position))
1295 intercepted = True
1295 intercepted = True
1296
1296
1297 elif key == QtCore.Qt.Key_Y:
1297 elif key == QtCore.Qt.Key_Y:
1298 self._kill_ring.rotate()
1298 self._kill_ring.rotate()
1299 intercepted = True
1299 intercepted = True
1300
1300
1301 elif key == QtCore.Qt.Key_Backspace:
1301 elif key == QtCore.Qt.Key_Backspace:
1302 cursor = self._get_word_start_cursor(position)
1302 cursor = self._get_word_start_cursor(position)
1303 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1303 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1304 self._kill_ring.kill_cursor(cursor)
1304 self._kill_ring.kill_cursor(cursor)
1305 intercepted = True
1305 intercepted = True
1306
1306
1307 elif key == QtCore.Qt.Key_D:
1307 elif key == QtCore.Qt.Key_D:
1308 cursor = self._get_word_end_cursor(position)
1308 cursor = self._get_word_end_cursor(position)
1309 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1309 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1310 self._kill_ring.kill_cursor(cursor)
1310 self._kill_ring.kill_cursor(cursor)
1311 intercepted = True
1311 intercepted = True
1312
1312
1313 elif key == QtCore.Qt.Key_Delete:
1313 elif key == QtCore.Qt.Key_Delete:
1314 intercepted = True
1314 intercepted = True
1315
1315
1316 elif key == QtCore.Qt.Key_Greater:
1316 elif key == QtCore.Qt.Key_Greater:
1317 self._control.moveCursor(QtGui.QTextCursor.End)
1317 self._control.moveCursor(QtGui.QTextCursor.End)
1318 intercepted = True
1318 intercepted = True
1319
1319
1320 elif key == QtCore.Qt.Key_Less:
1320 elif key == QtCore.Qt.Key_Less:
1321 self._control.setTextCursor(self._get_prompt_cursor())
1321 self._control.setTextCursor(self._get_prompt_cursor())
1322 intercepted = True
1322 intercepted = True
1323
1323
1324 #------ No modifiers ---------------------------------------------------
1324 #------ No modifiers ---------------------------------------------------
1325
1325
1326 else:
1326 else:
1327 if shift_down:
1327 if shift_down:
1328 anchormode = QtGui.QTextCursor.KeepAnchor
1328 anchormode = QtGui.QTextCursor.KeepAnchor
1329 else:
1329 else:
1330 anchormode = QtGui.QTextCursor.MoveAnchor
1330 anchormode = QtGui.QTextCursor.MoveAnchor
1331
1331
1332 if key == QtCore.Qt.Key_Escape:
1332 if key == QtCore.Qt.Key_Escape:
1333 self._keyboard_quit()
1333 self._keyboard_quit()
1334 intercepted = True
1334 intercepted = True
1335
1335
1336 elif key == QtCore.Qt.Key_Up:
1336 elif key == QtCore.Qt.Key_Up:
1337 if self._reading or not self._up_pressed(shift_down):
1337 if self._reading or not self._up_pressed(shift_down):
1338 intercepted = True
1338 intercepted = True
1339 else:
1339 else:
1340 prompt_line = self._get_prompt_cursor().blockNumber()
1340 prompt_line = self._get_prompt_cursor().blockNumber()
1341 intercepted = cursor.blockNumber() <= prompt_line
1341 intercepted = cursor.blockNumber() <= prompt_line
1342
1342
1343 elif key == QtCore.Qt.Key_Down:
1343 elif key == QtCore.Qt.Key_Down:
1344 if self._reading or not self._down_pressed(shift_down):
1344 if self._reading or not self._down_pressed(shift_down):
1345 intercepted = True
1345 intercepted = True
1346 else:
1346 else:
1347 end_line = self._get_end_cursor().blockNumber()
1347 end_line = self._get_end_cursor().blockNumber()
1348 intercepted = cursor.blockNumber() == end_line
1348 intercepted = cursor.blockNumber() == end_line
1349
1349
1350 elif key == QtCore.Qt.Key_Tab:
1350 elif key == QtCore.Qt.Key_Tab:
1351 if not self._reading:
1351 if not self._reading:
1352 if self._tab_pressed():
1352 if self._tab_pressed():
1353 # real tab-key, insert four spaces
1353 # real tab-key, insert four spaces
1354 cursor.insertText(' '*4)
1354 cursor.insertText(' '*4)
1355 intercepted = True
1355 intercepted = True
1356
1356
1357 elif key == QtCore.Qt.Key_Left:
1357 elif key == QtCore.Qt.Key_Left:
1358
1358
1359 # Move to the previous line
1359 # Move to the previous line
1360 line, col = cursor.blockNumber(), cursor.columnNumber()
1360 line, col = cursor.blockNumber(), cursor.columnNumber()
1361 if line > self._get_prompt_cursor().blockNumber() and \
1361 if line > self._get_prompt_cursor().blockNumber() and \
1362 col == len(self._continuation_prompt):
1362 col == len(self._continuation_prompt):
1363 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1363 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1364 mode=anchormode)
1364 mode=anchormode)
1365 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1365 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1366 mode=anchormode)
1366 mode=anchormode)
1367 intercepted = True
1367 intercepted = True
1368
1368
1369 # Regular left movement
1369 # Regular left movement
1370 else:
1370 else:
1371 intercepted = not self._in_buffer(position - 1)
1371 intercepted = not self._in_buffer(position - 1)
1372
1372
1373 elif key == QtCore.Qt.Key_Right:
1373 elif key == QtCore.Qt.Key_Right:
1374 original_block_number = cursor.blockNumber()
1374 original_block_number = cursor.blockNumber()
1375 self._control.moveCursor(QtGui.QTextCursor.Right,
1375 self._control.moveCursor(QtGui.QTextCursor.Right,
1376 mode=anchormode)
1376 mode=anchormode)
1377 if cursor.blockNumber() != original_block_number:
1377 if cursor.blockNumber() != original_block_number:
1378 self._control.moveCursor(QtGui.QTextCursor.Right,
1378 self._control.moveCursor(QtGui.QTextCursor.Right,
1379 n=len(self._continuation_prompt),
1379 n=len(self._continuation_prompt),
1380 mode=anchormode)
1380 mode=anchormode)
1381 intercepted = True
1381 intercepted = True
1382
1382
1383 elif key == QtCore.Qt.Key_Home:
1383 elif key == QtCore.Qt.Key_Home:
1384 start_line = cursor.blockNumber()
1384 start_line = cursor.blockNumber()
1385 if start_line == self._get_prompt_cursor().blockNumber():
1385 if start_line == self._get_prompt_cursor().blockNumber():
1386 start_pos = self._prompt_pos
1386 start_pos = self._prompt_pos
1387 else:
1387 else:
1388 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1388 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1389 QtGui.QTextCursor.KeepAnchor)
1389 QtGui.QTextCursor.KeepAnchor)
1390 start_pos = cursor.position()
1390 start_pos = cursor.position()
1391 start_pos += len(self._continuation_prompt)
1391 start_pos += len(self._continuation_prompt)
1392 cursor.setPosition(position)
1392 cursor.setPosition(position)
1393 if shift_down and self._in_buffer(position):
1393 if shift_down and self._in_buffer(position):
1394 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1394 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1395 else:
1395 else:
1396 cursor.setPosition(start_pos)
1396 cursor.setPosition(start_pos)
1397 self._set_cursor(cursor)
1397 self._set_cursor(cursor)
1398 intercepted = True
1398 intercepted = True
1399
1399
1400 elif key == QtCore.Qt.Key_Backspace:
1400 elif key == QtCore.Qt.Key_Backspace:
1401
1401
1402 # Line deletion (remove continuation prompt)
1402 # Line deletion (remove continuation prompt)
1403 line, col = cursor.blockNumber(), cursor.columnNumber()
1403 line, col = cursor.blockNumber(), cursor.columnNumber()
1404 if not self._reading and \
1404 if not self._reading and \
1405 col == len(self._continuation_prompt) and \
1405 col == len(self._continuation_prompt) and \
1406 line > self._get_prompt_cursor().blockNumber():
1406 line > self._get_prompt_cursor().blockNumber():
1407 cursor.beginEditBlock()
1407 cursor.beginEditBlock()
1408 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1408 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1409 QtGui.QTextCursor.KeepAnchor)
1409 QtGui.QTextCursor.KeepAnchor)
1410 cursor.removeSelectedText()
1410 cursor.removeSelectedText()
1411 cursor.deletePreviousChar()
1411 cursor.deletePreviousChar()
1412 cursor.endEditBlock()
1412 cursor.endEditBlock()
1413 intercepted = True
1413 intercepted = True
1414
1414
1415 # Regular backwards deletion
1415 # Regular backwards deletion
1416 else:
1416 else:
1417 anchor = cursor.anchor()
1417 anchor = cursor.anchor()
1418 if anchor == position:
1418 if anchor == position:
1419 intercepted = not self._in_buffer(position - 1)
1419 intercepted = not self._in_buffer(position - 1)
1420 else:
1420 else:
1421 intercepted = not self._in_buffer(min(anchor, position))
1421 intercepted = not self._in_buffer(min(anchor, position))
1422
1422
1423 elif key == QtCore.Qt.Key_Delete:
1423 elif key == QtCore.Qt.Key_Delete:
1424
1424
1425 # Line deletion (remove continuation prompt)
1425 # Line deletion (remove continuation prompt)
1426 if not self._reading and self._in_buffer(position) and \
1426 if not self._reading and self._in_buffer(position) and \
1427 cursor.atBlockEnd() and not cursor.hasSelection():
1427 cursor.atBlockEnd() and not cursor.hasSelection():
1428 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1428 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1429 QtGui.QTextCursor.KeepAnchor)
1429 QtGui.QTextCursor.KeepAnchor)
1430 cursor.movePosition(QtGui.QTextCursor.Right,
1430 cursor.movePosition(QtGui.QTextCursor.Right,
1431 QtGui.QTextCursor.KeepAnchor,
1431 QtGui.QTextCursor.KeepAnchor,
1432 len(self._continuation_prompt))
1432 len(self._continuation_prompt))
1433 cursor.removeSelectedText()
1433 cursor.removeSelectedText()
1434 intercepted = True
1434 intercepted = True
1435
1435
1436 # Regular forwards deletion:
1436 # Regular forwards deletion:
1437 else:
1437 else:
1438 anchor = cursor.anchor()
1438 anchor = cursor.anchor()
1439 intercepted = (not self._in_buffer(anchor) or
1439 intercepted = (not self._in_buffer(anchor) or
1440 not self._in_buffer(position))
1440 not self._in_buffer(position))
1441
1441
1442 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1442 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1443 # using the keyboard in any part of the buffer. Also, permit scrolling
1443 # using the keyboard in any part of the buffer. Also, permit scrolling
1444 # with Page Up/Down keys. Finally, if we're executing, don't move the
1444 # with Page Up/Down keys. Finally, if we're executing, don't move the
1445 # cursor (if even this made sense, we can't guarantee that the prompt
1445 # cursor (if even this made sense, we can't guarantee that the prompt
1446 # position is still valid due to text truncation).
1446 # position is still valid due to text truncation).
1447 if not (self._control_key_down(event.modifiers(), include_command=True)
1447 if not (self._control_key_down(event.modifiers(), include_command=True)
1448 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1448 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1449 or (self._executing and not self._reading)):
1449 or (self._executing and not self._reading)):
1450 self._keep_cursor_in_buffer()
1450 self._keep_cursor_in_buffer()
1451
1451
1452 return intercepted
1452 return intercepted
1453
1453
1454 def _event_filter_page_keypress(self, event):
1454 def _event_filter_page_keypress(self, event):
1455 """ Filter key events for the paging widget to create console-like
1455 """ Filter key events for the paging widget to create console-like
1456 interface.
1456 interface.
1457 """
1457 """
1458 key = event.key()
1458 key = event.key()
1459 ctrl_down = self._control_key_down(event.modifiers())
1459 ctrl_down = self._control_key_down(event.modifiers())
1460 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1460 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1461
1461
1462 if ctrl_down:
1462 if ctrl_down:
1463 if key == QtCore.Qt.Key_O:
1463 if key == QtCore.Qt.Key_O:
1464 self._control.setFocus()
1464 self._control.setFocus()
1465 intercept = True
1465 intercept = True
1466
1466
1467 elif alt_down:
1467 elif alt_down:
1468 if key == QtCore.Qt.Key_Greater:
1468 if key == QtCore.Qt.Key_Greater:
1469 self._page_control.moveCursor(QtGui.QTextCursor.End)
1469 self._page_control.moveCursor(QtGui.QTextCursor.End)
1470 intercepted = True
1470 intercepted = True
1471
1471
1472 elif key == QtCore.Qt.Key_Less:
1472 elif key == QtCore.Qt.Key_Less:
1473 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1473 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1474 intercepted = True
1474 intercepted = True
1475
1475
1476 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1476 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1477 if self._splitter:
1477 if self._splitter:
1478 self._page_control.hide()
1478 self._page_control.hide()
1479 self._control.setFocus()
1479 self._control.setFocus()
1480 else:
1480 else:
1481 self.layout().setCurrentWidget(self._control)
1481 self.layout().setCurrentWidget(self._control)
1482 # re-enable buffer truncation after paging
1482 # re-enable buffer truncation after paging
1483 self._control.document().setMaximumBlockCount(self.buffer_size)
1483 self._control.document().setMaximumBlockCount(self.buffer_size)
1484 return True
1484 return True
1485
1485
1486 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1486 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1487 QtCore.Qt.Key_Tab):
1487 QtCore.Qt.Key_Tab):
1488 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1488 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1489 QtCore.Qt.Key_PageDown,
1489 QtCore.Qt.Key_PageDown,
1490 QtCore.Qt.NoModifier)
1490 QtCore.Qt.NoModifier)
1491 QtGui.qApp.sendEvent(self._page_control, new_event)
1491 QtGui.qApp.sendEvent(self._page_control, new_event)
1492 return True
1492 return True
1493
1493
1494 elif key == QtCore.Qt.Key_Backspace:
1494 elif key == QtCore.Qt.Key_Backspace:
1495 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1495 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1496 QtCore.Qt.Key_PageUp,
1496 QtCore.Qt.Key_PageUp,
1497 QtCore.Qt.NoModifier)
1497 QtCore.Qt.NoModifier)
1498 QtGui.qApp.sendEvent(self._page_control, new_event)
1498 QtGui.qApp.sendEvent(self._page_control, new_event)
1499 return True
1499 return True
1500
1500
1501 # vi/less -like key bindings
1501 # vi/less -like key bindings
1502 elif key == QtCore.Qt.Key_J:
1502 elif key == QtCore.Qt.Key_J:
1503 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1503 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1504 QtCore.Qt.Key_Down,
1504 QtCore.Qt.Key_Down,
1505 QtCore.Qt.NoModifier)
1505 QtCore.Qt.NoModifier)
1506 QtGui.qApp.sendEvent(self._page_control, new_event)
1506 QtGui.qApp.sendEvent(self._page_control, new_event)
1507 return True
1507 return True
1508
1508
1509 # vi/less -like key bindings
1509 # vi/less -like key bindings
1510 elif key == QtCore.Qt.Key_K:
1510 elif key == QtCore.Qt.Key_K:
1511 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1511 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1512 QtCore.Qt.Key_Up,
1512 QtCore.Qt.Key_Up,
1513 QtCore.Qt.NoModifier)
1513 QtCore.Qt.NoModifier)
1514 QtGui.qApp.sendEvent(self._page_control, new_event)
1514 QtGui.qApp.sendEvent(self._page_control, new_event)
1515 return True
1515 return True
1516
1516
1517 return False
1517 return False
1518
1518
1519 def _on_flush_pending_stream_timer(self):
1519 def _on_flush_pending_stream_timer(self):
1520 """ Flush the pending stream output and change the
1520 """ Flush the pending stream output and change the
1521 prompt position appropriately.
1521 prompt position appropriately.
1522 """
1522 """
1523 cursor = self._control.textCursor()
1523 cursor = self._control.textCursor()
1524 cursor.movePosition(QtGui.QTextCursor.End)
1524 cursor.movePosition(QtGui.QTextCursor.End)
1525 pos = cursor.position()
1525 pos = cursor.position()
1526 self._flush_pending_stream()
1526 self._flush_pending_stream()
1527 cursor.movePosition(QtGui.QTextCursor.End)
1527 cursor.movePosition(QtGui.QTextCursor.End)
1528 diff = cursor.position() - pos
1528 diff = cursor.position() - pos
1529 if diff > 0:
1529 if diff > 0:
1530 self._prompt_pos += diff
1530 self._prompt_pos += diff
1531 self._append_before_prompt_pos += diff
1531 self._append_before_prompt_pos += diff
1532
1532
1533 def _flush_pending_stream(self):
1533 def _flush_pending_stream(self):
1534 """ Flush out pending text into the widget. """
1534 """ Flush out pending text into the widget. """
1535 text = self._pending_insert_text
1535 text = self._pending_insert_text
1536 self._pending_insert_text = []
1536 self._pending_insert_text = []
1537 buffer_size = self._control.document().maximumBlockCount()
1537 buffer_size = self._control.document().maximumBlockCount()
1538 if buffer_size > 0:
1538 if buffer_size > 0:
1539 text = self._get_last_lines_from_list(text, buffer_size)
1539 text = self._get_last_lines_from_list(text, buffer_size)
1540 text = ''.join(text)
1540 text = ''.join(text)
1541 t = time.time()
1541 t = time.time()
1542 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1542 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1543 # Set the flush interval to equal the maximum time to update text.
1543 # Set the flush interval to equal the maximum time to update text.
1544 self._pending_text_flush_interval.setInterval(max(100,
1544 self._pending_text_flush_interval.setInterval(max(100,
1545 (time.time()-t)*1000))
1545 (time.time()-t)*1000))
1546
1546
1547 def _format_as_columns(self, items, separator=' '):
1547 def _format_as_columns(self, items, separator=' '):
1548 """ Transform a list of strings into a single string with columns.
1548 """ Transform a list of strings into a single string with columns.
1549
1549
1550 Parameters
1550 Parameters
1551 ----------
1551 ----------
1552 items : sequence of strings
1552 items : sequence of strings
1553 The strings to process.
1553 The strings to process.
1554
1554
1555 separator : str, optional [default is two spaces]
1555 separator : str, optional [default is two spaces]
1556 The string that separates columns.
1556 The string that separates columns.
1557
1557
1558 Returns
1558 Returns
1559 -------
1559 -------
1560 The formatted string.
1560 The formatted string.
1561 """
1561 """
1562 # Calculate the number of characters available.
1562 # Calculate the number of characters available.
1563 width = self._control.viewport().width()
1563 width = self._control.viewport().width()
1564 char_width = QtGui.QFontMetrics(self.font).width(' ')
1564 char_width = QtGui.QFontMetrics(self.font).width(' ')
1565 displaywidth = max(10, (width / char_width) - 1)
1565 displaywidth = max(10, (width / char_width) - 1)
1566
1566
1567 return columnize(items, separator, displaywidth)
1567 return columnize(items, separator, displaywidth)
1568
1568
1569 def _get_block_plain_text(self, block):
1569 def _get_block_plain_text(self, block):
1570 """ Given a QTextBlock, return its unformatted text.
1570 """ Given a QTextBlock, return its unformatted text.
1571 """
1571 """
1572 cursor = QtGui.QTextCursor(block)
1572 cursor = QtGui.QTextCursor(block)
1573 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1573 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1574 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1574 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1575 QtGui.QTextCursor.KeepAnchor)
1575 QtGui.QTextCursor.KeepAnchor)
1576 return cursor.selection().toPlainText()
1576 return cursor.selection().toPlainText()
1577
1577
1578 def _get_cursor(self):
1578 def _get_cursor(self):
1579 """ Convenience method that returns a cursor for the current position.
1579 """ Convenience method that returns a cursor for the current position.
1580 """
1580 """
1581 return self._control.textCursor()
1581 return self._control.textCursor()
1582
1582
1583 def _get_end_cursor(self):
1583 def _get_end_cursor(self):
1584 """ Convenience method that returns a cursor for the last character.
1584 """ Convenience method that returns a cursor for the last character.
1585 """
1585 """
1586 cursor = self._control.textCursor()
1586 cursor = self._control.textCursor()
1587 cursor.movePosition(QtGui.QTextCursor.End)
1587 cursor.movePosition(QtGui.QTextCursor.End)
1588 return cursor
1588 return cursor
1589
1589
1590 def _get_input_buffer_cursor_column(self):
1590 def _get_input_buffer_cursor_column(self):
1591 """ Returns the column of the cursor in the input buffer, excluding the
1591 """ Returns the column of the cursor in the input buffer, excluding the
1592 contribution by the prompt, or -1 if there is no such column.
1592 contribution by the prompt, or -1 if there is no such column.
1593 """
1593 """
1594 prompt = self._get_input_buffer_cursor_prompt()
1594 prompt = self._get_input_buffer_cursor_prompt()
1595 if prompt is None:
1595 if prompt is None:
1596 return -1
1596 return -1
1597 else:
1597 else:
1598 cursor = self._control.textCursor()
1598 cursor = self._control.textCursor()
1599 return cursor.columnNumber() - len(prompt)
1599 return cursor.columnNumber() - len(prompt)
1600
1600
1601 def _get_input_buffer_cursor_line(self):
1601 def _get_input_buffer_cursor_line(self):
1602 """ Returns the text of the line of the input buffer that contains the
1602 """ Returns the text of the line of the input buffer that contains the
1603 cursor, or None if there is no such line.
1603 cursor, or None if there is no such line.
1604 """
1604 """
1605 prompt = self._get_input_buffer_cursor_prompt()
1605 prompt = self._get_input_buffer_cursor_prompt()
1606 if prompt is None:
1606 if prompt is None:
1607 return None
1607 return None
1608 else:
1608 else:
1609 cursor = self._control.textCursor()
1609 cursor = self._control.textCursor()
1610 text = self._get_block_plain_text(cursor.block())
1610 text = self._get_block_plain_text(cursor.block())
1611 return text[len(prompt):]
1611 return text[len(prompt):]
1612
1612
1613 def _get_input_buffer_cursor_pos(self):
1613 def _get_input_buffer_cursor_pos(self):
1614 """Return the cursor position within the input buffer."""
1614 """Return the cursor position within the input buffer."""
1615 cursor = self._control.textCursor()
1615 cursor = self._control.textCursor()
1616 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
1616 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
1617 input_buffer = cursor.selection().toPlainText()
1617 input_buffer = cursor.selection().toPlainText()
1618
1618
1619 # Don't count continuation prompts
1619 # Don't count continuation prompts
1620 return len(input_buffer.replace('\n' + self._continuation_prompt, '\n'))
1620 return len(input_buffer.replace('\n' + self._continuation_prompt, '\n'))
1621
1621
1622 def _get_input_buffer_cursor_prompt(self):
1622 def _get_input_buffer_cursor_prompt(self):
1623 """ Returns the (plain text) prompt for line of the input buffer that
1623 """ Returns the (plain text) prompt for line of the input buffer that
1624 contains the cursor, or None if there is no such line.
1624 contains the cursor, or None if there is no such line.
1625 """
1625 """
1626 if self._executing:
1626 if self._executing:
1627 return None
1627 return None
1628 cursor = self._control.textCursor()
1628 cursor = self._control.textCursor()
1629 if cursor.position() >= self._prompt_pos:
1629 if cursor.position() >= self._prompt_pos:
1630 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1630 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1631 return self._prompt
1631 return self._prompt
1632 else:
1632 else:
1633 return self._continuation_prompt
1633 return self._continuation_prompt
1634 else:
1634 else:
1635 return None
1635 return None
1636
1636
1637 def _get_last_lines(self, text, num_lines, return_count=False):
1637 def _get_last_lines(self, text, num_lines, return_count=False):
1638 """ Return last specified number of lines of text (like `tail -n`).
1638 """ Return last specified number of lines of text (like `tail -n`).
1639 If return_count is True, returns a tuple of clipped text and the
1639 If return_count is True, returns a tuple of clipped text and the
1640 number of lines in the clipped text.
1640 number of lines in the clipped text.
1641 """
1641 """
1642 pos = len(text)
1642 pos = len(text)
1643 if pos < num_lines:
1643 if pos < num_lines:
1644 if return_count:
1644 if return_count:
1645 return text, text.count('\n') if return_count else text
1645 return text, text.count('\n') if return_count else text
1646 else:
1646 else:
1647 return text
1647 return text
1648 i = 0
1648 i = 0
1649 while i < num_lines:
1649 while i < num_lines:
1650 pos = text.rfind('\n', None, pos)
1650 pos = text.rfind('\n', None, pos)
1651 if pos == -1:
1651 if pos == -1:
1652 pos = None
1652 pos = None
1653 break
1653 break
1654 i += 1
1654 i += 1
1655 if return_count:
1655 if return_count:
1656 return text[pos:], i
1656 return text[pos:], i
1657 else:
1657 else:
1658 return text[pos:]
1658 return text[pos:]
1659
1659
1660 def _get_last_lines_from_list(self, text_list, num_lines):
1660 def _get_last_lines_from_list(self, text_list, num_lines):
1661 """ Return the list of text clipped to last specified lines.
1661 """ Return the list of text clipped to last specified lines.
1662 """
1662 """
1663 ret = []
1663 ret = []
1664 lines_pending = num_lines
1664 lines_pending = num_lines
1665 for text in reversed(text_list):
1665 for text in reversed(text_list):
1666 text, lines_added = self._get_last_lines(text, lines_pending,
1666 text, lines_added = self._get_last_lines(text, lines_pending,
1667 return_count=True)
1667 return_count=True)
1668 ret.append(text)
1668 ret.append(text)
1669 lines_pending -= lines_added
1669 lines_pending -= lines_added
1670 if lines_pending <= 0:
1670 if lines_pending <= 0:
1671 break
1671 break
1672 return ret[::-1]
1672 return ret[::-1]
1673
1673
1674 def _get_prompt_cursor(self):
1674 def _get_prompt_cursor(self):
1675 """ Convenience method that returns a cursor for the prompt position.
1675 """ Convenience method that returns a cursor for the prompt position.
1676 """
1676 """
1677 cursor = self._control.textCursor()
1677 cursor = self._control.textCursor()
1678 cursor.setPosition(self._prompt_pos)
1678 cursor.setPosition(self._prompt_pos)
1679 return cursor
1679 return cursor
1680
1680
1681 def _get_selection_cursor(self, start, end):
1681 def _get_selection_cursor(self, start, end):
1682 """ Convenience method that returns a cursor with text selected between
1682 """ Convenience method that returns a cursor with text selected between
1683 the positions 'start' and 'end'.
1683 the positions 'start' and 'end'.
1684 """
1684 """
1685 cursor = self._control.textCursor()
1685 cursor = self._control.textCursor()
1686 cursor.setPosition(start)
1686 cursor.setPosition(start)
1687 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1687 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1688 return cursor
1688 return cursor
1689
1689
1690 def _get_word_start_cursor(self, position):
1690 def _get_word_start_cursor(self, position):
1691 """ Find the start of the word to the left the given position. If a
1691 """ Find the start of the word to the left the given position. If a
1692 sequence of non-word characters precedes the first word, skip over
1692 sequence of non-word characters precedes the first word, skip over
1693 them. (This emulates the behavior of bash, emacs, etc.)
1693 them. (This emulates the behavior of bash, emacs, etc.)
1694 """
1694 """
1695 document = self._control.document()
1695 document = self._control.document()
1696 position -= 1
1696 position -= 1
1697 while position >= self._prompt_pos and \
1697 while position >= self._prompt_pos and \
1698 not is_letter_or_number(document.characterAt(position)):
1698 not is_letter_or_number(document.characterAt(position)):
1699 position -= 1
1699 position -= 1
1700 while position >= self._prompt_pos and \
1700 while position >= self._prompt_pos and \
1701 is_letter_or_number(document.characterAt(position)):
1701 is_letter_or_number(document.characterAt(position)):
1702 position -= 1
1702 position -= 1
1703 cursor = self._control.textCursor()
1703 cursor = self._control.textCursor()
1704 cursor.setPosition(position + 1)
1704 cursor.setPosition(position + 1)
1705 return cursor
1705 return cursor
1706
1706
1707 def _get_word_end_cursor(self, position):
1707 def _get_word_end_cursor(self, position):
1708 """ Find the end of the word to the right the given position. If a
1708 """ Find the end of the word to the right the given position. If a
1709 sequence of non-word characters precedes the first word, skip over
1709 sequence of non-word characters precedes the first word, skip over
1710 them. (This emulates the behavior of bash, emacs, etc.)
1710 them. (This emulates the behavior of bash, emacs, etc.)
1711 """
1711 """
1712 document = self._control.document()
1712 document = self._control.document()
1713 end = self._get_end_cursor().position()
1713 end = self._get_end_cursor().position()
1714 while position < end and \
1714 while position < end and \
1715 not is_letter_or_number(document.characterAt(position)):
1715 not is_letter_or_number(document.characterAt(position)):
1716 position += 1
1716 position += 1
1717 while position < end and \
1717 while position < end and \
1718 is_letter_or_number(document.characterAt(position)):
1718 is_letter_or_number(document.characterAt(position)):
1719 position += 1
1719 position += 1
1720 cursor = self._control.textCursor()
1720 cursor = self._control.textCursor()
1721 cursor.setPosition(position)
1721 cursor.setPosition(position)
1722 return cursor
1722 return cursor
1723
1723
1724 def _insert_continuation_prompt(self, cursor):
1724 def _insert_continuation_prompt(self, cursor):
1725 """ Inserts new continuation prompt using the specified cursor.
1725 """ Inserts new continuation prompt using the specified cursor.
1726 """
1726 """
1727 if self._continuation_prompt_html is None:
1727 if self._continuation_prompt_html is None:
1728 self._insert_plain_text(cursor, self._continuation_prompt)
1728 self._insert_plain_text(cursor, self._continuation_prompt)
1729 else:
1729 else:
1730 self._continuation_prompt = self._insert_html_fetching_plain_text(
1730 self._continuation_prompt = self._insert_html_fetching_plain_text(
1731 cursor, self._continuation_prompt_html)
1731 cursor, self._continuation_prompt_html)
1732
1732
1733 def _insert_block(self, cursor, block_format=None):
1733 def _insert_block(self, cursor, block_format=None):
1734 """ Inserts an empty QTextBlock using the specified cursor.
1734 """ Inserts an empty QTextBlock using the specified cursor.
1735 """
1735 """
1736 if block_format is None:
1736 if block_format is None:
1737 block_format = QtGui.QTextBlockFormat()
1737 block_format = QtGui.QTextBlockFormat()
1738 cursor.insertBlock(block_format)
1738 cursor.insertBlock(block_format)
1739
1739
1740 def _insert_html(self, cursor, html):
1740 def _insert_html(self, cursor, html):
1741 """ Inserts HTML using the specified cursor in such a way that future
1741 """ Inserts HTML using the specified cursor in such a way that future
1742 formatting is unaffected.
1742 formatting is unaffected.
1743 """
1743 """
1744 cursor.beginEditBlock()
1744 cursor.beginEditBlock()
1745 cursor.insertHtml(html)
1745 cursor.insertHtml(html)
1746
1746
1747 # After inserting HTML, the text document "remembers" it's in "html
1747 # After inserting HTML, the text document "remembers" it's in "html
1748 # mode", which means that subsequent calls adding plain text will result
1748 # mode", which means that subsequent calls adding plain text will result
1749 # in unwanted formatting, lost tab characters, etc. The following code
1749 # in unwanted formatting, lost tab characters, etc. The following code
1750 # hacks around this behavior, which I consider to be a bug in Qt, by
1750 # hacks around this behavior, which I consider to be a bug in Qt, by
1751 # (crudely) resetting the document's style state.
1751 # (crudely) resetting the document's style state.
1752 cursor.movePosition(QtGui.QTextCursor.Left,
1752 cursor.movePosition(QtGui.QTextCursor.Left,
1753 QtGui.QTextCursor.KeepAnchor)
1753 QtGui.QTextCursor.KeepAnchor)
1754 if cursor.selection().toPlainText() == ' ':
1754 if cursor.selection().toPlainText() == ' ':
1755 cursor.removeSelectedText()
1755 cursor.removeSelectedText()
1756 else:
1756 else:
1757 cursor.movePosition(QtGui.QTextCursor.Right)
1757 cursor.movePosition(QtGui.QTextCursor.Right)
1758 cursor.insertText(' ', QtGui.QTextCharFormat())
1758 cursor.insertText(' ', QtGui.QTextCharFormat())
1759 cursor.endEditBlock()
1759 cursor.endEditBlock()
1760
1760
1761 def _insert_html_fetching_plain_text(self, cursor, html):
1761 def _insert_html_fetching_plain_text(self, cursor, html):
1762 """ Inserts HTML using the specified cursor, then returns its plain text
1762 """ Inserts HTML using the specified cursor, then returns its plain text
1763 version.
1763 version.
1764 """
1764 """
1765 cursor.beginEditBlock()
1765 cursor.beginEditBlock()
1766 cursor.removeSelectedText()
1766 cursor.removeSelectedText()
1767
1767
1768 start = cursor.position()
1768 start = cursor.position()
1769 self._insert_html(cursor, html)
1769 self._insert_html(cursor, html)
1770 end = cursor.position()
1770 end = cursor.position()
1771 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1771 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1772 text = cursor.selection().toPlainText()
1772 text = cursor.selection().toPlainText()
1773
1773
1774 cursor.setPosition(end)
1774 cursor.setPosition(end)
1775 cursor.endEditBlock()
1775 cursor.endEditBlock()
1776 return text
1776 return text
1777
1777
1778 def _insert_plain_text(self, cursor, text, flush=False):
1778 def _insert_plain_text(self, cursor, text, flush=False):
1779 """ Inserts plain text using the specified cursor, processing ANSI codes
1779 """ Inserts plain text using the specified cursor, processing ANSI codes
1780 if enabled.
1780 if enabled.
1781 """
1781 """
1782 # maximumBlockCount() can be different from self.buffer_size in
1782 # maximumBlockCount() can be different from self.buffer_size in
1783 # case input prompt is active.
1783 # case input prompt is active.
1784 buffer_size = self._control.document().maximumBlockCount()
1784 buffer_size = self._control.document().maximumBlockCount()
1785
1785
1786 if (self._executing and not flush and
1786 if (self._executing and not flush and
1787 self._pending_text_flush_interval.isActive() and
1787 self._pending_text_flush_interval.isActive() and
1788 cursor.position() == self._get_end_cursor().position()):
1788 cursor.position() == self._get_end_cursor().position()):
1789 # Queue the text to insert in case it is being inserted at end
1789 # Queue the text to insert in case it is being inserted at end
1790 self._pending_insert_text.append(text)
1790 self._pending_insert_text.append(text)
1791 if buffer_size > 0:
1791 if buffer_size > 0:
1792 self._pending_insert_text = self._get_last_lines_from_list(
1792 self._pending_insert_text = self._get_last_lines_from_list(
1793 self._pending_insert_text, buffer_size)
1793 self._pending_insert_text, buffer_size)
1794 return
1794 return
1795
1795
1796 if self._executing and not self._pending_text_flush_interval.isActive():
1796 if self._executing and not self._pending_text_flush_interval.isActive():
1797 self._pending_text_flush_interval.start()
1797 self._pending_text_flush_interval.start()
1798
1798
1799 # Clip the text to last `buffer_size` lines.
1799 # Clip the text to last `buffer_size` lines.
1800 if buffer_size > 0:
1800 if buffer_size > 0:
1801 text = self._get_last_lines(text, buffer_size)
1801 text = self._get_last_lines(text, buffer_size)
1802
1802
1803 cursor.beginEditBlock()
1803 cursor.beginEditBlock()
1804 if self.ansi_codes:
1804 if self.ansi_codes:
1805 for substring in self._ansi_processor.split_string(text):
1805 for substring in self._ansi_processor.split_string(text):
1806 for act in self._ansi_processor.actions:
1806 for act in self._ansi_processor.actions:
1807
1807
1808 # Unlike real terminal emulators, we don't distinguish
1808 # Unlike real terminal emulators, we don't distinguish
1809 # between the screen and the scrollback buffer. A screen
1809 # between the screen and the scrollback buffer. A screen
1810 # erase request clears everything.
1810 # erase request clears everything.
1811 if act.action == 'erase' and act.area == 'screen':
1811 if act.action == 'erase' and act.area == 'screen':
1812 cursor.select(QtGui.QTextCursor.Document)
1812 cursor.select(QtGui.QTextCursor.Document)
1813 cursor.removeSelectedText()
1813 cursor.removeSelectedText()
1814
1814
1815 # Simulate a form feed by scrolling just past the last line.
1815 # Simulate a form feed by scrolling just past the last line.
1816 elif act.action == 'scroll' and act.unit == 'page':
1816 elif act.action == 'scroll' and act.unit == 'page':
1817 cursor.insertText('\n')
1817 cursor.insertText('\n')
1818 cursor.endEditBlock()
1818 cursor.endEditBlock()
1819 self._set_top_cursor(cursor)
1819 self._set_top_cursor(cursor)
1820 cursor.joinPreviousEditBlock()
1820 cursor.joinPreviousEditBlock()
1821 cursor.deletePreviousChar()
1821 cursor.deletePreviousChar()
1822
1822
1823 elif act.action == 'carriage-return':
1823 elif act.action == 'carriage-return':
1824 cursor.movePosition(
1824 cursor.movePosition(
1825 cursor.StartOfLine, cursor.KeepAnchor)
1825 cursor.StartOfLine, cursor.KeepAnchor)
1826
1826
1827 elif act.action == 'beep':
1827 elif act.action == 'beep':
1828 QtGui.qApp.beep()
1828 QtGui.qApp.beep()
1829
1829
1830 elif act.action == 'backspace':
1830 elif act.action == 'backspace':
1831 if not cursor.atBlockStart():
1831 if not cursor.atBlockStart():
1832 cursor.movePosition(
1832 cursor.movePosition(
1833 cursor.PreviousCharacter, cursor.KeepAnchor)
1833 cursor.PreviousCharacter, cursor.KeepAnchor)
1834
1834
1835 elif act.action == 'newline':
1835 elif act.action == 'newline':
1836 cursor.movePosition(cursor.EndOfLine)
1836 cursor.movePosition(cursor.EndOfLine)
1837
1837
1838 format = self._ansi_processor.get_format()
1838 format = self._ansi_processor.get_format()
1839
1839
1840 selection = cursor.selectedText()
1840 selection = cursor.selectedText()
1841 if len(selection) == 0:
1841 if len(selection) == 0:
1842 cursor.insertText(substring, format)
1842 cursor.insertText(substring, format)
1843 elif substring is not None:
1843 elif substring is not None:
1844 # BS and CR are treated as a change in print
1844 # BS and CR are treated as a change in print
1845 # position, rather than a backwards character
1845 # position, rather than a backwards character
1846 # deletion for output equivalence with (I)Python
1846 # deletion for output equivalence with (I)Python
1847 # terminal.
1847 # terminal.
1848 if len(substring) >= len(selection):
1848 if len(substring) >= len(selection):
1849 cursor.insertText(substring, format)
1849 cursor.insertText(substring, format)
1850 else:
1850 else:
1851 old_text = selection[len(substring):]
1851 old_text = selection[len(substring):]
1852 cursor.insertText(substring + old_text, format)
1852 cursor.insertText(substring + old_text, format)
1853 cursor.movePosition(cursor.PreviousCharacter,
1853 cursor.movePosition(cursor.PreviousCharacter,
1854 cursor.KeepAnchor, len(old_text))
1854 cursor.KeepAnchor, len(old_text))
1855 else:
1855 else:
1856 cursor.insertText(text)
1856 cursor.insertText(text)
1857 cursor.endEditBlock()
1857 cursor.endEditBlock()
1858
1858
1859 def _insert_plain_text_into_buffer(self, cursor, text):
1859 def _insert_plain_text_into_buffer(self, cursor, text):
1860 """ Inserts text into the input buffer using the specified cursor (which
1860 """ Inserts text into the input buffer using the specified cursor (which
1861 must be in the input buffer), ensuring that continuation prompts are
1861 must be in the input buffer), ensuring that continuation prompts are
1862 inserted as necessary.
1862 inserted as necessary.
1863 """
1863 """
1864 lines = text.splitlines(True)
1864 lines = text.splitlines(True)
1865 if lines:
1865 if lines:
1866 if lines[-1].endswith('\n'):
1867 # If the text ends with a newline, add a blank line so a new
1868 # continuation prompt is produced.
1869 lines.append('')
1866 cursor.beginEditBlock()
1870 cursor.beginEditBlock()
1867 cursor.insertText(lines[0])
1871 cursor.insertText(lines[0])
1868 for line in lines[1:]:
1872 for line in lines[1:]:
1869 if self._continuation_prompt_html is None:
1873 if self._continuation_prompt_html is None:
1870 cursor.insertText(self._continuation_prompt)
1874 cursor.insertText(self._continuation_prompt)
1871 else:
1875 else:
1872 self._continuation_prompt = \
1876 self._continuation_prompt = \
1873 self._insert_html_fetching_plain_text(
1877 self._insert_html_fetching_plain_text(
1874 cursor, self._continuation_prompt_html)
1878 cursor, self._continuation_prompt_html)
1875 cursor.insertText(line)
1879 cursor.insertText(line)
1876 cursor.endEditBlock()
1880 cursor.endEditBlock()
1877
1881
1878 def _in_buffer(self, position=None):
1882 def _in_buffer(self, position=None):
1879 """ Returns whether the current cursor (or, if specified, a position) is
1883 """ Returns whether the current cursor (or, if specified, a position) is
1880 inside the editing region.
1884 inside the editing region.
1881 """
1885 """
1882 cursor = self._control.textCursor()
1886 cursor = self._control.textCursor()
1883 if position is None:
1887 if position is None:
1884 position = cursor.position()
1888 position = cursor.position()
1885 else:
1889 else:
1886 cursor.setPosition(position)
1890 cursor.setPosition(position)
1887 line = cursor.blockNumber()
1891 line = cursor.blockNumber()
1888 prompt_line = self._get_prompt_cursor().blockNumber()
1892 prompt_line = self._get_prompt_cursor().blockNumber()
1889 if line == prompt_line:
1893 if line == prompt_line:
1890 return position >= self._prompt_pos
1894 return position >= self._prompt_pos
1891 elif line > prompt_line:
1895 elif line > prompt_line:
1892 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1896 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1893 prompt_pos = cursor.position() + len(self._continuation_prompt)
1897 prompt_pos = cursor.position() + len(self._continuation_prompt)
1894 return position >= prompt_pos
1898 return position >= prompt_pos
1895 return False
1899 return False
1896
1900
1897 def _keep_cursor_in_buffer(self):
1901 def _keep_cursor_in_buffer(self):
1898 """ Ensures that the cursor is inside the editing region. Returns
1902 """ Ensures that the cursor is inside the editing region. Returns
1899 whether the cursor was moved.
1903 whether the cursor was moved.
1900 """
1904 """
1901 moved = not self._in_buffer()
1905 moved = not self._in_buffer()
1902 if moved:
1906 if moved:
1903 cursor = self._control.textCursor()
1907 cursor = self._control.textCursor()
1904 cursor.movePosition(QtGui.QTextCursor.End)
1908 cursor.movePosition(QtGui.QTextCursor.End)
1905 self._control.setTextCursor(cursor)
1909 self._control.setTextCursor(cursor)
1906 return moved
1910 return moved
1907
1911
1908 def _keyboard_quit(self):
1912 def _keyboard_quit(self):
1909 """ Cancels the current editing task ala Ctrl-G in Emacs.
1913 """ Cancels the current editing task ala Ctrl-G in Emacs.
1910 """
1914 """
1911 if self._temp_buffer_filled :
1915 if self._temp_buffer_filled :
1912 self._cancel_completion()
1916 self._cancel_completion()
1913 self._clear_temporary_buffer()
1917 self._clear_temporary_buffer()
1914 else:
1918 else:
1915 self.input_buffer = ''
1919 self.input_buffer = ''
1916
1920
1917 def _page(self, text, html=False):
1921 def _page(self, text, html=False):
1918 """ Displays text using the pager if it exceeds the height of the
1922 """ Displays text using the pager if it exceeds the height of the
1919 viewport.
1923 viewport.
1920
1924
1921 Parameters
1925 Parameters
1922 ----------
1926 ----------
1923 html : bool, optional (default False)
1927 html : bool, optional (default False)
1924 If set, the text will be interpreted as HTML instead of plain text.
1928 If set, the text will be interpreted as HTML instead of plain text.
1925 """
1929 """
1926 line_height = QtGui.QFontMetrics(self.font).height()
1930 line_height = QtGui.QFontMetrics(self.font).height()
1927 minlines = self._control.viewport().height() / line_height
1931 minlines = self._control.viewport().height() / line_height
1928 if self.paging != 'none' and \
1932 if self.paging != 'none' and \
1929 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1933 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1930 if self.paging == 'custom':
1934 if self.paging == 'custom':
1931 self.custom_page_requested.emit(text)
1935 self.custom_page_requested.emit(text)
1932 else:
1936 else:
1933 # disable buffer truncation during paging
1937 # disable buffer truncation during paging
1934 self._control.document().setMaximumBlockCount(0)
1938 self._control.document().setMaximumBlockCount(0)
1935 self._page_control.clear()
1939 self._page_control.clear()
1936 cursor = self._page_control.textCursor()
1940 cursor = self._page_control.textCursor()
1937 if html:
1941 if html:
1938 self._insert_html(cursor, text)
1942 self._insert_html(cursor, text)
1939 else:
1943 else:
1940 self._insert_plain_text(cursor, text)
1944 self._insert_plain_text(cursor, text)
1941 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1945 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1942
1946
1943 self._page_control.viewport().resize(self._control.size())
1947 self._page_control.viewport().resize(self._control.size())
1944 if self._splitter:
1948 if self._splitter:
1945 self._page_control.show()
1949 self._page_control.show()
1946 self._page_control.setFocus()
1950 self._page_control.setFocus()
1947 else:
1951 else:
1948 self.layout().setCurrentWidget(self._page_control)
1952 self.layout().setCurrentWidget(self._page_control)
1949 elif html:
1953 elif html:
1950 self._append_html(text)
1954 self._append_html(text)
1951 else:
1955 else:
1952 self._append_plain_text(text)
1956 self._append_plain_text(text)
1953
1957
1954 def _set_paging(self, paging):
1958 def _set_paging(self, paging):
1955 """
1959 """
1956 Change the pager to `paging` style.
1960 Change the pager to `paging` style.
1957
1961
1958 Parameters
1962 Parameters
1959 ----------
1963 ----------
1960 paging : string
1964 paging : string
1961 Either "hsplit", "vsplit", or "inside"
1965 Either "hsplit", "vsplit", or "inside"
1962 """
1966 """
1963 if self._splitter is None:
1967 if self._splitter is None:
1964 raise NotImplementedError("""can only switch if --paging=hsplit or
1968 raise NotImplementedError("""can only switch if --paging=hsplit or
1965 --paging=vsplit is used.""")
1969 --paging=vsplit is used.""")
1966 if paging == 'hsplit':
1970 if paging == 'hsplit':
1967 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1971 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1968 elif paging == 'vsplit':
1972 elif paging == 'vsplit':
1969 self._splitter.setOrientation(QtCore.Qt.Vertical)
1973 self._splitter.setOrientation(QtCore.Qt.Vertical)
1970 elif paging == 'inside':
1974 elif paging == 'inside':
1971 raise NotImplementedError("""switching to 'inside' paging not
1975 raise NotImplementedError("""switching to 'inside' paging not
1972 supported yet.""")
1976 supported yet.""")
1973 else:
1977 else:
1974 raise ValueError("unknown paging method '%s'" % paging)
1978 raise ValueError("unknown paging method '%s'" % paging)
1975 self.paging = paging
1979 self.paging = paging
1976
1980
1977 def _prompt_finished(self):
1981 def _prompt_finished(self):
1978 """ Called immediately after a prompt is finished, i.e. when some input
1982 """ Called immediately after a prompt is finished, i.e. when some input
1979 will be processed and a new prompt displayed.
1983 will be processed and a new prompt displayed.
1980 """
1984 """
1981 self._control.setReadOnly(True)
1985 self._control.setReadOnly(True)
1982 self._prompt_finished_hook()
1986 self._prompt_finished_hook()
1983
1987
1984 def _prompt_started(self):
1988 def _prompt_started(self):
1985 """ Called immediately after a new prompt is displayed.
1989 """ Called immediately after a new prompt is displayed.
1986 """
1990 """
1987 # Temporarily disable the maximum block count to permit undo/redo and
1991 # Temporarily disable the maximum block count to permit undo/redo and
1988 # to ensure that the prompt position does not change due to truncation.
1992 # to ensure that the prompt position does not change due to truncation.
1989 self._control.document().setMaximumBlockCount(0)
1993 self._control.document().setMaximumBlockCount(0)
1990 self._control.setUndoRedoEnabled(True)
1994 self._control.setUndoRedoEnabled(True)
1991
1995
1992 # Work around bug in QPlainTextEdit: input method is not re-enabled
1996 # Work around bug in QPlainTextEdit: input method is not re-enabled
1993 # when read-only is disabled.
1997 # when read-only is disabled.
1994 self._control.setReadOnly(False)
1998 self._control.setReadOnly(False)
1995 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1999 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1996
2000
1997 if not self._reading:
2001 if not self._reading:
1998 self._executing = False
2002 self._executing = False
1999 self._prompt_started_hook()
2003 self._prompt_started_hook()
2000
2004
2001 # If the input buffer has changed while executing, load it.
2005 # If the input buffer has changed while executing, load it.
2002 if self._input_buffer_pending:
2006 if self._input_buffer_pending:
2003 self.input_buffer = self._input_buffer_pending
2007 self.input_buffer = self._input_buffer_pending
2004 self._input_buffer_pending = ''
2008 self._input_buffer_pending = ''
2005
2009
2006 self._control.moveCursor(QtGui.QTextCursor.End)
2010 self._control.moveCursor(QtGui.QTextCursor.End)
2007
2011
2008 def _readline(self, prompt='', callback=None):
2012 def _readline(self, prompt='', callback=None):
2009 """ Reads one line of input from the user.
2013 """ Reads one line of input from the user.
2010
2014
2011 Parameters
2015 Parameters
2012 ----------
2016 ----------
2013 prompt : str, optional
2017 prompt : str, optional
2014 The prompt to print before reading the line.
2018 The prompt to print before reading the line.
2015
2019
2016 callback : callable, optional
2020 callback : callable, optional
2017 A callback to execute with the read line. If not specified, input is
2021 A callback to execute with the read line. If not specified, input is
2018 read *synchronously* and this method does not return until it has
2022 read *synchronously* and this method does not return until it has
2019 been read.
2023 been read.
2020
2024
2021 Returns
2025 Returns
2022 -------
2026 -------
2023 If a callback is specified, returns nothing. Otherwise, returns the
2027 If a callback is specified, returns nothing. Otherwise, returns the
2024 input string with the trailing newline stripped.
2028 input string with the trailing newline stripped.
2025 """
2029 """
2026 if self._reading:
2030 if self._reading:
2027 raise RuntimeError('Cannot read a line. Widget is already reading.')
2031 raise RuntimeError('Cannot read a line. Widget is already reading.')
2028
2032
2029 if not callback and not self.isVisible():
2033 if not callback and not self.isVisible():
2030 # If the user cannot see the widget, this function cannot return.
2034 # If the user cannot see the widget, this function cannot return.
2031 raise RuntimeError('Cannot synchronously read a line if the widget '
2035 raise RuntimeError('Cannot synchronously read a line if the widget '
2032 'is not visible!')
2036 'is not visible!')
2033
2037
2034 self._reading = True
2038 self._reading = True
2035 self._show_prompt(prompt, newline=False)
2039 self._show_prompt(prompt, newline=False)
2036
2040
2037 if callback is None:
2041 if callback is None:
2038 self._reading_callback = None
2042 self._reading_callback = None
2039 while self._reading:
2043 while self._reading:
2040 QtCore.QCoreApplication.processEvents()
2044 QtCore.QCoreApplication.processEvents()
2041 return self._get_input_buffer(force=True).rstrip('\n')
2045 return self._get_input_buffer(force=True).rstrip('\n')
2042
2046
2043 else:
2047 else:
2044 self._reading_callback = lambda: \
2048 self._reading_callback = lambda: \
2045 callback(self._get_input_buffer(force=True).rstrip('\n'))
2049 callback(self._get_input_buffer(force=True).rstrip('\n'))
2046
2050
2047 def _set_continuation_prompt(self, prompt, html=False):
2051 def _set_continuation_prompt(self, prompt, html=False):
2048 """ Sets the continuation prompt.
2052 """ Sets the continuation prompt.
2049
2053
2050 Parameters
2054 Parameters
2051 ----------
2055 ----------
2052 prompt : str
2056 prompt : str
2053 The prompt to show when more input is needed.
2057 The prompt to show when more input is needed.
2054
2058
2055 html : bool, optional (default False)
2059 html : bool, optional (default False)
2056 If set, the prompt will be inserted as formatted HTML. Otherwise,
2060 If set, the prompt will be inserted as formatted HTML. Otherwise,
2057 the prompt will be treated as plain text, though ANSI color codes
2061 the prompt will be treated as plain text, though ANSI color codes
2058 will be handled.
2062 will be handled.
2059 """
2063 """
2060 if html:
2064 if html:
2061 self._continuation_prompt_html = prompt
2065 self._continuation_prompt_html = prompt
2062 else:
2066 else:
2063 self._continuation_prompt = prompt
2067 self._continuation_prompt = prompt
2064 self._continuation_prompt_html = None
2068 self._continuation_prompt_html = None
2065
2069
2066 def _set_cursor(self, cursor):
2070 def _set_cursor(self, cursor):
2067 """ Convenience method to set the current cursor.
2071 """ Convenience method to set the current cursor.
2068 """
2072 """
2069 self._control.setTextCursor(cursor)
2073 self._control.setTextCursor(cursor)
2070
2074
2071 def _set_top_cursor(self, cursor):
2075 def _set_top_cursor(self, cursor):
2072 """ Scrolls the viewport so that the specified cursor is at the top.
2076 """ Scrolls the viewport so that the specified cursor is at the top.
2073 """
2077 """
2074 scrollbar = self._control.verticalScrollBar()
2078 scrollbar = self._control.verticalScrollBar()
2075 scrollbar.setValue(scrollbar.maximum())
2079 scrollbar.setValue(scrollbar.maximum())
2076 original_cursor = self._control.textCursor()
2080 original_cursor = self._control.textCursor()
2077 self._control.setTextCursor(cursor)
2081 self._control.setTextCursor(cursor)
2078 self._control.ensureCursorVisible()
2082 self._control.ensureCursorVisible()
2079 self._control.setTextCursor(original_cursor)
2083 self._control.setTextCursor(original_cursor)
2080
2084
2081 def _show_prompt(self, prompt=None, html=False, newline=True):
2085 def _show_prompt(self, prompt=None, html=False, newline=True):
2082 """ Writes a new prompt at the end of the buffer.
2086 """ Writes a new prompt at the end of the buffer.
2083
2087
2084 Parameters
2088 Parameters
2085 ----------
2089 ----------
2086 prompt : str, optional
2090 prompt : str, optional
2087 The prompt to show. If not specified, the previous prompt is used.
2091 The prompt to show. If not specified, the previous prompt is used.
2088
2092
2089 html : bool, optional (default False)
2093 html : bool, optional (default False)
2090 Only relevant when a prompt is specified. If set, the prompt will
2094 Only relevant when a prompt is specified. If set, the prompt will
2091 be inserted as formatted HTML. Otherwise, the prompt will be treated
2095 be inserted as formatted HTML. Otherwise, the prompt will be treated
2092 as plain text, though ANSI color codes will be handled.
2096 as plain text, though ANSI color codes will be handled.
2093
2097
2094 newline : bool, optional (default True)
2098 newline : bool, optional (default True)
2095 If set, a new line will be written before showing the prompt if
2099 If set, a new line will be written before showing the prompt if
2096 there is not already a newline at the end of the buffer.
2100 there is not already a newline at the end of the buffer.
2097 """
2101 """
2098 # Save the current end position to support _append*(before_prompt=True).
2102 # Save the current end position to support _append*(before_prompt=True).
2099 self._flush_pending_stream()
2103 self._flush_pending_stream()
2100 cursor = self._get_end_cursor()
2104 cursor = self._get_end_cursor()
2101 self._append_before_prompt_pos = cursor.position()
2105 self._append_before_prompt_pos = cursor.position()
2102
2106
2103 # Insert a preliminary newline, if necessary.
2107 # Insert a preliminary newline, if necessary.
2104 if newline and cursor.position() > 0:
2108 if newline and cursor.position() > 0:
2105 cursor.movePosition(QtGui.QTextCursor.Left,
2109 cursor.movePosition(QtGui.QTextCursor.Left,
2106 QtGui.QTextCursor.KeepAnchor)
2110 QtGui.QTextCursor.KeepAnchor)
2107 if cursor.selection().toPlainText() != '\n':
2111 if cursor.selection().toPlainText() != '\n':
2108 self._append_block()
2112 self._append_block()
2109 self._append_before_prompt_pos += 1
2113 self._append_before_prompt_pos += 1
2110
2114
2111 # Write the prompt.
2115 # Write the prompt.
2112 self._append_plain_text(self._prompt_sep)
2116 self._append_plain_text(self._prompt_sep)
2113 if prompt is None:
2117 if prompt is None:
2114 if self._prompt_html is None:
2118 if self._prompt_html is None:
2115 self._append_plain_text(self._prompt)
2119 self._append_plain_text(self._prompt)
2116 else:
2120 else:
2117 self._append_html(self._prompt_html)
2121 self._append_html(self._prompt_html)
2118 else:
2122 else:
2119 if html:
2123 if html:
2120 self._prompt = self._append_html_fetching_plain_text(prompt)
2124 self._prompt = self._append_html_fetching_plain_text(prompt)
2121 self._prompt_html = prompt
2125 self._prompt_html = prompt
2122 else:
2126 else:
2123 self._append_plain_text(prompt)
2127 self._append_plain_text(prompt)
2124 self._prompt = prompt
2128 self._prompt = prompt
2125 self._prompt_html = None
2129 self._prompt_html = None
2126
2130
2127 self._flush_pending_stream()
2131 self._flush_pending_stream()
2128 self._prompt_pos = self._get_end_cursor().position()
2132 self._prompt_pos = self._get_end_cursor().position()
2129 self._prompt_started()
2133 self._prompt_started()
2130
2134
2131 #------ Signal handlers ----------------------------------------------------
2135 #------ Signal handlers ----------------------------------------------------
2132
2136
2133 def _adjust_scrollbars(self):
2137 def _adjust_scrollbars(self):
2134 """ Expands the vertical scrollbar beyond the range set by Qt.
2138 """ Expands the vertical scrollbar beyond the range set by Qt.
2135 """
2139 """
2136 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2140 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2137 # and qtextedit.cpp.
2141 # and qtextedit.cpp.
2138 document = self._control.document()
2142 document = self._control.document()
2139 scrollbar = self._control.verticalScrollBar()
2143 scrollbar = self._control.verticalScrollBar()
2140 viewport_height = self._control.viewport().height()
2144 viewport_height = self._control.viewport().height()
2141 if isinstance(self._control, QtGui.QPlainTextEdit):
2145 if isinstance(self._control, QtGui.QPlainTextEdit):
2142 maximum = max(0, document.lineCount() - 1)
2146 maximum = max(0, document.lineCount() - 1)
2143 step = viewport_height / self._control.fontMetrics().lineSpacing()
2147 step = viewport_height / self._control.fontMetrics().lineSpacing()
2144 else:
2148 else:
2145 # QTextEdit does not do line-based layout and blocks will not in
2149 # QTextEdit does not do line-based layout and blocks will not in
2146 # general have the same height. Therefore it does not make sense to
2150 # general have the same height. Therefore it does not make sense to
2147 # attempt to scroll in line height increments.
2151 # attempt to scroll in line height increments.
2148 maximum = document.size().height()
2152 maximum = document.size().height()
2149 step = viewport_height
2153 step = viewport_height
2150 diff = maximum - scrollbar.maximum()
2154 diff = maximum - scrollbar.maximum()
2151 scrollbar.setRange(0, maximum)
2155 scrollbar.setRange(0, maximum)
2152 scrollbar.setPageStep(step)
2156 scrollbar.setPageStep(step)
2153
2157
2154 # Compensate for undesirable scrolling that occurs automatically due to
2158 # Compensate for undesirable scrolling that occurs automatically due to
2155 # maximumBlockCount() text truncation.
2159 # maximumBlockCount() text truncation.
2156 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2160 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2157 scrollbar.setValue(scrollbar.value() + diff)
2161 scrollbar.setValue(scrollbar.value() + diff)
2158
2162
2159 def _custom_context_menu_requested(self, pos):
2163 def _custom_context_menu_requested(self, pos):
2160 """ Shows a context menu at the given QPoint (in widget coordinates).
2164 """ Shows a context menu at the given QPoint (in widget coordinates).
2161 """
2165 """
2162 menu = self._context_menu_make(pos)
2166 menu = self._context_menu_make(pos)
2163 menu.exec_(self._control.mapToGlobal(pos))
2167 menu.exec_(self._control.mapToGlobal(pos))
General Comments 0
You need to be logged in to leave comments. Login now