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