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