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