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