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