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