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