##// END OF EJS Templates
Fixed several bugs involving the insertion of new lines. Pressing Enter in the ConsoleWidget now works as expected.
epatters -
Show More
@@ -1,101 +1,101 b''
1 """ Provides bracket matching for Q[Plain]TextEdit widgets.
1 """ Provides bracket matching for Q[Plain]TextEdit widgets.
2 """
2 """
3
3
4 # System library imports
4 # System library imports
5 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
6
6
7
7
8 class BracketMatcher(QtCore.QObject):
8 class BracketMatcher(QtCore.QObject):
9 """ Matches square brackets, braces, and parentheses based on cursor
9 """ Matches square brackets, braces, and parentheses based on cursor
10 position.
10 position.
11 """
11 """
12
12
13 # Protected class variables.
13 # Protected class variables.
14 _opening_map = { '(':')', '{':'}', '[':']' }
14 _opening_map = { '(':')', '{':'}', '[':']' }
15 _closing_map = { ')':'(', '}':'{', ']':'[' }
15 _closing_map = { ')':'(', '}':'{', ']':'[' }
16
16
17 #--------------------------------------------------------------------------
17 #--------------------------------------------------------------------------
18 # 'QObject' interface
18 # 'QObject' interface
19 #--------------------------------------------------------------------------
19 #--------------------------------------------------------------------------
20
20
21 def __init__(self, parent, multiline=True):
21 def __init__(self, parent):
22 """ Create a call tip manager that is attached to the specified Qt
22 """ Create a call tip manager that is attached to the specified Qt
23 text edit widget.
23 text edit widget.
24 """
24 """
25 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
25 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
26 QtCore.QObject.__init__(self, parent)
26 QtCore.QObject.__init__(self, parent)
27
27
28 # The format to apply to matching brackets.
28 # The format to apply to matching brackets.
29 self.format = QtGui.QTextCharFormat()
29 self.format = QtGui.QTextCharFormat()
30 self.format.setBackground(QtGui.QColor('silver'))
30 self.format.setBackground(QtGui.QColor('silver'))
31
31
32 parent.cursorPositionChanged.connect(self._cursor_position_changed)
32 parent.cursorPositionChanged.connect(self._cursor_position_changed)
33
33
34 #--------------------------------------------------------------------------
34 #--------------------------------------------------------------------------
35 # Protected interface
35 # Protected interface
36 #--------------------------------------------------------------------------
36 #--------------------------------------------------------------------------
37
37
38 def _find_match(self, position):
38 def _find_match(self, position):
39 """ Given a valid position in the text document, try to find the
39 """ Given a valid position in the text document, try to find the
40 position of the matching bracket. Returns -1 if unsuccessful.
40 position of the matching bracket. Returns -1 if unsuccessful.
41 """
41 """
42 # Decide what character to search for and what direction to search in.
42 # Decide what character to search for and what direction to search in.
43 document = self.parent().document()
43 document = self.parent().document()
44 qchar = document.characterAt(position)
44 qchar = document.characterAt(position)
45 start_char = qchar.toAscii()
45 start_char = qchar.toAscii()
46 search_char = self._opening_map.get(start_char)
46 search_char = self._opening_map.get(start_char)
47 if search_char:
47 if search_char:
48 increment = 1
48 increment = 1
49 else:
49 else:
50 search_char = self._closing_map.get(start_char)
50 search_char = self._closing_map.get(start_char)
51 if search_char:
51 if search_char:
52 increment = -1
52 increment = -1
53 else:
53 else:
54 return -1
54 return -1
55
55
56 # Search for the character.
56 # Search for the character.
57 depth = 0
57 depth = 0
58 while position >= 0 and position < document.characterCount():
58 while position >= 0 and position < document.characterCount():
59 char = qchar.toAscii()
59 char = qchar.toAscii()
60 if char == start_char:
60 if char == start_char:
61 depth += 1
61 depth += 1
62 elif char == search_char:
62 elif char == search_char:
63 depth -= 1
63 depth -= 1
64 if depth == 0:
64 if depth == 0:
65 break
65 break
66 position += increment
66 position += increment
67 qchar = document.characterAt(position)
67 qchar = document.characterAt(position)
68 else:
68 else:
69 position = -1
69 position = -1
70 return position
70 return position
71
71
72 def _selection_for_character(self, position):
72 def _selection_for_character(self, position):
73 """ Convenience method for selecting a character.
73 """ Convenience method for selecting a character.
74 """
74 """
75 selection = QtGui.QTextEdit.ExtraSelection()
75 selection = QtGui.QTextEdit.ExtraSelection()
76 cursor = self.parent().textCursor()
76 cursor = self.parent().textCursor()
77 cursor.setPosition(position)
77 cursor.setPosition(position)
78 cursor.movePosition(QtGui.QTextCursor.NextCharacter,
78 cursor.movePosition(QtGui.QTextCursor.NextCharacter,
79 QtGui.QTextCursor.KeepAnchor)
79 QtGui.QTextCursor.KeepAnchor)
80 selection.cursor = cursor
80 selection.cursor = cursor
81 selection.format = self.format
81 selection.format = self.format
82 return selection
82 return selection
83
83
84 #------ Signal handlers ----------------------------------------------------
84 #------ Signal handlers ----------------------------------------------------
85
85
86 def _cursor_position_changed(self):
86 def _cursor_position_changed(self):
87 """ Updates the document formatting based on the new cursor position.
87 """ Updates the document formatting based on the new cursor position.
88 """
88 """
89 # Clear out the old formatting.
89 # Clear out the old formatting.
90 text_edit = self.parent()
90 text_edit = self.parent()
91 text_edit.setExtraSelections([])
91 text_edit.setExtraSelections([])
92
92
93 # Attempt to match a bracket for the new cursor position.
93 # Attempt to match a bracket for the new cursor position.
94 cursor = text_edit.textCursor()
94 cursor = text_edit.textCursor()
95 if not cursor.hasSelection():
95 if not cursor.hasSelection():
96 position = cursor.position() - 1
96 position = cursor.position() - 1
97 match_position = self._find_match(position)
97 match_position = self._find_match(position)
98 if match_position != -1:
98 if match_position != -1:
99 extra_selections = [ self._selection_for_character(pos)
99 extra_selections = [ self._selection_for_character(pos)
100 for pos in (position, match_position) ]
100 for pos in (position, match_position) ]
101 text_edit.setExtraSelections(extra_selections)
101 text_edit.setExtraSelections(extra_selections)
@@ -1,1374 +1,1392 b''
1 # Standard library imports
1 # Standard library imports
2 import re
2 import re
3 import sys
3 import sys
4 from textwrap import dedent
4 from textwrap import dedent
5
5
6 # System library imports
6 # System library imports
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8
8
9 # Local imports
9 # Local imports
10 from IPython.config.configurable import Configurable
10 from IPython.config.configurable import Configurable
11 from IPython.frontend.qt.util import MetaQObjectHasTraits
11 from IPython.frontend.qt.util import MetaQObjectHasTraits
12 from IPython.utils.traitlets import Bool, Enum, Int
12 from IPython.utils.traitlets import Bool, Enum, Int
13 from ansi_code_processor import QtAnsiCodeProcessor
13 from ansi_code_processor import QtAnsiCodeProcessor
14 from completion_widget import CompletionWidget
14 from completion_widget import CompletionWidget
15
15
16
16
17 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
17 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
18 """ A QPlainTextEdit suitable for use with ConsoleWidget.
18 """ A QPlainTextEdit suitable for use with ConsoleWidget.
19 """
19 """
20 # Prevents text from being moved by drag and drop. Note that is not, for
20 # Prevents text from being moved by drag and drop. Note that is not, for
21 # some reason, sufficient to catch drag events in the ConsoleWidget's
21 # some reason, sufficient to catch drag events in the ConsoleWidget's
22 # event filter.
22 # event filter.
23 def dragEnterEvent(self, event): pass
23 def dragEnterEvent(self, event): pass
24 def dragLeaveEvent(self, event): pass
24 def dragLeaveEvent(self, event): pass
25 def dragMoveEvent(self, event): pass
25 def dragMoveEvent(self, event): pass
26 def dropEvent(self, event): pass
26 def dropEvent(self, event): pass
27
27
28 class ConsoleTextEdit(QtGui.QTextEdit):
28 class ConsoleTextEdit(QtGui.QTextEdit):
29 """ A QTextEdit suitable for use with ConsoleWidget.
29 """ A QTextEdit suitable for use with ConsoleWidget.
30 """
30 """
31 # See above.
31 # See above.
32 def dragEnterEvent(self, event): pass
32 def dragEnterEvent(self, event): pass
33 def dragLeaveEvent(self, event): pass
33 def dragLeaveEvent(self, event): pass
34 def dragMoveEvent(self, event): pass
34 def dragMoveEvent(self, event): pass
35 def dropEvent(self, event): pass
35 def dropEvent(self, event): pass
36
36
37
37
38 class ConsoleWidget(Configurable, QtGui.QWidget):
38 class ConsoleWidget(Configurable, QtGui.QWidget):
39 """ An abstract base class for console-type widgets. This class has
39 """ An abstract base class for console-type widgets. This class has
40 functionality for:
40 functionality for:
41
41
42 * Maintaining a prompt and editing region
42 * Maintaining a prompt and editing region
43 * Providing the traditional Unix-style console keyboard shortcuts
43 * Providing the traditional Unix-style console keyboard shortcuts
44 * Performing tab completion
44 * Performing tab completion
45 * Paging text
45 * Paging text
46 * Handling ANSI escape codes
46 * Handling ANSI escape codes
47
47
48 ConsoleWidget also provides a number of utility methods that will be
48 ConsoleWidget also provides a number of utility methods that will be
49 convenient to implementors of a console-style widget.
49 convenient to implementors of a console-style widget.
50 """
50 """
51 __metaclass__ = MetaQObjectHasTraits
51 __metaclass__ = MetaQObjectHasTraits
52
52
53 # Whether to process ANSI escape codes.
53 # Whether to process ANSI escape codes.
54 ansi_codes = Bool(True, config=True)
54 ansi_codes = Bool(True, config=True)
55
55
56 # The maximum number of lines of text before truncation. Specifying a
56 # The maximum number of lines of text before truncation. Specifying a
57 # non-positive number disables text truncation (not recommended).
57 # non-positive number disables text truncation (not recommended).
58 buffer_size = Int(500, config=True)
58 buffer_size = Int(500, config=True)
59
59
60 # Whether to use a list widget or plain text output for tab completion.
60 # Whether to use a list widget or plain text output for tab completion.
61 gui_completion = Bool(True, config=True)
61 gui_completion = Bool(True, config=True)
62
62
63 # The type of underlying text widget to use. Valid values are 'plain', which
63 # The type of underlying text widget to use. Valid values are 'plain', which
64 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
64 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
65 # NOTE: this value can only be specified during initialization.
65 # NOTE: this value can only be specified during initialization.
66 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
66 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
67
67
68 # The type of paging to use. Valid values are:
68 # The type of paging to use. Valid values are:
69 # 'inside' : The widget pages like a traditional terminal pager.
69 # 'inside' : The widget pages like a traditional terminal pager.
70 # 'hsplit' : When paging is requested, the widget is split
70 # 'hsplit' : When paging is requested, the widget is split
71 # horizontally. The top pane contains the console, and the
71 # horizontally. The top pane contains the console, and the
72 # bottom pane contains the paged text.
72 # bottom pane contains the paged text.
73 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
73 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
74 # 'custom' : No action is taken by the widget beyond emitting a
74 # 'custom' : No action is taken by the widget beyond emitting a
75 # 'custom_page_requested(str)' signal.
75 # 'custom_page_requested(str)' signal.
76 # 'none' : The text is written directly to the console.
76 # 'none' : The text is written directly to the console.
77 # NOTE: this value can only be specified during initialization.
77 # NOTE: this value can only be specified during initialization.
78 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
78 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
79 default_value='inside', config=True)
79 default_value='inside', config=True)
80
80
81 # Whether to override ShortcutEvents for the keybindings defined by this
81 # Whether to override ShortcutEvents for the keybindings defined by this
82 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
82 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
83 # priority (when it has focus) over, e.g., window-level menu shortcuts.
83 # priority (when it has focus) over, e.g., window-level menu shortcuts.
84 override_shortcuts = Bool(False)
84 override_shortcuts = Bool(False)
85
85
86 # Signals that indicate ConsoleWidget state.
86 # Signals that indicate ConsoleWidget state.
87 copy_available = QtCore.pyqtSignal(bool)
87 copy_available = QtCore.pyqtSignal(bool)
88 redo_available = QtCore.pyqtSignal(bool)
88 redo_available = QtCore.pyqtSignal(bool)
89 undo_available = QtCore.pyqtSignal(bool)
89 undo_available = QtCore.pyqtSignal(bool)
90
90
91 # Signal emitted when paging is needed and the paging style has been
91 # Signal emitted when paging is needed and the paging style has been
92 # specified as 'custom'.
92 # specified as 'custom'.
93 custom_page_requested = QtCore.pyqtSignal(object)
93 custom_page_requested = QtCore.pyqtSignal(object)
94
94
95 # Protected class variables.
95 # Protected class variables.
96 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
96 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
97 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
97 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
98 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
98 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
99 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
99 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
100 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
100 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
101 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
101 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
102 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
102 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
103 _shortcuts = set(_ctrl_down_remap.keys() +
103 _shortcuts = set(_ctrl_down_remap.keys() +
104 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
104 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
105
105
106 #---------------------------------------------------------------------------
106 #---------------------------------------------------------------------------
107 # 'QObject' interface
107 # 'QObject' interface
108 #---------------------------------------------------------------------------
108 #---------------------------------------------------------------------------
109
109
110 def __init__(self, parent=None, **kw):
110 def __init__(self, parent=None, **kw):
111 """ Create a ConsoleWidget.
111 """ Create a ConsoleWidget.
112
112
113 Parameters:
113 Parameters:
114 -----------
114 -----------
115 parent : QWidget, optional [default None]
115 parent : QWidget, optional [default None]
116 The parent for this widget.
116 The parent for this widget.
117 """
117 """
118 QtGui.QWidget.__init__(self, parent)
118 QtGui.QWidget.__init__(self, parent)
119 Configurable.__init__(self, **kw)
119 Configurable.__init__(self, **kw)
120
120
121 # Create the layout and underlying text widget.
121 # Create the layout and underlying text widget.
122 layout = QtGui.QStackedLayout(self)
122 layout = QtGui.QStackedLayout(self)
123 layout.setContentsMargins(0, 0, 0, 0)
123 layout.setContentsMargins(0, 0, 0, 0)
124 self._control = self._create_control()
124 self._control = self._create_control()
125 self._page_control = None
125 self._page_control = None
126 self._splitter = None
126 self._splitter = None
127 if self.paging in ('hsplit', 'vsplit'):
127 if self.paging in ('hsplit', 'vsplit'):
128 self._splitter = QtGui.QSplitter()
128 self._splitter = QtGui.QSplitter()
129 if self.paging == 'hsplit':
129 if self.paging == 'hsplit':
130 self._splitter.setOrientation(QtCore.Qt.Horizontal)
130 self._splitter.setOrientation(QtCore.Qt.Horizontal)
131 else:
131 else:
132 self._splitter.setOrientation(QtCore.Qt.Vertical)
132 self._splitter.setOrientation(QtCore.Qt.Vertical)
133 self._splitter.addWidget(self._control)
133 self._splitter.addWidget(self._control)
134 layout.addWidget(self._splitter)
134 layout.addWidget(self._splitter)
135 else:
135 else:
136 layout.addWidget(self._control)
136 layout.addWidget(self._control)
137
137
138 # Create the paging widget, if necessary.
138 # Create the paging widget, if necessary.
139 if self.paging in ('inside', 'hsplit', 'vsplit'):
139 if self.paging in ('inside', 'hsplit', 'vsplit'):
140 self._page_control = self._create_page_control()
140 self._page_control = self._create_page_control()
141 if self._splitter:
141 if self._splitter:
142 self._page_control.hide()
142 self._page_control.hide()
143 self._splitter.addWidget(self._page_control)
143 self._splitter.addWidget(self._page_control)
144 else:
144 else:
145 layout.addWidget(self._page_control)
145 layout.addWidget(self._page_control)
146
146
147 # Initialize protected variables. Some variables contain useful state
147 # Initialize protected variables. Some variables contain useful state
148 # information for subclasses; they should be considered read-only.
148 # information for subclasses; they should be considered read-only.
149 self._ansi_processor = QtAnsiCodeProcessor()
149 self._ansi_processor = QtAnsiCodeProcessor()
150 self._completion_widget = CompletionWidget(self._control)
150 self._completion_widget = CompletionWidget(self._control)
151 self._continuation_prompt = '> '
151 self._continuation_prompt = '> '
152 self._continuation_prompt_html = None
152 self._continuation_prompt_html = None
153 self._executing = False
153 self._executing = False
154 self._prompt = ''
154 self._prompt = ''
155 self._prompt_html = None
155 self._prompt_html = None
156 self._prompt_pos = 0
156 self._prompt_pos = 0
157 self._prompt_sep = ''
157 self._prompt_sep = ''
158 self._reading = False
158 self._reading = False
159 self._reading_callback = None
159 self._reading_callback = None
160 self._tab_width = 8
160 self._tab_width = 8
161
161
162 # Set a monospaced font.
162 # Set a monospaced font.
163 self.reset_font()
163 self.reset_font()
164
164
165 def eventFilter(self, obj, event):
165 def eventFilter(self, obj, event):
166 """ Reimplemented to ensure a console-like behavior in the underlying
166 """ Reimplemented to ensure a console-like behavior in the underlying
167 text widget.
167 text widget.
168 """
168 """
169 # Re-map keys for all filtered widgets.
169 # Re-map keys for all filtered widgets.
170 etype = event.type()
170 etype = event.type()
171 if etype == QtCore.QEvent.KeyPress and \
171 if etype == QtCore.QEvent.KeyPress and \
172 self._control_key_down(event.modifiers()) and \
172 self._control_key_down(event.modifiers()) and \
173 event.key() in self._ctrl_down_remap:
173 event.key() in self._ctrl_down_remap:
174 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
174 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
175 self._ctrl_down_remap[event.key()],
175 self._ctrl_down_remap[event.key()],
176 QtCore.Qt.NoModifier)
176 QtCore.Qt.NoModifier)
177 QtGui.qApp.sendEvent(obj, new_event)
177 QtGui.qApp.sendEvent(obj, new_event)
178 return True
178 return True
179
179
180 # Override shortucts for all filtered widgets. Note that on Mac OS it is
180 # Override shortucts for all filtered widgets. Note that on Mac OS it is
181 # always unnecessary to override shortcuts, hence the check below (users
181 # always unnecessary to override shortcuts, hence the check below (users
182 # should just use the Control key instead of the Command key).
182 # should just use the Control key instead of the Command key).
183 elif etype == QtCore.QEvent.ShortcutOverride and \
183 elif etype == QtCore.QEvent.ShortcutOverride and \
184 sys.platform != 'darwin' and \
184 sys.platform != 'darwin' and \
185 self._control_key_down(event.modifiers()) and \
185 self._control_key_down(event.modifiers()) and \
186 event.key() in self._shortcuts:
186 event.key() in self._shortcuts:
187 event.accept()
187 event.accept()
188 return False
188 return False
189
189
190 elif etype == QtCore.QEvent.KeyPress:
190 elif etype == QtCore.QEvent.KeyPress:
191 if obj == self._control:
191 if obj == self._control:
192 return self._event_filter_console_keypress(event)
192 return self._event_filter_console_keypress(event)
193 elif obj == self._page_control:
193 elif obj == self._page_control:
194 return self._event_filter_page_keypress(event)
194 return self._event_filter_page_keypress(event)
195
195
196 return super(ConsoleWidget, self).eventFilter(obj, event)
196 return super(ConsoleWidget, self).eventFilter(obj, event)
197
197
198 #---------------------------------------------------------------------------
198 #---------------------------------------------------------------------------
199 # 'QWidget' interface
199 # 'QWidget' interface
200 #---------------------------------------------------------------------------
200 #---------------------------------------------------------------------------
201
201
202 def sizeHint(self):
202 def sizeHint(self):
203 """ Reimplemented to suggest a size that is 80 characters wide and
203 """ Reimplemented to suggest a size that is 80 characters wide and
204 25 lines high.
204 25 lines high.
205 """
205 """
206 font_metrics = QtGui.QFontMetrics(self.font)
206 font_metrics = QtGui.QFontMetrics(self.font)
207 margin = (self._control.frameWidth() +
207 margin = (self._control.frameWidth() +
208 self._control.document().documentMargin()) * 2
208 self._control.document().documentMargin()) * 2
209 style = self.style()
209 style = self.style()
210 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
210 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
211
211
212 # Despite my best efforts to take the various margins into account, the
212 # Despite my best efforts to take the various margins into account, the
213 # width is still coming out a bit too small, so we include a fudge
213 # width is still coming out a bit too small, so we include a fudge
214 # factor of one character here.
214 # factor of one character here.
215 width = font_metrics.maxWidth() * 81 + margin
215 width = font_metrics.maxWidth() * 81 + margin
216 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
216 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
217 if self.paging == 'hsplit':
217 if self.paging == 'hsplit':
218 width = width * 2 + splitwidth
218 width = width * 2 + splitwidth
219
219
220 height = font_metrics.height() * 25 + margin
220 height = font_metrics.height() * 25 + margin
221 if self.paging == 'vsplit':
221 if self.paging == 'vsplit':
222 height = height * 2 + splitwidth
222 height = height * 2 + splitwidth
223
223
224 return QtCore.QSize(width, height)
224 return QtCore.QSize(width, height)
225
225
226 #---------------------------------------------------------------------------
226 #---------------------------------------------------------------------------
227 # 'ConsoleWidget' public interface
227 # 'ConsoleWidget' public interface
228 #---------------------------------------------------------------------------
228 #---------------------------------------------------------------------------
229
229
230 def can_paste(self):
230 def can_paste(self):
231 """ Returns whether text can be pasted from the clipboard.
231 """ Returns whether text can be pasted from the clipboard.
232 """
232 """
233 # Only accept text that can be ASCII encoded.
233 # Only accept text that can be ASCII encoded.
234 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
234 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
235 text = QtGui.QApplication.clipboard().text()
235 text = QtGui.QApplication.clipboard().text()
236 if not text.isEmpty():
236 if not text.isEmpty():
237 try:
237 try:
238 str(text)
238 str(text)
239 return True
239 return True
240 except UnicodeEncodeError:
240 except UnicodeEncodeError:
241 pass
241 pass
242 return False
242 return False
243
243
244 def clear(self, keep_input=True):
244 def clear(self, keep_input=True):
245 """ Clear the console, then write a new prompt. If 'keep_input' is set,
245 """ Clear the console, then write a new prompt. If 'keep_input' is set,
246 restores the old input buffer when the new prompt is written.
246 restores the old input buffer when the new prompt is written.
247 """
247 """
248 if keep_input:
248 if keep_input:
249 input_buffer = self.input_buffer
249 input_buffer = self.input_buffer
250 self._control.clear()
250 self._control.clear()
251 self._show_prompt()
251 self._show_prompt()
252 if keep_input:
252 if keep_input:
253 self.input_buffer = input_buffer
253 self.input_buffer = input_buffer
254
254
255 def copy(self):
255 def copy(self):
256 """ Copy the current selected text to the clipboard.
256 """ Copy the current selected text to the clipboard.
257 """
257 """
258 self._control.copy()
258 self._control.copy()
259
259
260 def execute(self, source=None, hidden=False, interactive=False):
260 def execute(self, source=None, hidden=False, interactive=False):
261 """ Executes source or the input buffer, possibly prompting for more
261 """ Executes source or the input buffer, possibly prompting for more
262 input.
262 input.
263
263
264 Parameters:
264 Parameters:
265 -----------
265 -----------
266 source : str, optional
266 source : str, optional
267
267
268 The source to execute. If not specified, the input buffer will be
268 The source to execute. If not specified, the input buffer will be
269 used. If specified and 'hidden' is False, the input buffer will be
269 used. If specified and 'hidden' is False, the input buffer will be
270 replaced with the source before execution.
270 replaced with the source before execution.
271
271
272 hidden : bool, optional (default False)
272 hidden : bool, optional (default False)
273
273
274 If set, no output will be shown and the prompt will not be modified.
274 If set, no output will be shown and the prompt will not be modified.
275 In other words, it will be completely invisible to the user that
275 In other words, it will be completely invisible to the user that
276 an execution has occurred.
276 an execution has occurred.
277
277
278 interactive : bool, optional (default False)
278 interactive : bool, optional (default False)
279
279
280 Whether the console is to treat the source as having been manually
280 Whether the console is to treat the source as having been manually
281 entered by the user. The effect of this parameter depends on the
281 entered by the user. The effect of this parameter depends on the
282 subclass implementation.
282 subclass implementation.
283
283
284 Raises:
284 Raises:
285 -------
285 -------
286 RuntimeError
286 RuntimeError
287 If incomplete input is given and 'hidden' is True. In this case,
287 If incomplete input is given and 'hidden' is True. In this case,
288 it is not possible to prompt for more input.
288 it is not possible to prompt for more input.
289
289
290 Returns:
290 Returns:
291 --------
291 --------
292 A boolean indicating whether the source was executed.
292 A boolean indicating whether the source was executed.
293 """
293 """
294 # WARNING: The order in which things happen here is very particular, in
294 # WARNING: The order in which things happen here is very particular, in
295 # large part because our syntax highlighting is fragile. If you change
295 # large part because our syntax highlighting is fragile. If you change
296 # something, test carefully!
296 # something, test carefully!
297
297
298 # Decide what to execute.
298 # Decide what to execute.
299 if source is None:
299 if source is None:
300 source = self.input_buffer
300 source = self.input_buffer
301 if not hidden:
301 if not hidden:
302 # A newline is appended later, but it should be considered part
302 # A newline is appended later, but it should be considered part
303 # of the input buffer.
303 # of the input buffer.
304 source += '\n'
304 source += '\n'
305 elif not hidden:
305 elif not hidden:
306 self.input_buffer = source
306 self.input_buffer = source
307
307
308 # Execute the source or show a continuation prompt if it is incomplete.
308 # Execute the source or show a continuation prompt if it is incomplete.
309 complete = self._is_complete(source, interactive)
309 complete = self._is_complete(source, interactive)
310 if hidden:
310 if hidden:
311 if complete:
311 if complete:
312 self._execute(source, hidden)
312 self._execute(source, hidden)
313 else:
313 else:
314 error = 'Incomplete noninteractive input: "%s"'
314 error = 'Incomplete noninteractive input: "%s"'
315 raise RuntimeError(error % source)
315 raise RuntimeError(error % source)
316 else:
316 else:
317 if complete:
317 if complete:
318 self._append_plain_text('\n')
318 self._append_plain_text('\n')
319 self._executing_input_buffer = self.input_buffer
319 self._executing_input_buffer = self.input_buffer
320 self._executing = True
320 self._executing = True
321 self._prompt_finished()
321 self._prompt_finished()
322
322
323 # The maximum block count is only in effect during execution.
323 # The maximum block count is only in effect during execution.
324 # This ensures that _prompt_pos does not become invalid due to
324 # This ensures that _prompt_pos does not become invalid due to
325 # text truncation.
325 # text truncation.
326 self._control.document().setMaximumBlockCount(self.buffer_size)
326 self._control.document().setMaximumBlockCount(self.buffer_size)
327
327
328 # Setting a positive maximum block count will automatically
328 # Setting a positive maximum block count will automatically
329 # disable the undo/redo history, but just to be safe:
329 # disable the undo/redo history, but just to be safe:
330 self._control.setUndoRedoEnabled(False)
330 self._control.setUndoRedoEnabled(False)
331
331
332 self._execute(source, hidden)
332 self._execute(source, hidden)
333
333
334 else:
334 else:
335 # Do this inside an edit block so continuation prompts are
335 # Do this inside an edit block so continuation prompts are
336 # removed seamlessly via undo/redo.
336 # removed seamlessly via undo/redo.
337 cursor = self._control.textCursor()
337 cursor = self._get_end_cursor()
338 cursor.beginEditBlock()
338 cursor.beginEditBlock()
339
339 cursor.insertText('\n')
340 self._append_plain_text('\n')
340 self._insert_continuation_prompt(cursor)
341 self._show_continuation_prompt()
341 self._control.moveCursor(QtGui.QTextCursor.End)
342
343 cursor.endEditBlock()
342 cursor.endEditBlock()
344
343
345 return complete
344 return complete
346
345
347 def _get_input_buffer(self):
346 def _get_input_buffer(self):
348 """ The text that the user has entered entered at the current prompt.
347 """ The text that the user has entered entered at the current prompt.
349 """
348 """
350 # If we're executing, the input buffer may not even exist anymore due to
349 # If we're executing, the input buffer may not even exist anymore due to
351 # the limit imposed by 'buffer_size'. Therefore, we store it.
350 # the limit imposed by 'buffer_size'. Therefore, we store it.
352 if self._executing:
351 if self._executing:
353 return self._executing_input_buffer
352 return self._executing_input_buffer
354
353
355 cursor = self._get_end_cursor()
354 cursor = self._get_end_cursor()
356 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
355 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
357 input_buffer = str(cursor.selection().toPlainText())
356 input_buffer = str(cursor.selection().toPlainText())
358
357
359 # Strip out continuation prompts.
358 # Strip out continuation prompts.
360 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
359 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
361
360
362 def _set_input_buffer(self, string):
361 def _set_input_buffer(self, string):
363 """ Replaces the text in the input buffer with 'string'.
362 """ Replaces the text in the input buffer with 'string'.
364 """
363 """
365 # For now, it is an error to modify the input buffer during execution.
364 # For now, it is an error to modify the input buffer during execution.
366 if self._executing:
365 if self._executing:
367 raise RuntimeError("Cannot change input buffer during execution.")
366 raise RuntimeError("Cannot change input buffer during execution.")
368
367
369 # Remove old text.
368 # Remove old text.
370 cursor = self._get_end_cursor()
369 cursor = self._get_end_cursor()
371 cursor.beginEditBlock()
370 cursor.beginEditBlock()
372 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
371 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
373 cursor.removeSelectedText()
372 cursor.removeSelectedText()
374
373
375 # Insert new text with continuation prompts.
374 # Insert new text with continuation prompts.
376 lines = string.splitlines(True)
375 lines = string.splitlines(True)
377 if lines:
376 if lines:
378 self._append_plain_text(lines[0])
377 self._append_plain_text(lines[0])
379 for i in xrange(1, len(lines)):
378 for i in xrange(1, len(lines)):
380 if self._continuation_prompt_html is None:
379 if self._continuation_prompt_html is None:
381 self._append_plain_text(self._continuation_prompt)
380 self._append_plain_text(self._continuation_prompt)
382 else:
381 else:
383 self._append_html(self._continuation_prompt_html)
382 self._append_html(self._continuation_prompt_html)
384 self._append_plain_text(lines[i])
383 self._append_plain_text(lines[i])
385 cursor.endEditBlock()
384 cursor.endEditBlock()
386 self._control.moveCursor(QtGui.QTextCursor.End)
385 self._control.moveCursor(QtGui.QTextCursor.End)
387
386
388 input_buffer = property(_get_input_buffer, _set_input_buffer)
387 input_buffer = property(_get_input_buffer, _set_input_buffer)
389
388
390 def _get_font(self):
389 def _get_font(self):
391 """ The base font being used by the ConsoleWidget.
390 """ The base font being used by the ConsoleWidget.
392 """
391 """
393 return self._control.document().defaultFont()
392 return self._control.document().defaultFont()
394
393
395 def _set_font(self, font):
394 def _set_font(self, font):
396 """ Sets the base font for the ConsoleWidget to the specified QFont.
395 """ Sets the base font for the ConsoleWidget to the specified QFont.
397 """
396 """
398 font_metrics = QtGui.QFontMetrics(font)
397 font_metrics = QtGui.QFontMetrics(font)
399 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
398 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
400
399
401 self._completion_widget.setFont(font)
400 self._completion_widget.setFont(font)
402 self._control.document().setDefaultFont(font)
401 self._control.document().setDefaultFont(font)
403 if self._page_control:
402 if self._page_control:
404 self._page_control.document().setDefaultFont(font)
403 self._page_control.document().setDefaultFont(font)
405
404
406 font = property(_get_font, _set_font)
405 font = property(_get_font, _set_font)
407
406
408 def paste(self):
407 def paste(self):
409 """ Paste the contents of the clipboard into the input region.
408 """ Paste the contents of the clipboard into the input region.
410 """
409 """
411 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
410 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
412 try:
411 try:
413 text = str(QtGui.QApplication.clipboard().text())
412 text = str(QtGui.QApplication.clipboard().text())
414 except UnicodeEncodeError:
413 except UnicodeEncodeError:
415 pass
414 pass
416 else:
415 else:
417 self._insert_plain_text_into_buffer(dedent(text))
416 self._insert_plain_text_into_buffer(dedent(text))
418
417
419 def print_(self, printer):
418 def print_(self, printer):
420 """ Print the contents of the ConsoleWidget to the specified QPrinter.
419 """ Print the contents of the ConsoleWidget to the specified QPrinter.
421 """
420 """
422 self._control.print_(printer)
421 self._control.print_(printer)
423
422
424 def redo(self):
423 def redo(self):
425 """ Redo the last operation. If there is no operation to redo, nothing
424 """ Redo the last operation. If there is no operation to redo, nothing
426 happens.
425 happens.
427 """
426 """
428 self._control.redo()
427 self._control.redo()
429
428
430 def reset_font(self):
429 def reset_font(self):
431 """ Sets the font to the default fixed-width font for this platform.
430 """ Sets the font to the default fixed-width font for this platform.
432 """
431 """
433 # FIXME: font family and size should be configurable by the user.
432 # FIXME: font family and size should be configurable by the user.
434
435 if sys.platform == 'win32':
433 if sys.platform == 'win32':
436 # Fixme: we should test whether Consolas is available and use it
434 # FIXME: we should test whether Consolas is available and use it
437 # first if it is. Consolas ships by default from Vista onwards,
435 # first if it is. Consolas ships by default from Vista onwards,
438 # it's *vastly* more readable and prettier than Courier, and is
436 # it's *vastly* more readable and prettier than Courier, and is
439 # often installed even on XP systems. So we should first check for
437 # often installed even on XP systems. So we should first check for
440 # it, and only fallback to Courier if absolutely necessary.
438 # it, and only fallback to Courier if absolutely necessary.
441 name = 'Courier'
439 name = 'Courier'
442 elif sys.platform == 'darwin':
440 elif sys.platform == 'darwin':
443 name = 'Monaco'
441 name = 'Monaco'
444 else:
442 else:
445 name = 'Monospace'
443 name = 'Monospace'
446 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
444 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
447 font.setStyleHint(QtGui.QFont.TypeWriter)
445 font.setStyleHint(QtGui.QFont.TypeWriter)
448 self._set_font(font)
446 self._set_font(font)
449
447
450 def select_all(self):
448 def select_all(self):
451 """ Selects all the text in the buffer.
449 """ Selects all the text in the buffer.
452 """
450 """
453 self._control.selectAll()
451 self._control.selectAll()
454
452
455 def _get_tab_width(self):
453 def _get_tab_width(self):
456 """ The width (in terms of space characters) for tab characters.
454 """ The width (in terms of space characters) for tab characters.
457 """
455 """
458 return self._tab_width
456 return self._tab_width
459
457
460 def _set_tab_width(self, tab_width):
458 def _set_tab_width(self, tab_width):
461 """ Sets the width (in terms of space characters) for tab characters.
459 """ Sets the width (in terms of space characters) for tab characters.
462 """
460 """
463 font_metrics = QtGui.QFontMetrics(self.font)
461 font_metrics = QtGui.QFontMetrics(self.font)
464 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
462 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
465
463
466 self._tab_width = tab_width
464 self._tab_width = tab_width
467
465
468 tab_width = property(_get_tab_width, _set_tab_width)
466 tab_width = property(_get_tab_width, _set_tab_width)
469
467
470 def undo(self):
468 def undo(self):
471 """ Undo the last operation. If there is no operation to undo, nothing
469 """ Undo the last operation. If there is no operation to undo, nothing
472 happens.
470 happens.
473 """
471 """
474 self._control.undo()
472 self._control.undo()
475
473
476 #---------------------------------------------------------------------------
474 #---------------------------------------------------------------------------
477 # 'ConsoleWidget' abstract interface
475 # 'ConsoleWidget' abstract interface
478 #---------------------------------------------------------------------------
476 #---------------------------------------------------------------------------
479
477
480 def _is_complete(self, source, interactive):
478 def _is_complete(self, source, interactive):
481 """ Returns whether 'source' can be executed. When triggered by an
479 """ Returns whether 'source' can be executed. When triggered by an
482 Enter/Return key press, 'interactive' is True; otherwise, it is
480 Enter/Return key press, 'interactive' is True; otherwise, it is
483 False.
481 False.
484 """
482 """
485 raise NotImplementedError
483 raise NotImplementedError
486
484
487 def _execute(self, source, hidden):
485 def _execute(self, source, hidden):
488 """ Execute 'source'. If 'hidden', do not show any output.
486 """ Execute 'source'. If 'hidden', do not show any output.
489 """
487 """
490 raise NotImplementedError
488 raise NotImplementedError
491
489
492 def _prompt_started_hook(self):
490 def _prompt_started_hook(self):
493 """ Called immediately after a new prompt is displayed.
491 """ Called immediately after a new prompt is displayed.
494 """
492 """
495 pass
493 pass
496
494
497 def _prompt_finished_hook(self):
495 def _prompt_finished_hook(self):
498 """ Called immediately after a prompt is finished, i.e. when some input
496 """ Called immediately after a prompt is finished, i.e. when some input
499 will be processed and a new prompt displayed.
497 will be processed and a new prompt displayed.
500 """
498 """
501 pass
499 pass
502
500
503 def _up_pressed(self):
501 def _up_pressed(self):
504 """ Called when the up key is pressed. Returns whether to continue
502 """ Called when the up key is pressed. Returns whether to continue
505 processing the event.
503 processing the event.
506 """
504 """
507 return True
505 return True
508
506
509 def _down_pressed(self):
507 def _down_pressed(self):
510 """ Called when the down key is pressed. Returns whether to continue
508 """ Called when the down key is pressed. Returns whether to continue
511 processing the event.
509 processing the event.
512 """
510 """
513 return True
511 return True
514
512
515 def _tab_pressed(self):
513 def _tab_pressed(self):
516 """ Called when the tab key is pressed. Returns whether to continue
514 """ Called when the tab key is pressed. Returns whether to continue
517 processing the event.
515 processing the event.
518 """
516 """
519 return False
517 return False
520
518
521 #--------------------------------------------------------------------------
519 #--------------------------------------------------------------------------
522 # 'ConsoleWidget' protected interface
520 # 'ConsoleWidget' protected interface
523 #--------------------------------------------------------------------------
521 #--------------------------------------------------------------------------
524
522
525 def _append_html(self, html):
523 def _append_html(self, html):
526 """ Appends html at the end of the console buffer.
524 """ Appends html at the end of the console buffer.
527 """
525 """
528 cursor = self._get_end_cursor()
526 cursor = self._get_end_cursor()
529 self._insert_html(cursor, html)
527 self._insert_html(cursor, html)
530
528
531 def _append_html_fetching_plain_text(self, html):
529 def _append_html_fetching_plain_text(self, html):
532 """ Appends 'html', then returns the plain text version of it.
530 """ Appends 'html', then returns the plain text version of it.
533 """
531 """
534 cursor = self._get_end_cursor()
532 cursor = self._get_end_cursor()
535 return self._insert_html_fetching_plain_text(cursor, html)
533 return self._insert_html_fetching_plain_text(cursor, html)
536
534
537 def _append_plain_text(self, text):
535 def _append_plain_text(self, text):
538 """ Appends plain text at the end of the console buffer, processing
536 """ Appends plain text at the end of the console buffer, processing
539 ANSI codes if enabled.
537 ANSI codes if enabled.
540 """
538 """
541 cursor = self._get_end_cursor()
539 cursor = self._get_end_cursor()
542 self._insert_plain_text(cursor, text)
540 self._insert_plain_text(cursor, text)
543
541
544 def _append_plain_text_keeping_prompt(self, text):
542 def _append_plain_text_keeping_prompt(self, text):
545 """ Writes 'text' after the current prompt, then restores the old prompt
543 """ Writes 'text' after the current prompt, then restores the old prompt
546 with its old input buffer.
544 with its old input buffer.
547 """
545 """
548 input_buffer = self.input_buffer
546 input_buffer = self.input_buffer
549 self._append_plain_text('\n')
547 self._append_plain_text('\n')
550 self._prompt_finished()
548 self._prompt_finished()
551
549
552 self._append_plain_text(text)
550 self._append_plain_text(text)
553 self._show_prompt()
551 self._show_prompt()
554 self.input_buffer = input_buffer
552 self.input_buffer = input_buffer
555
553
556 def _complete_with_items(self, cursor, items):
554 def _complete_with_items(self, cursor, items):
557 """ Performs completion with 'items' at the specified cursor location.
555 """ Performs completion with 'items' at the specified cursor location.
558 """
556 """
559 if len(items) == 1:
557 if len(items) == 1:
560 cursor.setPosition(self._control.textCursor().position(),
558 cursor.setPosition(self._control.textCursor().position(),
561 QtGui.QTextCursor.KeepAnchor)
559 QtGui.QTextCursor.KeepAnchor)
562 cursor.insertText(items[0])
560 cursor.insertText(items[0])
563 elif len(items) > 1:
561 elif len(items) > 1:
564 if self.gui_completion:
562 if self.gui_completion:
565 self._completion_widget.show_items(cursor, items)
563 self._completion_widget.show_items(cursor, items)
566 else:
564 else:
567 text = self._format_as_columns(items)
565 text = self._format_as_columns(items)
568 self._append_plain_text_keeping_prompt(text)
566 self._append_plain_text_keeping_prompt(text)
569
567
570 def _control_key_down(self, modifiers):
568 def _control_key_down(self, modifiers):
571 """ Given a KeyboardModifiers flags object, return whether the Control
569 """ Given a KeyboardModifiers flags object, return whether the Control
572 key is down (on Mac OS, treat the Command key as a synonym for
570 key is down (on Mac OS, treat the Command key as a synonym for
573 Control).
571 Control).
574 """
572 """
575 down = bool(modifiers & QtCore.Qt.ControlModifier)
573 down = bool(modifiers & QtCore.Qt.ControlModifier)
576
574
577 # Note: on Mac OS, ControlModifier corresponds to the Command key while
575 # Note: on Mac OS, ControlModifier corresponds to the Command key while
578 # MetaModifier corresponds to the Control key.
576 # MetaModifier corresponds to the Control key.
579 if sys.platform == 'darwin':
577 if sys.platform == 'darwin':
580 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
578 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
581
579
582 return down
580 return down
583
581
584 def _create_control(self):
582 def _create_control(self):
585 """ Creates and connects the underlying text widget.
583 """ Creates and connects the underlying text widget.
586 """
584 """
587 if self.kind == 'plain':
585 if self.kind == 'plain':
588 control = ConsolePlainTextEdit()
586 control = ConsolePlainTextEdit()
589 elif self.kind == 'rich':
587 elif self.kind == 'rich':
590 control = ConsoleTextEdit()
588 control = ConsoleTextEdit()
591 control.setAcceptRichText(False)
589 control.setAcceptRichText(False)
592 control.installEventFilter(self)
590 control.installEventFilter(self)
593 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
591 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
594 control.customContextMenuRequested.connect(self._show_context_menu)
592 control.customContextMenuRequested.connect(self._show_context_menu)
595 control.copyAvailable.connect(self.copy_available)
593 control.copyAvailable.connect(self.copy_available)
596 control.redoAvailable.connect(self.redo_available)
594 control.redoAvailable.connect(self.redo_available)
597 control.undoAvailable.connect(self.undo_available)
595 control.undoAvailable.connect(self.undo_available)
598 control.setReadOnly(True)
596 control.setReadOnly(True)
599 control.setUndoRedoEnabled(False)
597 control.setUndoRedoEnabled(False)
600 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
598 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
601 return control
599 return control
602
600
603 def _create_page_control(self):
601 def _create_page_control(self):
604 """ Creates and connects the underlying paging widget.
602 """ Creates and connects the underlying paging widget.
605 """
603 """
606 control = ConsolePlainTextEdit()
604 control = ConsolePlainTextEdit()
607 control.installEventFilter(self)
605 control.installEventFilter(self)
608 control.setReadOnly(True)
606 control.setReadOnly(True)
609 control.setUndoRedoEnabled(False)
607 control.setUndoRedoEnabled(False)
610 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
608 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
611 return control
609 return control
612
610
613 def _event_filter_console_keypress(self, event):
611 def _event_filter_console_keypress(self, event):
614 """ Filter key events for the underlying text widget to create a
612 """ Filter key events for the underlying text widget to create a
615 console-like interface.
613 console-like interface.
616 """
614 """
617 intercepted = False
615 intercepted = False
618 cursor = self._control.textCursor()
616 cursor = self._control.textCursor()
619 position = cursor.position()
617 position = cursor.position()
620 key = event.key()
618 key = event.key()
621 ctrl_down = self._control_key_down(event.modifiers())
619 ctrl_down = self._control_key_down(event.modifiers())
622 alt_down = event.modifiers() & QtCore.Qt.AltModifier
620 alt_down = event.modifiers() & QtCore.Qt.AltModifier
623 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
621 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
624
622
625 if event.matches(QtGui.QKeySequence.Paste):
623 if event.matches(QtGui.QKeySequence.Paste):
626 # Call our paste instead of the underlying text widget's.
624 # Call our paste instead of the underlying text widget's.
627 self.paste()
625 self.paste()
628 intercepted = True
626 intercepted = True
629
627
630 elif ctrl_down:
628 elif ctrl_down:
631 if key == QtCore.Qt.Key_K:
629 if key == QtCore.Qt.Key_K:
632 if self._in_buffer(position):
630 if self._in_buffer(position):
633 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
631 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
634 QtGui.QTextCursor.KeepAnchor)
632 QtGui.QTextCursor.KeepAnchor)
635 if not cursor.hasSelection():
633 if not cursor.hasSelection():
636 # Line deletion (remove continuation prompt)
634 # Line deletion (remove continuation prompt)
637 cursor.movePosition(QtGui.QTextCursor.NextBlock,
635 cursor.movePosition(QtGui.QTextCursor.NextBlock,
638 QtGui.QTextCursor.KeepAnchor)
636 QtGui.QTextCursor.KeepAnchor)
639 cursor.movePosition(QtGui.QTextCursor.Right,
637 cursor.movePosition(QtGui.QTextCursor.Right,
640 QtGui.QTextCursor.KeepAnchor,
638 QtGui.QTextCursor.KeepAnchor,
641 len(self._continuation_prompt))
639 len(self._continuation_prompt))
642 cursor.removeSelectedText()
640 cursor.removeSelectedText()
643 intercepted = True
641 intercepted = True
644
642
645 elif key == QtCore.Qt.Key_L:
643 elif key == QtCore.Qt.Key_L:
646 # It would be better to simply move the prompt block to the top
644 # It would be better to simply move the prompt block to the top
647 # of the control viewport. QPlainTextEdit has a private method
645 # of the control viewport. QPlainTextEdit has a private method
648 # to do this (setTopBlock), but it cannot be duplicated here
646 # to do this (setTopBlock), but it cannot be duplicated here
649 # because it requires access to the QTextControl that underlies
647 # because it requires access to the QTextControl that underlies
650 # both QPlainTextEdit and QTextEdit. In short, this can only be
648 # both QPlainTextEdit and QTextEdit. In short, this can only be
651 # achieved by appending newlines after the prompt, which is a
649 # achieved by appending newlines after the prompt, which is a
652 # gigantic hack and likely to cause other problems.
650 # gigantic hack and likely to cause other problems.
653 self.clear()
651 self.clear()
654 intercepted = True
652 intercepted = True
655
653
656 elif key == QtCore.Qt.Key_X:
654 elif key == QtCore.Qt.Key_X:
657 # FIXME: Instead of disabling cut completely, only allow it
655 # FIXME: Instead of disabling cut completely, only allow it
658 # when safe.
656 # when safe.
659 intercepted = True
657 intercepted = True
660
658
661 elif key == QtCore.Qt.Key_Y:
659 elif key == QtCore.Qt.Key_Y:
662 self.paste()
660 self.paste()
663 intercepted = True
661 intercepted = True
664
662
665 elif alt_down:
663 elif alt_down:
666 if key == QtCore.Qt.Key_B:
664 if key == QtCore.Qt.Key_B:
667 self._set_cursor(self._get_word_start_cursor(position))
665 self._set_cursor(self._get_word_start_cursor(position))
668 intercepted = True
666 intercepted = True
669
667
670 elif key == QtCore.Qt.Key_F:
668 elif key == QtCore.Qt.Key_F:
671 self._set_cursor(self._get_word_end_cursor(position))
669 self._set_cursor(self._get_word_end_cursor(position))
672 intercepted = True
670 intercepted = True
673
671
674 elif key == QtCore.Qt.Key_Backspace:
672 elif key == QtCore.Qt.Key_Backspace:
675 cursor = self._get_word_start_cursor(position)
673 cursor = self._get_word_start_cursor(position)
676 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
674 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
677 cursor.removeSelectedText()
675 cursor.removeSelectedText()
678 intercepted = True
676 intercepted = True
679
677
680 elif key == QtCore.Qt.Key_D:
678 elif key == QtCore.Qt.Key_D:
681 cursor = self._get_word_end_cursor(position)
679 cursor = self._get_word_end_cursor(position)
682 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
680 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
683 cursor.removeSelectedText()
681 cursor.removeSelectedText()
684 intercepted = True
682 intercepted = True
685
683
686 else:
684 else:
687 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
685 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
688 if self._reading:
689 self._append_plain_text('\n')
690 self._reading = False
691 if self._reading_callback:
692 self._reading_callback()
693 elif not self._executing:
694 self.execute(interactive=True)
695 intercepted = True
686 intercepted = True
687 if self._in_buffer(position):
688 if self._reading:
689 self._append_plain_text('\n')
690 self._reading = False
691 if self._reading_callback:
692 self._reading_callback()
693
694 # If there is only whitespace after the cursor, execute.
695 # Otherwise, split the line with a continuation prompt.
696 elif not self._executing:
697 cursor.movePosition(QtGui.QTextCursor.End,
698 QtGui.QTextCursor.KeepAnchor)
699 if cursor.selectedText().trimmed().isEmpty():
700 self.execute(interactive=True)
701 else:
702 cursor.beginEditBlock()
703 cursor.setPosition(position)
704 cursor.insertText('\n')
705 self._insert_continuation_prompt(cursor)
706
707 # Ensure that the whole input buffer is visible.
708 # FIXME: This will not be usable if the input buffer
709 # is taller than the console widget.
710 self._control.moveCursor(QtGui.QTextCursor.End)
711 self._control.setTextCursor(cursor)
712
713 cursor.endEditBlock()
696
714
697 elif key == QtCore.Qt.Key_Up:
715 elif key == QtCore.Qt.Key_Up:
698 if self._reading or not self._up_pressed():
716 if self._reading or not self._up_pressed():
699 intercepted = True
717 intercepted = True
700 else:
718 else:
701 prompt_line = self._get_prompt_cursor().blockNumber()
719 prompt_line = self._get_prompt_cursor().blockNumber()
702 intercepted = cursor.blockNumber() <= prompt_line
720 intercepted = cursor.blockNumber() <= prompt_line
703
721
704 elif key == QtCore.Qt.Key_Down:
722 elif key == QtCore.Qt.Key_Down:
705 if self._reading or not self._down_pressed():
723 if self._reading or not self._down_pressed():
706 intercepted = True
724 intercepted = True
707 else:
725 else:
708 end_line = self._get_end_cursor().blockNumber()
726 end_line = self._get_end_cursor().blockNumber()
709 intercepted = cursor.blockNumber() == end_line
727 intercepted = cursor.blockNumber() == end_line
710
728
711 elif key == QtCore.Qt.Key_Tab:
729 elif key == QtCore.Qt.Key_Tab:
712 if not self._reading:
730 if not self._reading:
713 intercepted = not self._tab_pressed()
731 intercepted = not self._tab_pressed()
714
732
715 elif key == QtCore.Qt.Key_Left:
733 elif key == QtCore.Qt.Key_Left:
716 intercepted = not self._in_buffer(position - 1)
734 intercepted = not self._in_buffer(position - 1)
717
735
718 elif key == QtCore.Qt.Key_Home:
736 elif key == QtCore.Qt.Key_Home:
719 start_line = cursor.blockNumber()
737 start_line = cursor.blockNumber()
720 if start_line == self._get_prompt_cursor().blockNumber():
738 if start_line == self._get_prompt_cursor().blockNumber():
721 start_pos = self._prompt_pos
739 start_pos = self._prompt_pos
722 else:
740 else:
723 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
741 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
724 QtGui.QTextCursor.KeepAnchor)
742 QtGui.QTextCursor.KeepAnchor)
725 start_pos = cursor.position()
743 start_pos = cursor.position()
726 start_pos += len(self._continuation_prompt)
744 start_pos += len(self._continuation_prompt)
727 cursor.setPosition(position)
745 cursor.setPosition(position)
728 if shift_down and self._in_buffer(position):
746 if shift_down and self._in_buffer(position):
729 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
747 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
730 else:
748 else:
731 cursor.setPosition(start_pos)
749 cursor.setPosition(start_pos)
732 self._set_cursor(cursor)
750 self._set_cursor(cursor)
733 intercepted = True
751 intercepted = True
734
752
735 elif key == QtCore.Qt.Key_Backspace:
753 elif key == QtCore.Qt.Key_Backspace:
736
754
737 # Line deletion (remove continuation prompt)
755 # Line deletion (remove continuation prompt)
738 len_prompt = len(self._continuation_prompt)
756 len_prompt = len(self._continuation_prompt)
739 if not self._reading and \
757 if not self._reading and \
740 cursor.columnNumber() == len_prompt and \
758 cursor.columnNumber() == len_prompt and \
741 position != self._prompt_pos:
759 position != self._prompt_pos:
742 cursor.beginEditBlock()
760 cursor.beginEditBlock()
743 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
761 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
744 QtGui.QTextCursor.KeepAnchor)
762 QtGui.QTextCursor.KeepAnchor)
745 cursor.removeSelectedText()
763 cursor.removeSelectedText()
746 cursor.deletePreviousChar()
764 cursor.deletePreviousChar()
747 cursor.endEditBlock()
765 cursor.endEditBlock()
748 intercepted = True
766 intercepted = True
749
767
750 # Regular backwards deletion
768 # Regular backwards deletion
751 else:
769 else:
752 anchor = cursor.anchor()
770 anchor = cursor.anchor()
753 if anchor == position:
771 if anchor == position:
754 intercepted = not self._in_buffer(position - 1)
772 intercepted = not self._in_buffer(position - 1)
755 else:
773 else:
756 intercepted = not self._in_buffer(min(anchor, position))
774 intercepted = not self._in_buffer(min(anchor, position))
757
775
758 elif key == QtCore.Qt.Key_Delete:
776 elif key == QtCore.Qt.Key_Delete:
759
777
760 # Line deletion (remove continuation prompt)
778 # Line deletion (remove continuation prompt)
761 if not self._reading and cursor.atBlockEnd() and not \
779 if not self._reading and cursor.atBlockEnd() and not \
762 cursor.hasSelection():
780 cursor.hasSelection():
763 cursor.movePosition(QtGui.QTextCursor.NextBlock,
781 cursor.movePosition(QtGui.QTextCursor.NextBlock,
764 QtGui.QTextCursor.KeepAnchor)
782 QtGui.QTextCursor.KeepAnchor)
765 cursor.movePosition(QtGui.QTextCursor.Right,
783 cursor.movePosition(QtGui.QTextCursor.Right,
766 QtGui.QTextCursor.KeepAnchor,
784 QtGui.QTextCursor.KeepAnchor,
767 len(self._continuation_prompt))
785 len(self._continuation_prompt))
768 cursor.removeSelectedText()
786 cursor.removeSelectedText()
769 intercepted = True
787 intercepted = True
770
788
771 # Regular forwards deletion:
789 # Regular forwards deletion:
772 else:
790 else:
773 anchor = cursor.anchor()
791 anchor = cursor.anchor()
774 intercepted = (not self._in_buffer(anchor) or
792 intercepted = (not self._in_buffer(anchor) or
775 not self._in_buffer(position))
793 not self._in_buffer(position))
776
794
777 # Don't move the cursor if control is down to allow copy-paste using
795 # Don't move the cursor if control is down to allow copy-paste using
778 # the keyboard in any part of the buffer.
796 # the keyboard in any part of the buffer.
779 if not ctrl_down:
797 if not ctrl_down:
780 self._keep_cursor_in_buffer()
798 self._keep_cursor_in_buffer()
781
799
782 return intercepted
800 return intercepted
783
801
784 def _event_filter_page_keypress(self, event):
802 def _event_filter_page_keypress(self, event):
785 """ Filter key events for the paging widget to create console-like
803 """ Filter key events for the paging widget to create console-like
786 interface.
804 interface.
787 """
805 """
788 key = event.key()
806 key = event.key()
789
807
790 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
808 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
791 if self._splitter:
809 if self._splitter:
792 self._page_control.hide()
810 self._page_control.hide()
793 else:
811 else:
794 self.layout().setCurrentWidget(self._control)
812 self.layout().setCurrentWidget(self._control)
795 return True
813 return True
796
814
797 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
815 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
798 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
816 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
799 QtCore.Qt.Key_Down,
817 QtCore.Qt.Key_Down,
800 QtCore.Qt.NoModifier)
818 QtCore.Qt.NoModifier)
801 QtGui.qApp.sendEvent(self._page_control, new_event)
819 QtGui.qApp.sendEvent(self._page_control, new_event)
802 return True
820 return True
803
821
804 return False
822 return False
805
823
806 def _format_as_columns(self, items, separator=' '):
824 def _format_as_columns(self, items, separator=' '):
807 """ Transform a list of strings into a single string with columns.
825 """ Transform a list of strings into a single string with columns.
808
826
809 Parameters
827 Parameters
810 ----------
828 ----------
811 items : sequence of strings
829 items : sequence of strings
812 The strings to process.
830 The strings to process.
813
831
814 separator : str, optional [default is two spaces]
832 separator : str, optional [default is two spaces]
815 The string that separates columns.
833 The string that separates columns.
816
834
817 Returns
835 Returns
818 -------
836 -------
819 The formatted string.
837 The formatted string.
820 """
838 """
821 # Note: this code is adapted from columnize 0.3.2.
839 # Note: this code is adapted from columnize 0.3.2.
822 # See http://code.google.com/p/pycolumnize/
840 # See http://code.google.com/p/pycolumnize/
823
841
824 # Calculate the number of characters available.
842 # Calculate the number of characters available.
825 width = self._control.viewport().width()
843 width = self._control.viewport().width()
826 char_width = QtGui.QFontMetrics(self.font).maxWidth()
844 char_width = QtGui.QFontMetrics(self.font).maxWidth()
827 displaywidth = max(10, (width / char_width) - 1)
845 displaywidth = max(10, (width / char_width) - 1)
828
846
829 # Some degenerate cases.
847 # Some degenerate cases.
830 size = len(items)
848 size = len(items)
831 if size == 0:
849 if size == 0:
832 return '\n'
850 return '\n'
833 elif size == 1:
851 elif size == 1:
834 return '%s\n' % str(items[0])
852 return '%s\n' % str(items[0])
835
853
836 # Try every row count from 1 upwards
854 # Try every row count from 1 upwards
837 array_index = lambda nrows, row, col: nrows*col + row
855 array_index = lambda nrows, row, col: nrows*col + row
838 for nrows in range(1, size):
856 for nrows in range(1, size):
839 ncols = (size + nrows - 1) // nrows
857 ncols = (size + nrows - 1) // nrows
840 colwidths = []
858 colwidths = []
841 totwidth = -len(separator)
859 totwidth = -len(separator)
842 for col in range(ncols):
860 for col in range(ncols):
843 # Get max column width for this column
861 # Get max column width for this column
844 colwidth = 0
862 colwidth = 0
845 for row in range(nrows):
863 for row in range(nrows):
846 i = array_index(nrows, row, col)
864 i = array_index(nrows, row, col)
847 if i >= size: break
865 if i >= size: break
848 x = items[i]
866 x = items[i]
849 colwidth = max(colwidth, len(x))
867 colwidth = max(colwidth, len(x))
850 colwidths.append(colwidth)
868 colwidths.append(colwidth)
851 totwidth += colwidth + len(separator)
869 totwidth += colwidth + len(separator)
852 if totwidth > displaywidth:
870 if totwidth > displaywidth:
853 break
871 break
854 if totwidth <= displaywidth:
872 if totwidth <= displaywidth:
855 break
873 break
856
874
857 # The smallest number of rows computed and the max widths for each
875 # The smallest number of rows computed and the max widths for each
858 # column has been obtained. Now we just have to format each of the rows.
876 # column has been obtained. Now we just have to format each of the rows.
859 string = ''
877 string = ''
860 for row in range(nrows):
878 for row in range(nrows):
861 texts = []
879 texts = []
862 for col in range(ncols):
880 for col in range(ncols):
863 i = row + nrows*col
881 i = row + nrows*col
864 if i >= size:
882 if i >= size:
865 texts.append('')
883 texts.append('')
866 else:
884 else:
867 texts.append(items[i])
885 texts.append(items[i])
868 while texts and not texts[-1]:
886 while texts and not texts[-1]:
869 del texts[-1]
887 del texts[-1]
870 for col in range(len(texts)):
888 for col in range(len(texts)):
871 texts[col] = texts[col].ljust(colwidths[col])
889 texts[col] = texts[col].ljust(colwidths[col])
872 string += '%s\n' % str(separator.join(texts))
890 string += '%s\n' % str(separator.join(texts))
873 return string
891 return string
874
892
875 def _get_block_plain_text(self, block):
893 def _get_block_plain_text(self, block):
876 """ Given a QTextBlock, return its unformatted text.
894 """ Given a QTextBlock, return its unformatted text.
877 """
895 """
878 cursor = QtGui.QTextCursor(block)
896 cursor = QtGui.QTextCursor(block)
879 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
897 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
880 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
898 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
881 QtGui.QTextCursor.KeepAnchor)
899 QtGui.QTextCursor.KeepAnchor)
882 return str(cursor.selection().toPlainText())
900 return str(cursor.selection().toPlainText())
883
901
884 def _get_cursor(self):
902 def _get_cursor(self):
885 """ Convenience method that returns a cursor for the current position.
903 """ Convenience method that returns a cursor for the current position.
886 """
904 """
887 return self._control.textCursor()
905 return self._control.textCursor()
888
906
889 def _get_end_cursor(self):
907 def _get_end_cursor(self):
890 """ Convenience method that returns a cursor for the last character.
908 """ Convenience method that returns a cursor for the last character.
891 """
909 """
892 cursor = self._control.textCursor()
910 cursor = self._control.textCursor()
893 cursor.movePosition(QtGui.QTextCursor.End)
911 cursor.movePosition(QtGui.QTextCursor.End)
894 return cursor
912 return cursor
895
913
896 def _get_input_buffer_cursor_column(self):
914 def _get_input_buffer_cursor_column(self):
897 """ Returns the column of the cursor in the input buffer, excluding the
915 """ Returns the column of the cursor in the input buffer, excluding the
898 contribution by the prompt, or -1 if there is no such column.
916 contribution by the prompt, or -1 if there is no such column.
899 """
917 """
900 prompt = self._get_input_buffer_cursor_prompt()
918 prompt = self._get_input_buffer_cursor_prompt()
901 if prompt is None:
919 if prompt is None:
902 return -1
920 return -1
903 else:
921 else:
904 cursor = self._control.textCursor()
922 cursor = self._control.textCursor()
905 return cursor.columnNumber() - len(prompt)
923 return cursor.columnNumber() - len(prompt)
906
924
907 def _get_input_buffer_cursor_line(self):
925 def _get_input_buffer_cursor_line(self):
908 """ Returns line of the input buffer that contains the cursor, or None
926 """ Returns line of the input buffer that contains the cursor, or None
909 if there is no such line.
927 if there is no such line.
910 """
928 """
911 prompt = self._get_input_buffer_cursor_prompt()
929 prompt = self._get_input_buffer_cursor_prompt()
912 if prompt is None:
930 if prompt is None:
913 return None
931 return None
914 else:
932 else:
915 cursor = self._control.textCursor()
933 cursor = self._control.textCursor()
916 text = self._get_block_plain_text(cursor.block())
934 text = self._get_block_plain_text(cursor.block())
917 return text[len(prompt):]
935 return text[len(prompt):]
918
936
919 def _get_input_buffer_cursor_prompt(self):
937 def _get_input_buffer_cursor_prompt(self):
920 """ Returns the (plain text) prompt for line of the input buffer that
938 """ Returns the (plain text) prompt for line of the input buffer that
921 contains the cursor, or None if there is no such line.
939 contains the cursor, or None if there is no such line.
922 """
940 """
923 if self._executing:
941 if self._executing:
924 return None
942 return None
925 cursor = self._control.textCursor()
943 cursor = self._control.textCursor()
926 if cursor.position() >= self._prompt_pos:
944 if cursor.position() >= self._prompt_pos:
927 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
945 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
928 return self._prompt
946 return self._prompt
929 else:
947 else:
930 return self._continuation_prompt
948 return self._continuation_prompt
931 else:
949 else:
932 return None
950 return None
933
951
934 def _get_prompt_cursor(self):
952 def _get_prompt_cursor(self):
935 """ Convenience method that returns a cursor for the prompt position.
953 """ Convenience method that returns a cursor for the prompt position.
936 """
954 """
937 cursor = self._control.textCursor()
955 cursor = self._control.textCursor()
938 cursor.setPosition(self._prompt_pos)
956 cursor.setPosition(self._prompt_pos)
939 return cursor
957 return cursor
940
958
941 def _get_selection_cursor(self, start, end):
959 def _get_selection_cursor(self, start, end):
942 """ Convenience method that returns a cursor with text selected between
960 """ Convenience method that returns a cursor with text selected between
943 the positions 'start' and 'end'.
961 the positions 'start' and 'end'.
944 """
962 """
945 cursor = self._control.textCursor()
963 cursor = self._control.textCursor()
946 cursor.setPosition(start)
964 cursor.setPosition(start)
947 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
965 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
948 return cursor
966 return cursor
949
967
950 def _get_word_start_cursor(self, position):
968 def _get_word_start_cursor(self, position):
951 """ Find the start of the word to the left the given position. If a
969 """ Find the start of the word to the left the given position. If a
952 sequence of non-word characters precedes the first word, skip over
970 sequence of non-word characters precedes the first word, skip over
953 them. (This emulates the behavior of bash, emacs, etc.)
971 them. (This emulates the behavior of bash, emacs, etc.)
954 """
972 """
955 document = self._control.document()
973 document = self._control.document()
956 position -= 1
974 position -= 1
957 while position >= self._prompt_pos and \
975 while position >= self._prompt_pos and \
958 not document.characterAt(position).isLetterOrNumber():
976 not document.characterAt(position).isLetterOrNumber():
959 position -= 1
977 position -= 1
960 while position >= self._prompt_pos and \
978 while position >= self._prompt_pos and \
961 document.characterAt(position).isLetterOrNumber():
979 document.characterAt(position).isLetterOrNumber():
962 position -= 1
980 position -= 1
963 cursor = self._control.textCursor()
981 cursor = self._control.textCursor()
964 cursor.setPosition(position + 1)
982 cursor.setPosition(position + 1)
965 return cursor
983 return cursor
966
984
967 def _get_word_end_cursor(self, position):
985 def _get_word_end_cursor(self, position):
968 """ Find the end of the word to the right the given position. If a
986 """ Find the end of the word to the right the given position. If a
969 sequence of non-word characters precedes the first word, skip over
987 sequence of non-word characters precedes the first word, skip over
970 them. (This emulates the behavior of bash, emacs, etc.)
988 them. (This emulates the behavior of bash, emacs, etc.)
971 """
989 """
972 document = self._control.document()
990 document = self._control.document()
973 end = self._get_end_cursor().position()
991 end = self._get_end_cursor().position()
974 while position < end and \
992 while position < end and \
975 not document.characterAt(position).isLetterOrNumber():
993 not document.characterAt(position).isLetterOrNumber():
976 position += 1
994 position += 1
977 while position < end and \
995 while position < end and \
978 document.characterAt(position).isLetterOrNumber():
996 document.characterAt(position).isLetterOrNumber():
979 position += 1
997 position += 1
980 cursor = self._control.textCursor()
998 cursor = self._control.textCursor()
981 cursor.setPosition(position)
999 cursor.setPosition(position)
982 return cursor
1000 return cursor
983
1001
1002 def _insert_continuation_prompt(self, cursor):
1003 """ Inserts new continuation prompt using the specified cursor.
1004 """
1005 if self._continuation_prompt_html is None:
1006 self._insert_plain_text(cursor, self._continuation_prompt)
1007 else:
1008 self._continuation_prompt = self._insert_html_fetching_plain_text(
1009 cursor, self._continuation_prompt_html)
1010
984 def _insert_html(self, cursor, html):
1011 def _insert_html(self, cursor, html):
985 """ Inserts HTML using the specified cursor in such a way that future
1012 """ Inserts HTML using the specified cursor in such a way that future
986 formatting is unaffected.
1013 formatting is unaffected.
987 """
1014 """
988 cursor.beginEditBlock()
1015 cursor.beginEditBlock()
989 cursor.insertHtml(html)
1016 cursor.insertHtml(html)
990
1017
991 # After inserting HTML, the text document "remembers" it's in "html
1018 # After inserting HTML, the text document "remembers" it's in "html
992 # mode", which means that subsequent calls adding plain text will result
1019 # mode", which means that subsequent calls adding plain text will result
993 # in unwanted formatting, lost tab characters, etc. The following code
1020 # in unwanted formatting, lost tab characters, etc. The following code
994 # hacks around this behavior, which I consider to be a bug in Qt, by
1021 # hacks around this behavior, which I consider to be a bug in Qt, by
995 # (crudely) resetting the document's style state.
1022 # (crudely) resetting the document's style state.
996 cursor.movePosition(QtGui.QTextCursor.Left,
1023 cursor.movePosition(QtGui.QTextCursor.Left,
997 QtGui.QTextCursor.KeepAnchor)
1024 QtGui.QTextCursor.KeepAnchor)
998 if cursor.selection().toPlainText() == ' ':
1025 if cursor.selection().toPlainText() == ' ':
999 cursor.removeSelectedText()
1026 cursor.removeSelectedText()
1000 else:
1027 else:
1001 cursor.movePosition(QtGui.QTextCursor.Right)
1028 cursor.movePosition(QtGui.QTextCursor.Right)
1002 cursor.insertText(' ', QtGui.QTextCharFormat())
1029 cursor.insertText(' ', QtGui.QTextCharFormat())
1003 cursor.endEditBlock()
1030 cursor.endEditBlock()
1004
1031
1005 def _insert_html_fetching_plain_text(self, cursor, html):
1032 def _insert_html_fetching_plain_text(self, cursor, html):
1006 """ Inserts HTML using the specified cursor, then returns its plain text
1033 """ Inserts HTML using the specified cursor, then returns its plain text
1007 version.
1034 version.
1008 """
1035 """
1009 cursor.beginEditBlock()
1036 cursor.beginEditBlock()
1010 cursor.removeSelectedText()
1037 cursor.removeSelectedText()
1011
1038
1012 start = cursor.position()
1039 start = cursor.position()
1013 self._insert_html(cursor, html)
1040 self._insert_html(cursor, html)
1014 end = cursor.position()
1041 end = cursor.position()
1015 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1042 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1016 text = str(cursor.selection().toPlainText())
1043 text = str(cursor.selection().toPlainText())
1017
1044
1018 cursor.setPosition(end)
1045 cursor.setPosition(end)
1019 cursor.endEditBlock()
1046 cursor.endEditBlock()
1020 return text
1047 return text
1021
1048
1022 def _insert_plain_text(self, cursor, text):
1049 def _insert_plain_text(self, cursor, text):
1023 """ Inserts plain text using the specified cursor, processing ANSI codes
1050 """ Inserts plain text using the specified cursor, processing ANSI codes
1024 if enabled.
1051 if enabled.
1025 """
1052 """
1026 cursor.beginEditBlock()
1053 cursor.beginEditBlock()
1027 if self.ansi_codes:
1054 if self.ansi_codes:
1028 for substring in self._ansi_processor.split_string(text):
1055 for substring in self._ansi_processor.split_string(text):
1029 for action in self._ansi_processor.actions:
1056 for action in self._ansi_processor.actions:
1030 if action.kind == 'erase' and action.area == 'screen':
1057 if action.kind == 'erase' and action.area == 'screen':
1031 cursor.select(QtGui.QTextCursor.Document)
1058 cursor.select(QtGui.QTextCursor.Document)
1032 cursor.removeSelectedText()
1059 cursor.removeSelectedText()
1033 format = self._ansi_processor.get_format()
1060 format = self._ansi_processor.get_format()
1034 cursor.insertText(substring, format)
1061 cursor.insertText(substring, format)
1035 else:
1062 else:
1036 cursor.insertText(text)
1063 cursor.insertText(text)
1037 cursor.endEditBlock()
1064 cursor.endEditBlock()
1038
1065
1039 def _insert_plain_text_into_buffer(self, text):
1066 def _insert_plain_text_into_buffer(self, text):
1040 """ Inserts text into the input buffer at the current cursor position,
1067 """ Inserts text into the input buffer at the current cursor position,
1041 ensuring that continuation prompts are inserted as necessary.
1068 ensuring that continuation prompts are inserted as necessary.
1042 """
1069 """
1043 lines = str(text).splitlines(True)
1070 lines = str(text).splitlines(True)
1044 if lines:
1071 if lines:
1045 self._keep_cursor_in_buffer()
1072 self._keep_cursor_in_buffer()
1046 cursor = self._control.textCursor()
1073 cursor = self._control.textCursor()
1047 cursor.beginEditBlock()
1074 cursor.beginEditBlock()
1048 cursor.insertText(lines[0])
1075 cursor.insertText(lines[0])
1049 for line in lines[1:]:
1076 for line in lines[1:]:
1050 if self._continuation_prompt_html is None:
1077 if self._continuation_prompt_html is None:
1051 cursor.insertText(self._continuation_prompt)
1078 cursor.insertText(self._continuation_prompt)
1052 else:
1079 else:
1053 self._continuation_prompt = \
1080 self._continuation_prompt = \
1054 self._insert_html_fetching_plain_text(
1081 self._insert_html_fetching_plain_text(
1055 cursor, self._continuation_prompt_html)
1082 cursor, self._continuation_prompt_html)
1056 cursor.insertText(line)
1083 cursor.insertText(line)
1057 cursor.endEditBlock()
1084 cursor.endEditBlock()
1058 self._control.setTextCursor(cursor)
1085 self._control.setTextCursor(cursor)
1059
1086
1060 def _in_buffer(self, position=None):
1087 def _in_buffer(self, position=None):
1061 """ Returns whether the current cursor (or, if specified, a position) is
1088 """ Returns whether the current cursor (or, if specified, a position) is
1062 inside the editing region.
1089 inside the editing region.
1063 """
1090 """
1064 cursor = self._control.textCursor()
1091 cursor = self._control.textCursor()
1065 if position is None:
1092 if position is None:
1066 position = cursor.position()
1093 position = cursor.position()
1067 else:
1094 else:
1068 cursor.setPosition(position)
1095 cursor.setPosition(position)
1069 line = cursor.blockNumber()
1096 line = cursor.blockNumber()
1070 prompt_line = self._get_prompt_cursor().blockNumber()
1097 prompt_line = self._get_prompt_cursor().blockNumber()
1071 if line == prompt_line:
1098 if line == prompt_line:
1072 return position >= self._prompt_pos
1099 return position >= self._prompt_pos
1073 elif line > prompt_line:
1100 elif line > prompt_line:
1074 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1101 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1075 prompt_pos = cursor.position() + len(self._continuation_prompt)
1102 prompt_pos = cursor.position() + len(self._continuation_prompt)
1076 return position >= prompt_pos
1103 return position >= prompt_pos
1077 return False
1104 return False
1078
1105
1079 def _keep_cursor_in_buffer(self):
1106 def _keep_cursor_in_buffer(self):
1080 """ Ensures that the cursor is inside the editing region. Returns
1107 """ Ensures that the cursor is inside the editing region. Returns
1081 whether the cursor was moved.
1108 whether the cursor was moved.
1082 """
1109 """
1083 moved = not self._in_buffer()
1110 moved = not self._in_buffer()
1084 if moved:
1111 if moved:
1085 cursor = self._control.textCursor()
1112 cursor = self._control.textCursor()
1086 cursor.movePosition(QtGui.QTextCursor.End)
1113 cursor.movePosition(QtGui.QTextCursor.End)
1087 self._control.setTextCursor(cursor)
1114 self._control.setTextCursor(cursor)
1088 return moved
1115 return moved
1089
1116
1090 def _page(self, text):
1117 def _page(self, text):
1091 """ Displays text using the pager if it exceeds the height of the
1118 """ Displays text using the pager if it exceeds the height of the
1092 visible area.
1119 visible area.
1093 """
1120 """
1094 if self.paging == 'none':
1121 if self.paging == 'none':
1095 self._append_plain_text(text)
1122 self._append_plain_text(text)
1096 else:
1123 else:
1097 line_height = QtGui.QFontMetrics(self.font).height()
1124 line_height = QtGui.QFontMetrics(self.font).height()
1098 minlines = self._control.viewport().height() / line_height
1125 minlines = self._control.viewport().height() / line_height
1099 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1126 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1100 if self.paging == 'custom':
1127 if self.paging == 'custom':
1101 self.custom_page_requested.emit(text)
1128 self.custom_page_requested.emit(text)
1102 else:
1129 else:
1103 self._page_control.clear()
1130 self._page_control.clear()
1104 cursor = self._page_control.textCursor()
1131 cursor = self._page_control.textCursor()
1105 self._insert_plain_text(cursor, text)
1132 self._insert_plain_text(cursor, text)
1106 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1133 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1107
1134
1108 self._page_control.viewport().resize(self._control.size())
1135 self._page_control.viewport().resize(self._control.size())
1109 if self._splitter:
1136 if self._splitter:
1110 self._page_control.show()
1137 self._page_control.show()
1111 self._page_control.setFocus()
1138 self._page_control.setFocus()
1112 else:
1139 else:
1113 self.layout().setCurrentWidget(self._page_control)
1140 self.layout().setCurrentWidget(self._page_control)
1114 else:
1141 else:
1115 self._append_plain_text(text)
1142 self._append_plain_text(text)
1116
1143
1117 def _prompt_started(self):
1144 def _prompt_started(self):
1118 """ Called immediately after a new prompt is displayed.
1145 """ Called immediately after a new prompt is displayed.
1119 """
1146 """
1120 # Temporarily disable the maximum block count to permit undo/redo and
1147 # Temporarily disable the maximum block count to permit undo/redo and
1121 # to ensure that the prompt position does not change due to truncation.
1148 # to ensure that the prompt position does not change due to truncation.
1122 # Because setting this property clears the undo/redo history, we only
1149 # Because setting this property clears the undo/redo history, we only
1123 # set it if we have to.
1150 # set it if we have to.
1124 if self._control.document().maximumBlockCount() > 0:
1151 if self._control.document().maximumBlockCount() > 0:
1125 self._control.document().setMaximumBlockCount(0)
1152 self._control.document().setMaximumBlockCount(0)
1126 self._control.setUndoRedoEnabled(True)
1153 self._control.setUndoRedoEnabled(True)
1127
1154
1128 self._control.setReadOnly(False)
1155 self._control.setReadOnly(False)
1129 self._control.moveCursor(QtGui.QTextCursor.End)
1156 self._control.moveCursor(QtGui.QTextCursor.End)
1130
1157
1131 self._executing = False
1158 self._executing = False
1132 self._prompt_started_hook()
1159 self._prompt_started_hook()
1133
1160
1134 def _prompt_finished(self):
1161 def _prompt_finished(self):
1135 """ Called immediately after a prompt is finished, i.e. when some input
1162 """ Called immediately after a prompt is finished, i.e. when some input
1136 will be processed and a new prompt displayed.
1163 will be processed and a new prompt displayed.
1137 """
1164 """
1138 self._control.setReadOnly(True)
1165 self._control.setReadOnly(True)
1139 self._prompt_finished_hook()
1166 self._prompt_finished_hook()
1140
1167
1141 def _readline(self, prompt='', callback=None):
1168 def _readline(self, prompt='', callback=None):
1142 """ Reads one line of input from the user.
1169 """ Reads one line of input from the user.
1143
1170
1144 Parameters
1171 Parameters
1145 ----------
1172 ----------
1146 prompt : str, optional
1173 prompt : str, optional
1147 The prompt to print before reading the line.
1174 The prompt to print before reading the line.
1148
1175
1149 callback : callable, optional
1176 callback : callable, optional
1150 A callback to execute with the read line. If not specified, input is
1177 A callback to execute with the read line. If not specified, input is
1151 read *synchronously* and this method does not return until it has
1178 read *synchronously* and this method does not return until it has
1152 been read.
1179 been read.
1153
1180
1154 Returns
1181 Returns
1155 -------
1182 -------
1156 If a callback is specified, returns nothing. Otherwise, returns the
1183 If a callback is specified, returns nothing. Otherwise, returns the
1157 input string with the trailing newline stripped.
1184 input string with the trailing newline stripped.
1158 """
1185 """
1159 if self._reading:
1186 if self._reading:
1160 raise RuntimeError('Cannot read a line. Widget is already reading.')
1187 raise RuntimeError('Cannot read a line. Widget is already reading.')
1161
1188
1162 if not callback and not self.isVisible():
1189 if not callback and not self.isVisible():
1163 # If the user cannot see the widget, this function cannot return.
1190 # If the user cannot see the widget, this function cannot return.
1164 raise RuntimeError('Cannot synchronously read a line if the widget '
1191 raise RuntimeError('Cannot synchronously read a line if the widget '
1165 'is not visible!')
1192 'is not visible!')
1166
1193
1167 self._reading = True
1194 self._reading = True
1168 self._show_prompt(prompt, newline=False)
1195 self._show_prompt(prompt, newline=False)
1169
1196
1170 if callback is None:
1197 if callback is None:
1171 self._reading_callback = None
1198 self._reading_callback = None
1172 while self._reading:
1199 while self._reading:
1173 QtCore.QCoreApplication.processEvents()
1200 QtCore.QCoreApplication.processEvents()
1174 return self.input_buffer.rstrip('\n')
1201 return self.input_buffer.rstrip('\n')
1175
1202
1176 else:
1203 else:
1177 self._reading_callback = lambda: \
1204 self._reading_callback = lambda: \
1178 callback(self.input_buffer.rstrip('\n'))
1205 callback(self.input_buffer.rstrip('\n'))
1179
1206
1180 def _set_continuation_prompt(self, prompt, html=False):
1207 def _set_continuation_prompt(self, prompt, html=False):
1181 """ Sets the continuation prompt.
1208 """ Sets the continuation prompt.
1182
1209
1183 Parameters
1210 Parameters
1184 ----------
1211 ----------
1185 prompt : str
1212 prompt : str
1186 The prompt to show when more input is needed.
1213 The prompt to show when more input is needed.
1187
1214
1188 html : bool, optional (default False)
1215 html : bool, optional (default False)
1189 If set, the prompt will be inserted as formatted HTML. Otherwise,
1216 If set, the prompt will be inserted as formatted HTML. Otherwise,
1190 the prompt will be treated as plain text, though ANSI color codes
1217 the prompt will be treated as plain text, though ANSI color codes
1191 will be handled.
1218 will be handled.
1192 """
1219 """
1193 if html:
1220 if html:
1194 self._continuation_prompt_html = prompt
1221 self._continuation_prompt_html = prompt
1195 else:
1222 else:
1196 self._continuation_prompt = prompt
1223 self._continuation_prompt = prompt
1197 self._continuation_prompt_html = None
1224 self._continuation_prompt_html = None
1198
1225
1199 def _set_cursor(self, cursor):
1226 def _set_cursor(self, cursor):
1200 """ Convenience method to set the current cursor.
1227 """ Convenience method to set the current cursor.
1201 """
1228 """
1202 self._control.setTextCursor(cursor)
1229 self._control.setTextCursor(cursor)
1203
1230
1204 def _show_context_menu(self, pos):
1231 def _show_context_menu(self, pos):
1205 """ Shows a context menu at the given QPoint (in widget coordinates).
1232 """ Shows a context menu at the given QPoint (in widget coordinates).
1206 """
1233 """
1207 menu = QtGui.QMenu()
1234 menu = QtGui.QMenu()
1208
1235
1209 copy_action = menu.addAction('Copy', self.copy)
1236 copy_action = menu.addAction('Copy', self.copy)
1210 copy_action.setEnabled(self._get_cursor().hasSelection())
1237 copy_action.setEnabled(self._get_cursor().hasSelection())
1211 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1238 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1212
1239
1213 paste_action = menu.addAction('Paste', self.paste)
1240 paste_action = menu.addAction('Paste', self.paste)
1214 paste_action.setEnabled(self.can_paste())
1241 paste_action.setEnabled(self.can_paste())
1215 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1242 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1216
1243
1217 menu.addSeparator()
1244 menu.addSeparator()
1218 menu.addAction('Select All', self.select_all)
1245 menu.addAction('Select All', self.select_all)
1219
1246
1220 menu.exec_(self._control.mapToGlobal(pos))
1247 menu.exec_(self._control.mapToGlobal(pos))
1221
1248
1222 def _show_prompt(self, prompt=None, html=False, newline=True):
1249 def _show_prompt(self, prompt=None, html=False, newline=True):
1223 """ Writes a new prompt at the end of the buffer.
1250 """ Writes a new prompt at the end of the buffer.
1224
1251
1225 Parameters
1252 Parameters
1226 ----------
1253 ----------
1227 prompt : str, optional
1254 prompt : str, optional
1228 The prompt to show. If not specified, the previous prompt is used.
1255 The prompt to show. If not specified, the previous prompt is used.
1229
1256
1230 html : bool, optional (default False)
1257 html : bool, optional (default False)
1231 Only relevant when a prompt is specified. If set, the prompt will
1258 Only relevant when a prompt is specified. If set, the prompt will
1232 be inserted as formatted HTML. Otherwise, the prompt will be treated
1259 be inserted as formatted HTML. Otherwise, the prompt will be treated
1233 as plain text, though ANSI color codes will be handled.
1260 as plain text, though ANSI color codes will be handled.
1234
1261
1235 newline : bool, optional (default True)
1262 newline : bool, optional (default True)
1236 If set, a new line will be written before showing the prompt if
1263 If set, a new line will be written before showing the prompt if
1237 there is not already a newline at the end of the buffer.
1264 there is not already a newline at the end of the buffer.
1238 """
1265 """
1239 # Insert a preliminary newline, if necessary.
1266 # Insert a preliminary newline, if necessary.
1240 if newline:
1267 if newline:
1241 cursor = self._get_end_cursor()
1268 cursor = self._get_end_cursor()
1242 if cursor.position() > 0:
1269 if cursor.position() > 0:
1243 cursor.movePosition(QtGui.QTextCursor.Left,
1270 cursor.movePosition(QtGui.QTextCursor.Left,
1244 QtGui.QTextCursor.KeepAnchor)
1271 QtGui.QTextCursor.KeepAnchor)
1245 if str(cursor.selection().toPlainText()) != '\n':
1272 if str(cursor.selection().toPlainText()) != '\n':
1246 self._append_plain_text('\n')
1273 self._append_plain_text('\n')
1247
1274
1248 # Write the prompt.
1275 # Write the prompt.
1249 self._append_plain_text(self._prompt_sep)
1276 self._append_plain_text(self._prompt_sep)
1250 if prompt is None:
1277 if prompt is None:
1251 if self._prompt_html is None:
1278 if self._prompt_html is None:
1252 self._append_plain_text(self._prompt)
1279 self._append_plain_text(self._prompt)
1253 else:
1280 else:
1254 self._append_html(self._prompt_html)
1281 self._append_html(self._prompt_html)
1255 else:
1282 else:
1256 if html:
1283 if html:
1257 self._prompt = self._append_html_fetching_plain_text(prompt)
1284 self._prompt = self._append_html_fetching_plain_text(prompt)
1258 self._prompt_html = prompt
1285 self._prompt_html = prompt
1259 else:
1286 else:
1260 self._append_plain_text(prompt)
1287 self._append_plain_text(prompt)
1261 self._prompt = prompt
1288 self._prompt = prompt
1262 self._prompt_html = None
1289 self._prompt_html = None
1263
1290
1264 self._prompt_pos = self._get_end_cursor().position()
1291 self._prompt_pos = self._get_end_cursor().position()
1265 self._prompt_started()
1292 self._prompt_started()
1266
1293
1267 def _show_continuation_prompt(self):
1268 """ Writes a new continuation prompt at the end of the buffer.
1269 """
1270 if self._continuation_prompt_html is None:
1271 self._append_plain_text(self._continuation_prompt)
1272 else:
1273 self._continuation_prompt = self._append_html_fetching_plain_text(
1274 self._continuation_prompt_html)
1275
1276
1294
1277 class HistoryConsoleWidget(ConsoleWidget):
1295 class HistoryConsoleWidget(ConsoleWidget):
1278 """ A ConsoleWidget that keeps a history of the commands that have been
1296 """ A ConsoleWidget that keeps a history of the commands that have been
1279 executed.
1297 executed.
1280 """
1298 """
1281
1299
1282 #---------------------------------------------------------------------------
1300 #---------------------------------------------------------------------------
1283 # 'object' interface
1301 # 'object' interface
1284 #---------------------------------------------------------------------------
1302 #---------------------------------------------------------------------------
1285
1303
1286 def __init__(self, *args, **kw):
1304 def __init__(self, *args, **kw):
1287 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1305 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1288 self._history = []
1306 self._history = []
1289 self._history_index = 0
1307 self._history_index = 0
1290
1308
1291 #---------------------------------------------------------------------------
1309 #---------------------------------------------------------------------------
1292 # 'ConsoleWidget' public interface
1310 # 'ConsoleWidget' public interface
1293 #---------------------------------------------------------------------------
1311 #---------------------------------------------------------------------------
1294
1312
1295 def execute(self, source=None, hidden=False, interactive=False):
1313 def execute(self, source=None, hidden=False, interactive=False):
1296 """ Reimplemented to the store history.
1314 """ Reimplemented to the store history.
1297 """
1315 """
1298 if not hidden:
1316 if not hidden:
1299 history = self.input_buffer if source is None else source
1317 history = self.input_buffer if source is None else source
1300
1318
1301 executed = super(HistoryConsoleWidget, self).execute(
1319 executed = super(HistoryConsoleWidget, self).execute(
1302 source, hidden, interactive)
1320 source, hidden, interactive)
1303
1321
1304 if executed and not hidden:
1322 if executed and not hidden:
1305 # Save the command unless it was a blank line.
1323 # Save the command unless it was a blank line.
1306 history = history.rstrip()
1324 history = history.rstrip()
1307 if history:
1325 if history:
1308 self._history.append(history)
1326 self._history.append(history)
1309 self._history_index = len(self._history)
1327 self._history_index = len(self._history)
1310
1328
1311 return executed
1329 return executed
1312
1330
1313 #---------------------------------------------------------------------------
1331 #---------------------------------------------------------------------------
1314 # 'ConsoleWidget' abstract interface
1332 # 'ConsoleWidget' abstract interface
1315 #---------------------------------------------------------------------------
1333 #---------------------------------------------------------------------------
1316
1334
1317 def _up_pressed(self):
1335 def _up_pressed(self):
1318 """ Called when the up key is pressed. Returns whether to continue
1336 """ Called when the up key is pressed. Returns whether to continue
1319 processing the event.
1337 processing the event.
1320 """
1338 """
1321 prompt_cursor = self._get_prompt_cursor()
1339 prompt_cursor = self._get_prompt_cursor()
1322 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1340 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1323 self.history_previous()
1341 self.history_previous()
1324
1342
1325 # Go to the first line of prompt for seemless history scrolling.
1343 # Go to the first line of prompt for seemless history scrolling.
1326 cursor = self._get_prompt_cursor()
1344 cursor = self._get_prompt_cursor()
1327 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1345 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1328 self._set_cursor(cursor)
1346 self._set_cursor(cursor)
1329
1347
1330 return False
1348 return False
1331 return True
1349 return True
1332
1350
1333 def _down_pressed(self):
1351 def _down_pressed(self):
1334 """ Called when the down key is pressed. Returns whether to continue
1352 """ Called when the down key is pressed. Returns whether to continue
1335 processing the event.
1353 processing the event.
1336 """
1354 """
1337 end_cursor = self._get_end_cursor()
1355 end_cursor = self._get_end_cursor()
1338 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1356 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1339 self.history_next()
1357 self.history_next()
1340 return False
1358 return False
1341 return True
1359 return True
1342
1360
1343 #---------------------------------------------------------------------------
1361 #---------------------------------------------------------------------------
1344 # 'HistoryConsoleWidget' public interface
1362 # 'HistoryConsoleWidget' public interface
1345 #---------------------------------------------------------------------------
1363 #---------------------------------------------------------------------------
1346
1364
1347 def history_previous(self):
1365 def history_previous(self):
1348 """ If possible, set the input buffer to the previous item in the
1366 """ If possible, set the input buffer to the previous item in the
1349 history.
1367 history.
1350 """
1368 """
1351 if self._history_index > 0:
1369 if self._history_index > 0:
1352 self._history_index -= 1
1370 self._history_index -= 1
1353 self.input_buffer = self._history[self._history_index]
1371 self.input_buffer = self._history[self._history_index]
1354
1372
1355 def history_next(self):
1373 def history_next(self):
1356 """ Set the input buffer to the next item in the history, or a blank
1374 """ Set the input buffer to the next item in the history, or a blank
1357 line if there is no subsequent item.
1375 line if there is no subsequent item.
1358 """
1376 """
1359 if self._history_index < len(self._history):
1377 if self._history_index < len(self._history):
1360 self._history_index += 1
1378 self._history_index += 1
1361 if self._history_index < len(self._history):
1379 if self._history_index < len(self._history):
1362 self.input_buffer = self._history[self._history_index]
1380 self.input_buffer = self._history[self._history_index]
1363 else:
1381 else:
1364 self.input_buffer = ''
1382 self.input_buffer = ''
1365
1383
1366 #---------------------------------------------------------------------------
1384 #---------------------------------------------------------------------------
1367 # 'HistoryConsoleWidget' protected interface
1385 # 'HistoryConsoleWidget' protected interface
1368 #---------------------------------------------------------------------------
1386 #---------------------------------------------------------------------------
1369
1387
1370 def _set_history(self, history):
1388 def _set_history(self, history):
1371 """ Replace the current history with a sequence of history items.
1389 """ Replace the current history with a sequence of history items.
1372 """
1390 """
1373 self._history = list(history)
1391 self._history = list(history)
1374 self._history_index = len(self._history)
1392 self._history_index = len(self._history)
@@ -1,420 +1,420 b''
1 # Standard library imports
1 # Standard library imports
2 import signal
2 import signal
3 import sys
3 import sys
4
4
5 # System library imports
5 # System library imports
6 from pygments.lexers import PythonLexer
6 from pygments.lexers import PythonLexer
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8
8
9 # Local imports
9 # Local imports
10 from IPython.core.inputsplitter import InputSplitter
10 from IPython.core.inputsplitter import InputSplitter
11 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
11 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 from IPython.utils.traitlets import Bool
12 from IPython.utils.traitlets import Bool
13 from bracket_matcher import BracketMatcher
13 from bracket_matcher import BracketMatcher
14 from call_tip_widget import CallTipWidget
14 from call_tip_widget import CallTipWidget
15 from completion_lexer import CompletionLexer
15 from completion_lexer import CompletionLexer
16 from console_widget import HistoryConsoleWidget
16 from console_widget import HistoryConsoleWidget
17 from pygments_highlighter import PygmentsHighlighter
17 from pygments_highlighter import PygmentsHighlighter
18
18
19
19
20 class FrontendHighlighter(PygmentsHighlighter):
20 class FrontendHighlighter(PygmentsHighlighter):
21 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 """ A PygmentsHighlighter that can be turned on and off and that ignores
22 prompts.
22 prompts.
23 """
23 """
24
24
25 def __init__(self, frontend):
25 def __init__(self, frontend):
26 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 super(FrontendHighlighter, self).__init__(frontend._control.document())
27 self._current_offset = 0
27 self._current_offset = 0
28 self._frontend = frontend
28 self._frontend = frontend
29 self.highlighting_on = False
29 self.highlighting_on = False
30
30
31 def highlightBlock(self, qstring):
31 def highlightBlock(self, qstring):
32 """ Highlight a block of text. Reimplemented to highlight selectively.
32 """ Highlight a block of text. Reimplemented to highlight selectively.
33 """
33 """
34 if not self.highlighting_on:
34 if not self.highlighting_on:
35 return
35 return
36
36
37 # The input to this function is unicode string that may contain
37 # The input to this function is unicode string that may contain
38 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 # paragraph break characters, non-breaking spaces, etc. Here we acquire
39 # the string as plain text so we can compare it.
39 # the string as plain text so we can compare it.
40 current_block = self.currentBlock()
40 current_block = self.currentBlock()
41 string = self._frontend._get_block_plain_text(current_block)
41 string = self._frontend._get_block_plain_text(current_block)
42
42
43 # Decide whether to check for the regular or continuation prompt.
43 # Decide whether to check for the regular or continuation prompt.
44 if current_block.contains(self._frontend._prompt_pos):
44 if current_block.contains(self._frontend._prompt_pos):
45 prompt = self._frontend._prompt
45 prompt = self._frontend._prompt
46 else:
46 else:
47 prompt = self._frontend._continuation_prompt
47 prompt = self._frontend._continuation_prompt
48
48
49 # Don't highlight the part of the string that contains the prompt.
49 # Don't highlight the part of the string that contains the prompt.
50 if string.startswith(prompt):
50 if string.startswith(prompt):
51 self._current_offset = len(prompt)
51 self._current_offset = len(prompt)
52 qstring.remove(0, len(prompt))
52 qstring.remove(0, len(prompt))
53 else:
53 else:
54 self._current_offset = 0
54 self._current_offset = 0
55
55
56 PygmentsHighlighter.highlightBlock(self, qstring)
56 PygmentsHighlighter.highlightBlock(self, qstring)
57
57
58 def rehighlightBlock(self, block):
58 def rehighlightBlock(self, block):
59 """ Reimplemented to temporarily enable highlighting if disabled.
59 """ Reimplemented to temporarily enable highlighting if disabled.
60 """
60 """
61 old = self.highlighting_on
61 old = self.highlighting_on
62 self.highlighting_on = True
62 self.highlighting_on = True
63 super(FrontendHighlighter, self).rehighlightBlock(block)
63 super(FrontendHighlighter, self).rehighlightBlock(block)
64 self.highlighting_on = old
64 self.highlighting_on = old
65
65
66 def setFormat(self, start, count, format):
66 def setFormat(self, start, count, format):
67 """ Reimplemented to highlight selectively.
67 """ Reimplemented to highlight selectively.
68 """
68 """
69 start += self._current_offset
69 start += self._current_offset
70 PygmentsHighlighter.setFormat(self, start, count, format)
70 PygmentsHighlighter.setFormat(self, start, count, format)
71
71
72
72
73 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
73 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
74 """ A Qt frontend for a generic Python kernel.
74 """ A Qt frontend for a generic Python kernel.
75 """
75 """
76
76
77 # An option and corresponding signal for overriding the default kernel
77 # An option and corresponding signal for overriding the default kernel
78 # interrupt behavior.
78 # interrupt behavior.
79 custom_interrupt = Bool(False)
79 custom_interrupt = Bool(False)
80 custom_interrupt_requested = QtCore.pyqtSignal()
80 custom_interrupt_requested = QtCore.pyqtSignal()
81
81
82 # An option and corresponding signal for overriding the default kernel
82 # An option and corresponding signal for overriding the default kernel
83 # restart behavior.
83 # restart behavior.
84 custom_restart = Bool(False)
84 custom_restart = Bool(False)
85 custom_restart_requested = QtCore.pyqtSignal()
85 custom_restart_requested = QtCore.pyqtSignal()
86
86
87 # Emitted when an 'execute_reply' has been received from the kernel and
87 # Emitted when an 'execute_reply' has been received from the kernel and
88 # processed by the FrontendWidget.
88 # processed by the FrontendWidget.
89 executed = QtCore.pyqtSignal(object)
89 executed = QtCore.pyqtSignal(object)
90
90
91 # Protected class variables.
91 # Protected class variables.
92 _input_splitter_class = InputSplitter
92 _input_splitter_class = InputSplitter
93
93
94 #---------------------------------------------------------------------------
94 #---------------------------------------------------------------------------
95 # 'object' interface
95 # 'object' interface
96 #---------------------------------------------------------------------------
96 #---------------------------------------------------------------------------
97
97
98 def __init__(self, *args, **kw):
98 def __init__(self, *args, **kw):
99 super(FrontendWidget, self).__init__(*args, **kw)
99 super(FrontendWidget, self).__init__(*args, **kw)
100
100
101 # FrontendWidget protected variables.
101 # FrontendWidget protected variables.
102 self._bracket_matcher = BracketMatcher(self._control)
102 self._bracket_matcher = BracketMatcher(self._control)
103 self._call_tip_widget = CallTipWidget(self._control)
103 self._call_tip_widget = CallTipWidget(self._control)
104 self._completion_lexer = CompletionLexer(PythonLexer())
104 self._completion_lexer = CompletionLexer(PythonLexer())
105 self._hidden = False
105 self._hidden = False
106 self._highlighter = FrontendHighlighter(self)
106 self._highlighter = FrontendHighlighter(self)
107 self._input_splitter = self._input_splitter_class(input_mode='block')
107 self._input_splitter = self._input_splitter_class(input_mode='block')
108 self._kernel_manager = None
108 self._kernel_manager = None
109
109
110 # Configure the ConsoleWidget.
110 # Configure the ConsoleWidget.
111 self.tab_width = 4
111 self.tab_width = 4
112 self._set_continuation_prompt('... ')
112 self._set_continuation_prompt('... ')
113
113
114 # Connect signal handlers.
114 # Connect signal handlers.
115 document = self._control.document()
115 document = self._control.document()
116 document.contentsChange.connect(self._document_contents_change)
116 document.contentsChange.connect(self._document_contents_change)
117
117
118 #---------------------------------------------------------------------------
118 #---------------------------------------------------------------------------
119 # 'ConsoleWidget' abstract interface
119 # 'ConsoleWidget' abstract interface
120 #---------------------------------------------------------------------------
120 #---------------------------------------------------------------------------
121
121
122 def _is_complete(self, source, interactive):
122 def _is_complete(self, source, interactive):
123 """ Returns whether 'source' can be completely processed and a new
123 """ Returns whether 'source' can be completely processed and a new
124 prompt created. When triggered by an Enter/Return key press,
124 prompt created. When triggered by an Enter/Return key press,
125 'interactive' is True; otherwise, it is False.
125 'interactive' is True; otherwise, it is False.
126 """
126 """
127 complete = self._input_splitter.push(source.expandtabs(4))
127 complete = self._input_splitter.push(source.expandtabs(4))
128 if interactive:
128 if interactive:
129 complete = not self._input_splitter.push_accepts_more()
129 complete = not self._input_splitter.push_accepts_more()
130 return complete
130 return complete
131
131
132 def _execute(self, source, hidden):
132 def _execute(self, source, hidden):
133 """ Execute 'source'. If 'hidden', do not show any output.
133 """ Execute 'source'. If 'hidden', do not show any output.
134 """
134 """
135 self.kernel_manager.xreq_channel.execute(source, hidden)
135 self.kernel_manager.xreq_channel.execute(source, hidden)
136 self._hidden = hidden
136 self._hidden = hidden
137
137
138 def _prompt_started_hook(self):
138 def _prompt_started_hook(self):
139 """ Called immediately after a new prompt is displayed.
139 """ Called immediately after a new prompt is displayed.
140 """
140 """
141 if not self._reading:
141 if not self._reading:
142 self._highlighter.highlighting_on = True
142 self._highlighter.highlighting_on = True
143
143
144 def _prompt_finished_hook(self):
144 def _prompt_finished_hook(self):
145 """ Called immediately after a prompt is finished, i.e. when some input
145 """ Called immediately after a prompt is finished, i.e. when some input
146 will be processed and a new prompt displayed.
146 will be processed and a new prompt displayed.
147 """
147 """
148 if not self._reading:
148 if not self._reading:
149 self._highlighter.highlighting_on = False
149 self._highlighter.highlighting_on = False
150
150
151 def _tab_pressed(self):
151 def _tab_pressed(self):
152 """ Called when the tab key is pressed. Returns whether to continue
152 """ Called when the tab key is pressed. Returns whether to continue
153 processing the event.
153 processing the event.
154 """
154 """
155 # Perform tab completion if:
155 # Perform tab completion if:
156 # 1) The cursor is in the input buffer.
156 # 1) The cursor is in the input buffer.
157 # 2) There is a non-whitespace character before the cursor.
157 # 2) There is a non-whitespace character before the cursor.
158 text = self._get_input_buffer_cursor_line()
158 text = self._get_input_buffer_cursor_line()
159 if text is None:
159 if text is None:
160 return False
160 return False
161 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
161 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
162 if complete:
162 if complete:
163 self._complete()
163 self._complete()
164 return not complete
164 return not complete
165
165
166 #---------------------------------------------------------------------------
166 #---------------------------------------------------------------------------
167 # 'ConsoleWidget' protected interface
167 # 'ConsoleWidget' protected interface
168 #---------------------------------------------------------------------------
168 #---------------------------------------------------------------------------
169
169
170 def _event_filter_console_keypress(self, event):
170 def _event_filter_console_keypress(self, event):
171 """ Reimplemented to allow execution interruption.
171 """ Reimplemented to allow execution interruption.
172 """
172 """
173 key = event.key()
173 key = event.key()
174 if self._executing and self._control_key_down(event.modifiers()):
174 if self._executing and self._control_key_down(event.modifiers()):
175 if key == QtCore.Qt.Key_C:
175 if key == QtCore.Qt.Key_C:
176 self._kernel_interrupt()
176 self._kernel_interrupt()
177 return True
177 return True
178 elif key == QtCore.Qt.Key_Period:
178 elif key == QtCore.Qt.Key_Period:
179 self._kernel_restart()
179 self._kernel_restart()
180 return True
180 return True
181 return super(FrontendWidget, self)._event_filter_console_keypress(event)
181 return super(FrontendWidget, self)._event_filter_console_keypress(event)
182
182
183 def _show_continuation_prompt(self):
183 def _insert_continuation_prompt(self, cursor):
184 """ Reimplemented for auto-indentation.
184 """ Reimplemented for auto-indentation.
185 """
185 """
186 super(FrontendWidget, self)._show_continuation_prompt()
186 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
187 spaces = self._input_splitter.indent_spaces
187 spaces = self._input_splitter.indent_spaces
188 self._append_plain_text('\t' * (spaces / self.tab_width))
188 cursor.insertText('\t' * (spaces / self.tab_width))
189 self._append_plain_text(' ' * (spaces % self.tab_width))
189 cursor.insertText(' ' * (spaces % self.tab_width))
190
190
191 #---------------------------------------------------------------------------
191 #---------------------------------------------------------------------------
192 # 'BaseFrontendMixin' abstract interface
192 # 'BaseFrontendMixin' abstract interface
193 #---------------------------------------------------------------------------
193 #---------------------------------------------------------------------------
194
194
195 def _handle_complete_reply(self, rep):
195 def _handle_complete_reply(self, rep):
196 """ Handle replies for tab completion.
196 """ Handle replies for tab completion.
197 """
197 """
198 cursor = self._get_cursor()
198 cursor = self._get_cursor()
199 if rep['parent_header']['msg_id'] == self._complete_id and \
199 if rep['parent_header']['msg_id'] == self._complete_id and \
200 cursor.position() == self._complete_pos:
200 cursor.position() == self._complete_pos:
201 text = '.'.join(self._get_context())
201 text = '.'.join(self._get_context())
202 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
202 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
203 self._complete_with_items(cursor, rep['content']['matches'])
203 self._complete_with_items(cursor, rep['content']['matches'])
204
204
205 def _handle_execute_reply(self, msg):
205 def _handle_execute_reply(self, msg):
206 """ Handles replies for code execution.
206 """ Handles replies for code execution.
207 """
207 """
208 if not self._hidden:
208 if not self._hidden:
209 # Make sure that all output from the SUB channel has been processed
209 # Make sure that all output from the SUB channel has been processed
210 # before writing a new prompt.
210 # before writing a new prompt.
211 self.kernel_manager.sub_channel.flush()
211 self.kernel_manager.sub_channel.flush()
212
212
213 content = msg['content']
213 content = msg['content']
214 status = content['status']
214 status = content['status']
215 if status == 'ok':
215 if status == 'ok':
216 self._process_execute_ok(msg)
216 self._process_execute_ok(msg)
217 elif status == 'error':
217 elif status == 'error':
218 self._process_execute_error(msg)
218 self._process_execute_error(msg)
219 elif status == 'abort':
219 elif status == 'abort':
220 self._process_execute_abort(msg)
220 self._process_execute_abort(msg)
221
221
222 self._show_interpreter_prompt_for_reply(msg)
222 self._show_interpreter_prompt_for_reply(msg)
223 self.executed.emit(msg)
223 self.executed.emit(msg)
224
224
225 def _handle_input_request(self, msg):
225 def _handle_input_request(self, msg):
226 """ Handle requests for raw_input.
226 """ Handle requests for raw_input.
227 """
227 """
228 if self._hidden:
228 if self._hidden:
229 raise RuntimeError('Request for raw input during hidden execution.')
229 raise RuntimeError('Request for raw input during hidden execution.')
230
230
231 # Make sure that all output from the SUB channel has been processed
231 # Make sure that all output from the SUB channel has been processed
232 # before entering readline mode.
232 # before entering readline mode.
233 self.kernel_manager.sub_channel.flush()
233 self.kernel_manager.sub_channel.flush()
234
234
235 def callback(line):
235 def callback(line):
236 self.kernel_manager.rep_channel.input(line)
236 self.kernel_manager.rep_channel.input(line)
237 self._readline(msg['content']['prompt'], callback=callback)
237 self._readline(msg['content']['prompt'], callback=callback)
238
238
239 def _handle_object_info_reply(self, rep):
239 def _handle_object_info_reply(self, rep):
240 """ Handle replies for call tips.
240 """ Handle replies for call tips.
241 """
241 """
242 cursor = self._get_cursor()
242 cursor = self._get_cursor()
243 if rep['parent_header']['msg_id'] == self._call_tip_id and \
243 if rep['parent_header']['msg_id'] == self._call_tip_id and \
244 cursor.position() == self._call_tip_pos:
244 cursor.position() == self._call_tip_pos:
245 doc = rep['content']['docstring']
245 doc = rep['content']['docstring']
246 if doc:
246 if doc:
247 self._call_tip_widget.show_docstring(doc)
247 self._call_tip_widget.show_docstring(doc)
248
248
249 def _handle_pyout(self, msg):
249 def _handle_pyout(self, msg):
250 """ Handle display hook output.
250 """ Handle display hook output.
251 """
251 """
252 if not self._hidden and self._is_from_this_session(msg):
252 if not self._hidden and self._is_from_this_session(msg):
253 self._append_plain_text(msg['content']['data'] + '\n')
253 self._append_plain_text(msg['content']['data'] + '\n')
254
254
255 def _handle_stream(self, msg):
255 def _handle_stream(self, msg):
256 """ Handle stdout, stderr, and stdin.
256 """ Handle stdout, stderr, and stdin.
257 """
257 """
258 if not self._hidden and self._is_from_this_session(msg):
258 if not self._hidden and self._is_from_this_session(msg):
259 self._append_plain_text(msg['content']['data'])
259 self._append_plain_text(msg['content']['data'])
260 self._control.moveCursor(QtGui.QTextCursor.End)
260 self._control.moveCursor(QtGui.QTextCursor.End)
261
261
262 def _started_channels(self):
262 def _started_channels(self):
263 """ Called when the KernelManager channels have started listening or
263 """ Called when the KernelManager channels have started listening or
264 when the frontend is assigned an already listening KernelManager.
264 when the frontend is assigned an already listening KernelManager.
265 """
265 """
266 self._control.clear()
266 self._control.clear()
267 self._append_plain_text(self._get_banner())
267 self._append_plain_text(self._get_banner())
268 self._show_interpreter_prompt()
268 self._show_interpreter_prompt()
269
269
270 def _stopped_channels(self):
270 def _stopped_channels(self):
271 """ Called when the KernelManager channels have stopped listening or
271 """ Called when the KernelManager channels have stopped listening or
272 when a listening KernelManager is removed from the frontend.
272 when a listening KernelManager is removed from the frontend.
273 """
273 """
274 self._executing = self._reading = False
274 self._executing = self._reading = False
275 self._highlighter.highlighting_on = False
275 self._highlighter.highlighting_on = False
276
276
277 #---------------------------------------------------------------------------
277 #---------------------------------------------------------------------------
278 # 'FrontendWidget' interface
278 # 'FrontendWidget' interface
279 #---------------------------------------------------------------------------
279 #---------------------------------------------------------------------------
280
280
281 def execute_file(self, path, hidden=False):
281 def execute_file(self, path, hidden=False):
282 """ Attempts to execute file with 'path'. If 'hidden', no output is
282 """ Attempts to execute file with 'path'. If 'hidden', no output is
283 shown.
283 shown.
284 """
284 """
285 self.execute('execfile("%s")' % path, hidden=hidden)
285 self.execute('execfile("%s")' % path, hidden=hidden)
286
286
287 #---------------------------------------------------------------------------
287 #---------------------------------------------------------------------------
288 # 'FrontendWidget' protected interface
288 # 'FrontendWidget' protected interface
289 #---------------------------------------------------------------------------
289 #---------------------------------------------------------------------------
290
290
291 def _call_tip(self):
291 def _call_tip(self):
292 """ Shows a call tip, if appropriate, at the current cursor location.
292 """ Shows a call tip, if appropriate, at the current cursor location.
293 """
293 """
294 # Decide if it makes sense to show a call tip
294 # Decide if it makes sense to show a call tip
295 cursor = self._get_cursor()
295 cursor = self._get_cursor()
296 cursor.movePosition(QtGui.QTextCursor.Left)
296 cursor.movePosition(QtGui.QTextCursor.Left)
297 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
297 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
298 return False
298 return False
299 context = self._get_context(cursor)
299 context = self._get_context(cursor)
300 if not context:
300 if not context:
301 return False
301 return False
302
302
303 # Send the metadata request to the kernel
303 # Send the metadata request to the kernel
304 name = '.'.join(context)
304 name = '.'.join(context)
305 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
305 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
306 self._call_tip_pos = self._get_cursor().position()
306 self._call_tip_pos = self._get_cursor().position()
307 return True
307 return True
308
308
309 def _complete(self):
309 def _complete(self):
310 """ Performs completion at the current cursor location.
310 """ Performs completion at the current cursor location.
311 """
311 """
312 context = self._get_context()
312 context = self._get_context()
313 if context:
313 if context:
314 # Send the completion request to the kernel
314 # Send the completion request to the kernel
315 self._complete_id = self.kernel_manager.xreq_channel.complete(
315 self._complete_id = self.kernel_manager.xreq_channel.complete(
316 '.'.join(context), # text
316 '.'.join(context), # text
317 self._get_input_buffer_cursor_line(), # line
317 self._get_input_buffer_cursor_line(), # line
318 self._get_input_buffer_cursor_column(), # cursor_pos
318 self._get_input_buffer_cursor_column(), # cursor_pos
319 self.input_buffer) # block
319 self.input_buffer) # block
320 self._complete_pos = self._get_cursor().position()
320 self._complete_pos = self._get_cursor().position()
321
321
322 def _get_banner(self):
322 def _get_banner(self):
323 """ Gets a banner to display at the beginning of a session.
323 """ Gets a banner to display at the beginning of a session.
324 """
324 """
325 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
325 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
326 '"license" for more information.'
326 '"license" for more information.'
327 return banner % (sys.version, sys.platform)
327 return banner % (sys.version, sys.platform)
328
328
329 def _get_context(self, cursor=None):
329 def _get_context(self, cursor=None):
330 """ Gets the context for the specified cursor (or the current cursor
330 """ Gets the context for the specified cursor (or the current cursor
331 if none is specified).
331 if none is specified).
332 """
332 """
333 if cursor is None:
333 if cursor is None:
334 cursor = self._get_cursor()
334 cursor = self._get_cursor()
335 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
335 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
336 QtGui.QTextCursor.KeepAnchor)
336 QtGui.QTextCursor.KeepAnchor)
337 text = str(cursor.selection().toPlainText())
337 text = str(cursor.selection().toPlainText())
338 return self._completion_lexer.get_context(text)
338 return self._completion_lexer.get_context(text)
339
339
340 def _kernel_interrupt(self):
340 def _kernel_interrupt(self):
341 """ Attempts to interrupt the running kernel.
341 """ Attempts to interrupt the running kernel.
342 """
342 """
343 if self.custom_interrupt:
343 if self.custom_interrupt:
344 self.custom_interrupt_requested.emit()
344 self.custom_interrupt_requested.emit()
345 elif self.kernel_manager.has_kernel:
345 elif self.kernel_manager.has_kernel:
346 self.kernel_manager.signal_kernel(signal.SIGINT)
346 self.kernel_manager.signal_kernel(signal.SIGINT)
347 else:
347 else:
348 self._append_plain_text('Kernel process is either remote or '
348 self._append_plain_text('Kernel process is either remote or '
349 'unspecified. Cannot interrupt.\n')
349 'unspecified. Cannot interrupt.\n')
350
350
351 def _kernel_restart(self):
351 def _kernel_restart(self):
352 """ Attempts to restart the running kernel.
352 """ Attempts to restart the running kernel.
353 """
353 """
354 if self.custom_restart:
354 if self.custom_restart:
355 self.custom_restart_requested.emit()
355 self.custom_restart_requested.emit()
356 elif self.kernel_manager.has_kernel:
356 elif self.kernel_manager.has_kernel:
357 try:
357 try:
358 self.kernel_manager.restart_kernel()
358 self.kernel_manager.restart_kernel()
359 except RuntimeError:
359 except RuntimeError:
360 message = 'Kernel started externally. Cannot restart.\n'
360 message = 'Kernel started externally. Cannot restart.\n'
361 self._append_plain_text(message)
361 self._append_plain_text(message)
362 else:
362 else:
363 self._stopped_channels()
363 self._stopped_channels()
364 self._append_plain_text('Kernel restarting...\n')
364 self._append_plain_text('Kernel restarting...\n')
365 self._show_interpreter_prompt()
365 self._show_interpreter_prompt()
366 else:
366 else:
367 self._append_plain_text('Kernel process is either remote or '
367 self._append_plain_text('Kernel process is either remote or '
368 'unspecified. Cannot restart.\n')
368 'unspecified. Cannot restart.\n')
369
369
370 def _process_execute_abort(self, msg):
370 def _process_execute_abort(self, msg):
371 """ Process a reply for an aborted execution request.
371 """ Process a reply for an aborted execution request.
372 """
372 """
373 self._append_plain_text("ERROR: execution aborted\n")
373 self._append_plain_text("ERROR: execution aborted\n")
374
374
375 def _process_execute_error(self, msg):
375 def _process_execute_error(self, msg):
376 """ Process a reply for an execution request that resulted in an error.
376 """ Process a reply for an execution request that resulted in an error.
377 """
377 """
378 content = msg['content']
378 content = msg['content']
379 traceback = ''.join(content['traceback'])
379 traceback = ''.join(content['traceback'])
380 self._append_plain_text(traceback)
380 self._append_plain_text(traceback)
381
381
382 def _process_execute_ok(self, msg):
382 def _process_execute_ok(self, msg):
383 """ Process a reply for a successful execution equest.
383 """ Process a reply for a successful execution equest.
384 """
384 """
385 payload = msg['content']['payload']
385 payload = msg['content']['payload']
386 for item in payload:
386 for item in payload:
387 if not self._process_execute_payload(item):
387 if not self._process_execute_payload(item):
388 warning = 'Received unknown payload of type %s\n'
388 warning = 'Received unknown payload of type %s\n'
389 self._append_plain_text(warning % repr(item['source']))
389 self._append_plain_text(warning % repr(item['source']))
390
390
391 def _process_execute_payload(self, item):
391 def _process_execute_payload(self, item):
392 """ Process a single payload item from the list of payload items in an
392 """ Process a single payload item from the list of payload items in an
393 execution reply. Returns whether the payload was handled.
393 execution reply. Returns whether the payload was handled.
394 """
394 """
395 # The basic FrontendWidget doesn't handle payloads, as they are a
395 # The basic FrontendWidget doesn't handle payloads, as they are a
396 # mechanism for going beyond the standard Python interpreter model.
396 # mechanism for going beyond the standard Python interpreter model.
397 return False
397 return False
398
398
399 def _show_interpreter_prompt(self):
399 def _show_interpreter_prompt(self):
400 """ Shows a prompt for the interpreter.
400 """ Shows a prompt for the interpreter.
401 """
401 """
402 self._show_prompt('>>> ')
402 self._show_prompt('>>> ')
403
403
404 def _show_interpreter_prompt_for_reply(self, msg):
404 def _show_interpreter_prompt_for_reply(self, msg):
405 """ Shows a prompt for the interpreter given an 'execute_reply' message.
405 """ Shows a prompt for the interpreter given an 'execute_reply' message.
406 """
406 """
407 self._show_interpreter_prompt()
407 self._show_interpreter_prompt()
408
408
409 #------ Signal handlers ----------------------------------------------------
409 #------ Signal handlers ----------------------------------------------------
410
410
411 def _document_contents_change(self, position, removed, added):
411 def _document_contents_change(self, position, removed, added):
412 """ Called whenever the document's content changes. Display a call tip
412 """ Called whenever the document's content changes. Display a call tip
413 if appropriate.
413 if appropriate.
414 """
414 """
415 # Calculate where the cursor should be *after* the change:
415 # Calculate where the cursor should be *after* the change:
416 position += added
416 position += added
417
417
418 document = self._control.document()
418 document = self._control.document()
419 if position == self._get_cursor().position():
419 if position == self._get_cursor().position():
420 self._call_tip()
420 self._call_tip()
General Comments 0
You need to be logged in to leave comments. Login now