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