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