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