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