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