##// END OF EJS Templates
* CallTipWidget and CompletionWidget no longer need to be fed key presses. This means that can be attached to any Q[Plain]TextEdit with zero hassle....
epatters -
Show More
@@ -1,182 +1,165 b''
1 1 # Standard library imports
2 2 import re
3 3 from textwrap import dedent
4 4
5 5 # System library imports
6 6 from PyQt4 import QtCore, QtGui
7 7
8 8
9 9 class CallTipWidget(QtGui.QLabel):
10 10 """ Shows call tips by parsing the current text of Q[Plain]TextEdit.
11 11 """
12 12
13 13 #--------------------------------------------------------------------------
14 14 # 'QObject' interface
15 15 #--------------------------------------------------------------------------
16 16
17 17 def __init__(self, parent):
18 18 """ Create a call tip manager that is attached to the specified Qt
19 19 text edit widget.
20 20 """
21 21 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
22 22 QtGui.QLabel.__init__(self, parent, QtCore.Qt.ToolTip)
23 23
24 24 self.setFont(parent.document().defaultFont())
25 25 self.setForegroundRole(QtGui.QPalette.ToolTipText)
26 26 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
27 27 self.setPalette(QtGui.QToolTip.palette())
28 28
29 29 self.setAlignment(QtCore.Qt.AlignLeft)
30 30 self.setIndent(1)
31 31 self.setFrameStyle(QtGui.QFrame.NoFrame)
32 32 self.setMargin(1 + self.style().pixelMetric(
33 33 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
34 34 self.setWindowOpacity(self.style().styleHint(
35 35 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
36 36
37 def eventFilter(self, obj, event):
38 """ Reimplemented to hide on certain key presses and on parent focus
39 changes.
40 """
41 if obj == self.parent():
42 etype = event.type()
43 if (etype == QtCore.QEvent.KeyPress and
44 event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
45 QtCore.Qt.Key_Escape)):
46 self.hide()
47 elif etype == QtCore.QEvent.FocusOut:
48 self.hide()
49
50 return QtGui.QLabel.eventFilter(self, obj, event)
51
37 52 #--------------------------------------------------------------------------
38 53 # 'QWidget' interface
39 54 #--------------------------------------------------------------------------
40 55
41 56 def hideEvent(self, event):
42 """ Reimplemented to disconnect the cursor movement handler.
57 """ Reimplemented to disconnect signal handlers and event filter.
43 58 """
44 59 QtGui.QLabel.hideEvent(self, event)
45 self.parent().cursorPositionChanged.disconnect(self._update_tip)
46
47 def keyPressEvent(self, event):
48 """ Reimplemented to hide on certain key presses.
49 """
50 if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
51 QtCore.Qt.Key_Escape):
52 self.hide()
60 parent = self.parent()
61 parent.cursorPositionChanged.disconnect(self._cursor_position_changed)
62 parent.removeEventFilter(self)
53 63
54 64 def paintEvent(self, event):
55 65 """ Reimplemented to paint the background panel.
56 66 """
57 67 painter = QtGui.QStylePainter(self)
58 68 option = QtGui.QStyleOptionFrame()
59 69 option.init(self)
60 70 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
61 71 painter.end()
62 72
63 73 QtGui.QLabel.paintEvent(self, event)
64 74
65 75 def showEvent(self, event):
66 """ Reimplemented to connect the cursor movement handler.
76 """ Reimplemented to connect signal handlers and event filter.
67 77 """
68 78 QtGui.QLabel.showEvent(self, event)
69 self.parent().cursorPositionChanged.connect(self._update_tip)
79 parent = self.parent()
80 parent.cursorPositionChanged.connect(self._cursor_position_changed)
81 parent.installEventFilter(self)
70 82
71 83 #--------------------------------------------------------------------------
72 84 # 'CallTipWidget' interface
73 85 #--------------------------------------------------------------------------
74 86
75 87 def show_docstring(self, doc, maxlines=20):
76 88 """ Attempts to show the specified docstring at the current cursor
77 89 location. The docstring is dedented and possibly truncated for
78 90 length.
79 91 """
80 92 doc = dedent(doc.rstrip()).lstrip()
81 93 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
82 94 if match:
83 95 doc = doc[:match.end()] + '\n[Documentation continues...]'
84 96 return self.show_tip(doc)
85 97
86 98 def show_tip(self, tip):
87 99 """ Attempts to show the specified tip at the current cursor location.
88 100 """
89 101 text_edit = self.parent()
90 102 document = text_edit.document()
91 103 cursor = text_edit.textCursor()
92 104 search_pos = cursor.position() - 1
93 105 self._start_position, _ = self._find_parenthesis(search_pos,
94 106 forward=False)
95 107 if self._start_position == -1:
96 108 return False
97 109
98 110 point = text_edit.cursorRect(cursor).bottomRight()
99 111 point = text_edit.mapToGlobal(point)
100 112 self.move(point)
101 113 self.setText(tip)
102 114 if self.isVisible():
103 115 self.resize(self.sizeHint())
104 116 else:
105 117 self.show()
106 118 return True
107 119
108 120 #--------------------------------------------------------------------------
109 121 # Protected interface
110 122 #--------------------------------------------------------------------------
111 123
112 124 def _find_parenthesis(self, position, forward=True):
113 125 """ If 'forward' is True (resp. False), proceed forwards
114 126 (resp. backwards) through the line that contains 'position' until an
115 127 unmatched closing (resp. opening) parenthesis is found. Returns a
116 128 tuple containing the position of this parenthesis (or -1 if it is
117 129 not found) and the number commas (at depth 0) found along the way.
118 130 """
119 131 commas = depth = 0
120 132 document = self.parent().document()
121 133 qchar = document.characterAt(position)
122 134 while (position > 0 and qchar.isPrint() and
123 135 # Need to check explicitly for line/paragraph separators:
124 136 qchar.unicode() not in (0x2028, 0x2029)):
125 137 char = qchar.toAscii()
126 138 if char == ',' and depth == 0:
127 139 commas += 1
128 140 elif char == ')':
129 141 if forward and depth == 0:
130 142 break
131 143 depth += 1
132 144 elif char == '(':
133 145 if not forward and depth == 0:
134 146 break
135 147 depth -= 1
136 148 position += 1 if forward else -1
137 149 qchar = document.characterAt(position)
138 150 else:
139 151 position = -1
140 152 return position, commas
141 153
142 def _highlight_tip(self, tip, current_argument):
143 """ Highlight the current argument (arguments start at 0), ending at the
144 next comma or unmatched closing parenthesis.
145
146 FIXME: This is an unreliable way to do things and it isn't being
147 used right now. Instead, we should use inspect.getargspec
148 metadata for this purpose.
149 """
150 start = tip.find('(')
151 if start != -1:
152 for i in xrange(current_argument):
153 start = tip.find(',', start)
154 if start != -1:
155 end = start + 1
156 while end < len(tip):
157 char = tip[end]
158 depth = 0
159 if (char == ',' and depth == 0):
160 break
161 elif char == '(':
162 depth += 1
163 elif char == ')':
164 if depth == 0:
165 break
166 depth -= 1
167 end += 1
168 tip = tip[:start+1] + '<font color="blue">' + \
169 tip[start+1:end] + '</font>' + tip[end:]
170 tip = tip.replace('\n', '<br/>')
171 return tip
172
173 def _update_tip(self):
154 #------ Signal handlers ----------------------------------------------------
155
156 def _cursor_position_changed(self):
174 157 """ Updates the tip based on user cursor movement.
175 158 """
176 159 cursor = self.parent().textCursor()
177 160 if cursor.position() <= self._start_position:
178 161 self.hide()
179 162 else:
180 163 position, commas = self._find_parenthesis(self._start_position + 1)
181 164 if position != -1:
182 165 self.hide()
@@ -1,124 +1,136 b''
1 1 # System library imports
2 2 from PyQt4 import QtCore, QtGui
3 3
4 4
5 5 class CompletionWidget(QtGui.QListWidget):
6 6 """ A widget for GUI tab completion.
7 7 """
8 8
9 9 #--------------------------------------------------------------------------
10 # 'QWidget' interface
10 # 'QObject' interface
11 11 #--------------------------------------------------------------------------
12 12
13 13 def __init__(self, parent):
14 14 """ Create a completion widget that is attached to the specified Qt
15 15 text edit widget.
16 16 """
17 17 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 18 QtGui.QListWidget.__init__(self, parent)
19 19
20 20 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
21 21 self.setAttribute(QtCore.Qt.WA_StaticContents)
22 22
23 23 # Ensure that parent keeps focus when widget is displayed.
24 24 self.setFocusProxy(parent)
25 25
26 26 self.setFrameShadow(QtGui.QFrame.Plain)
27 27 self.setFrameShape(QtGui.QFrame.StyledPanel)
28 28
29 29 self.itemActivated.connect(self._complete_current)
30 30
31 def eventFilter(self, obj, event):
32 """ Reimplemented to handle keyboard input and to auto-hide when our
33 parent loses focus.
34 """
35 if obj == self.parent():
36 etype = event.type()
37
38 if etype == QtCore.QEvent.KeyPress:
39 key, text = event.key(), event.text()
40 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
41 QtCore.Qt.Key_Tab):
42 self._complete_current()
43 return True
44 elif key == QtCore.Qt.Key_Escape:
45 self.hide()
46 return True
47 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
48 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
49 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
50 QtGui.QListWidget.keyPressEvent(self, event)
51 return True
52
53 elif etype == QtCore.QEvent.FocusOut:
54 self.hide()
55
56 return QtGui.QListWidget.eventFilter(self, obj, event)
57
58 #--------------------------------------------------------------------------
59 # 'QWidget' interface
60 #--------------------------------------------------------------------------
61
31 62 def hideEvent(self, event):
32 """ Reimplemented to disconnect the cursor movement handler.
63 """ Reimplemented to disconnect signal handlers and event filter.
33 64 """
34 65 QtGui.QListWidget.hideEvent(self, event)
66 parent = self.parent()
35 67 try:
36 self.parent().cursorPositionChanged.disconnect(self._update_current)
68 parent.cursorPositionChanged.disconnect(self._update_current)
37 69 except TypeError:
38 70 pass
39
40 def keyPressEvent(self, event):
41 """ Reimplemented to update the list.
42 """
43 key, text = event.key(), event.text()
44
45 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
46 QtCore.Qt.Key_Tab):
47 self._complete_current()
48 event.accept()
49
50 elif key == QtCore.Qt.Key_Escape:
51 self.hide()
52 event.accept()
53
54 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
55 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
56 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
57 QtGui.QListWidget.keyPressEvent(self, event)
58 event.accept()
59
60 else:
61 event.ignore()
71 parent.removeEventFilter(self)
62 72
63 73 def showEvent(self, event):
64 """ Reimplemented to connect the cursor movement handler.
74 """ Reimplemented to connect signal handlers and event filter.
65 75 """
66 76 QtGui.QListWidget.showEvent(self, event)
67 self.parent().cursorPositionChanged.connect(self._update_current)
77 parent = self.parent()
78 parent.cursorPositionChanged.connect(self._update_current)
79 parent.installEventFilter(self)
68 80
69 81 #--------------------------------------------------------------------------
70 82 # 'CompletionWidget' interface
71 83 #--------------------------------------------------------------------------
72 84
73 85 def show_items(self, cursor, items):
74 86 """ Shows the completion widget with 'items' at the position specified
75 87 by 'cursor'.
76 88 """
77 89 text_edit = self.parent()
78 90 point = text_edit.cursorRect(cursor).bottomRight()
79 91 point = text_edit.mapToGlobal(point)
80 92 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
81 93 if screen_rect.size().height() - point.y() - self.height() < 0:
82 94 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
83 95 point.setY(point.y() - self.height())
84 96 self.move(point)
85 97
86 98 self._start_position = cursor.position()
87 99 self.clear()
88 100 self.addItems(items)
89 101 self.setCurrentRow(0)
90 102 self.show()
91 103
92 104 #--------------------------------------------------------------------------
93 105 # Protected interface
94 106 #--------------------------------------------------------------------------
95 107
96 108 def _complete_current(self):
97 109 """ Perform the completion with the currently selected item.
98 110 """
99 111 self._current_text_cursor().insertText(self.currentItem().text())
100 112 self.hide()
101 113
102 114 def _current_text_cursor(self):
103 115 """ Returns a cursor with text between the start position and the
104 116 current position selected.
105 117 """
106 118 cursor = self.parent().textCursor()
107 119 if cursor.position() >= self._start_position:
108 120 cursor.setPosition(self._start_position,
109 121 QtGui.QTextCursor.KeepAnchor)
110 122 return cursor
111 123
112 124 def _update_current(self):
113 125 """ Updates the current item based on the current text.
114 126 """
115 127 prefix = self._current_text_cursor().selection().toPlainText()
116 128 if prefix:
117 129 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
118 130 QtCore.Qt.MatchCaseSensitive))
119 131 if items:
120 132 self.setCurrentItem(items[0])
121 133 else:
122 134 self.hide()
123 135 else:
124 136 self.hide()
@@ -1,1047 +1,1049 b''
1 1 # Standard library imports
2 2 import sys
3 3
4 4 # System library imports
5 5 from PyQt4 import QtCore, QtGui
6 6
7 7 # Local imports
8 8 from ansi_code_processor import QtAnsiCodeProcessor
9 9 from completion_widget import CompletionWidget
10 10
11 11
12 12 class ConsoleWidget(QtGui.QWidget):
13 13 """ Base class for console-type widgets. This class is mainly concerned with
14 14 dealing with the prompt, keeping the cursor inside the editing line, and
15 15 handling ANSI escape sequences.
16 16 """
17 17
18 18 # Whether to process ANSI escape codes.
19 19 ansi_codes = True
20 20
21 21 # The maximum number of lines of text before truncation.
22 22 buffer_size = 500
23 23
24 24 # Whether to use a CompletionWidget or plain text output for tab completion.
25 25 gui_completion = True
26 26
27 27 # Whether to override ShortcutEvents for the keybindings defined by this
28 28 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
29 29 # priority (when it has focus) over, e.g., window-level menu shortcuts.
30 30 override_shortcuts = False
31 31
32 32 # Signals that indicate ConsoleWidget state.
33 33 copy_available = QtCore.pyqtSignal(bool)
34 34 redo_available = QtCore.pyqtSignal(bool)
35 35 undo_available = QtCore.pyqtSignal(bool)
36 36
37 37 # Protected class variables.
38 38 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
39 39 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
40 40 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
41 41 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
42 42 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
43 43 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
44 44 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
45 45 _shortcuts = set(_ctrl_down_remap.keys() +
46 46 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
47 47
48 48 #---------------------------------------------------------------------------
49 49 # 'QObject' interface
50 50 #---------------------------------------------------------------------------
51 51
52 52 def __init__(self, kind='plain', parent=None):
53 53 """ Create a ConsoleWidget.
54 54
55 55 Parameters
56 56 ----------
57 57 kind : str, optional [default 'plain']
58 58 The type of text widget to use. Valid values are 'plain', which
59 specifies a QPlainTextEdit, and 'rich', which specifies an
60 QTextEdit.
59 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
61 60
62 61 parent : QWidget, optional [default None]
63 62 The parent for this widget.
64 63 """
65 64 super(ConsoleWidget, self).__init__(parent)
66 65
67 66 # Create the underlying text widget.
68 67 self._control = self._create_control(kind)
69 68
70 69 # Initialize protected variables. Some variables contain useful state
71 70 # information for subclasses; they should be considered read-only.
72 71 self._ansi_processor = QtAnsiCodeProcessor()
73 72 self._completion_widget = CompletionWidget(self._control)
74 73 self._continuation_prompt = '> '
75 74 self._continuation_prompt_html = None
76 75 self._executing = False
77 76 self._prompt = ''
78 77 self._prompt_html = None
79 78 self._prompt_pos = 0
80 79 self._reading = False
81 80 self._reading_callback = None
82 81 self._tab_width = 8
83 82
84 83 # Set a monospaced font.
85 84 self.reset_font()
86 85
87 86 def eventFilter(self, obj, event):
88 87 """ Reimplemented to ensure a console-like behavior in the underlying
89 88 text widget.
90 89 """
91 90 if obj == self._control:
92 91 etype = event.type()
93 92
94 93 # Disable moving text by drag and drop.
95 94 if etype == QtCore.QEvent.DragMove:
96 95 return True
97 96
98 97 elif etype == QtCore.QEvent.KeyPress:
99 98 return self._event_filter_keypress(event)
100 99
101 100 # On Mac OS, it is always unnecessary to override shortcuts, hence
102 101 # the check below. Users should just use the Control key instead of
103 102 # the Command key.
104 103 elif etype == QtCore.QEvent.ShortcutOverride:
105 104 if sys.platform != 'darwin' and \
106 105 self._control_key_down(event.modifiers()) and \
107 106 event.key() in self._shortcuts:
108 107 event.accept()
109 108 return False
110 109
111 110 return super(ConsoleWidget, self).eventFilter(obj, event)
112 111
113 112 #---------------------------------------------------------------------------
114 113 # 'ConsoleWidget' public interface
115 114 #---------------------------------------------------------------------------
116 115
117 116 def clear(self, keep_input=False):
118 117 """ Clear the console, then write a new prompt. If 'keep_input' is set,
119 118 restores the old input buffer when the new prompt is written.
120 119 """
121 120 self._control.clear()
122 121 if keep_input:
123 122 input_buffer = self.input_buffer
124 123 self._show_prompt()
125 124 if keep_input:
126 125 self.input_buffer = input_buffer
127 126
128 127 def copy(self):
129 128 """ Copy the current selected text to the clipboard.
130 129 """
131 130 self._control.copy()
132 131
133 132 def execute(self, source=None, hidden=False, interactive=False):
134 133 """ Executes source or the input buffer, possibly prompting for more
135 134 input.
136 135
137 136 Parameters:
138 137 -----------
139 138 source : str, optional
140 139
141 140 The source to execute. If not specified, the input buffer will be
142 141 used. If specified and 'hidden' is False, the input buffer will be
143 142 replaced with the source before execution.
144 143
145 144 hidden : bool, optional (default False)
146 145
147 146 If set, no output will be shown and the prompt will not be modified.
148 147 In other words, it will be completely invisible to the user that
149 148 an execution has occurred.
150 149
151 150 interactive : bool, optional (default False)
152 151
153 152 Whether the console is to treat the source as having been manually
154 153 entered by the user. The effect of this parameter depends on the
155 154 subclass implementation.
156 155
157 156 Raises:
158 157 -------
159 158 RuntimeError
160 159 If incomplete input is given and 'hidden' is True. In this case,
161 160 it not possible to prompt for more input.
162 161
163 162 Returns:
164 163 --------
165 164 A boolean indicating whether the source was executed.
166 165 """
167 166 if not hidden:
168 167 if source is not None:
169 168 self.input_buffer = source
170 169
171 170 self._append_plain_text('\n')
172 171 self._executing_input_buffer = self.input_buffer
173 172 self._executing = True
174 173 self._prompt_finished()
175 174
176 175 real_source = self.input_buffer if source is None else source
177 176 complete = self._is_complete(real_source, interactive)
178 177 if complete:
179 178 if not hidden:
180 179 # The maximum block count is only in effect during execution.
181 180 # This ensures that _prompt_pos does not become invalid due to
182 181 # text truncation.
183 182 self._control.document().setMaximumBlockCount(self.buffer_size)
184 183 self._execute(real_source, hidden)
185 184 elif hidden:
186 185 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
187 186 else:
188 187 self._show_continuation_prompt()
189 188
190 189 return complete
191 190
192 191 def _get_input_buffer(self):
193 192 """ The text that the user has entered entered at the current prompt.
194 193 """
195 194 # If we're executing, the input buffer may not even exist anymore due to
196 195 # the limit imposed by 'buffer_size'. Therefore, we store it.
197 196 if self._executing:
198 197 return self._executing_input_buffer
199 198
200 199 cursor = self._get_end_cursor()
201 200 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
202 201 input_buffer = str(cursor.selection().toPlainText())
203 202
204 203 # Strip out continuation prompts.
205 204 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
206 205
207 206 def _set_input_buffer(self, string):
208 207 """ Replaces the text in the input buffer with 'string'.
209 208 """
210 209 # Remove old text.
211 210 cursor = self._get_end_cursor()
212 211 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
213 212 cursor.removeSelectedText()
214 213
215 214 # Insert new text with continuation prompts.
216 215 lines = string.splitlines(True)
217 216 if lines:
218 217 self._append_plain_text(lines[0])
219 218 for i in xrange(1, len(lines)):
220 219 if self._continuation_prompt_html is None:
221 220 self._append_plain_text(self._continuation_prompt)
222 221 else:
223 222 self._append_html(self._continuation_prompt_html)
224 223 self._append_plain_text(lines[i])
225 224 self._control.moveCursor(QtGui.QTextCursor.End)
226 225
227 226 input_buffer = property(_get_input_buffer, _set_input_buffer)
228 227
229 228 def _get_input_buffer_cursor_line(self):
230 229 """ The text in the line of the input buffer in which the user's cursor
231 230 rests. Returns a string if there is such a line; otherwise, None.
232 231 """
233 232 if self._executing:
234 233 return None
235 234 cursor = self._control.textCursor()
236 235 if cursor.position() >= self._prompt_pos:
237 236 text = self._get_block_plain_text(cursor.block())
238 237 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
239 238 return text[len(self._prompt):]
240 239 else:
241 240 return text[len(self._continuation_prompt):]
242 241 else:
243 242 return None
244 243
245 244 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
246 245
247 246 def _get_font(self):
248 247 """ The base font being used by the ConsoleWidget.
249 248 """
250 249 return self._control.document().defaultFont()
251 250
252 251 def _set_font(self, font):
253 252 """ Sets the base font for the ConsoleWidget to the specified QFont.
254 253 """
255 254 font_metrics = QtGui.QFontMetrics(font)
256 255 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
257 256
258 257 self._completion_widget.setFont(font)
259 258 self._control.document().setDefaultFont(font)
260 259
261 260 font = property(_get_font, _set_font)
262 261
263 262 def paste(self):
264 263 """ Paste the contents of the clipboard into the input region.
265 264 """
266 265 self._keep_cursor_in_buffer()
267 266 self._control.paste()
268 267
269 268 def print_(self, printer):
270 269 """ Print the contents of the ConsoleWidget to the specified QPrinter.
271 270 """
272 271 self._control.print_(printer)
273 272
274 273 def redo(self):
275 274 """ Redo the last operation. If there is no operation to redo, nothing
276 275 happens.
277 276 """
278 277 self._control.redo()
279 278
280 279 def reset_font(self):
281 280 """ Sets the font to the default fixed-width font for this platform.
282 281 """
283 282 if sys.platform == 'win32':
284 283 name = 'Courier'
285 284 elif sys.platform == 'darwin':
286 285 name = 'Monaco'
287 286 else:
288 287 name = 'Monospace'
289 288 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
290 289 font.setStyleHint(QtGui.QFont.TypeWriter)
291 290 self._set_font(font)
292 291
293 292 def select_all(self):
294 293 """ Selects all the text in the buffer.
295 294 """
296 295 self._control.selectAll()
297 296
298 297 def _get_tab_width(self):
299 298 """ The width (in terms of space characters) for tab characters.
300 299 """
301 300 return self._tab_width
302 301
303 302 def _set_tab_width(self, tab_width):
304 303 """ Sets the width (in terms of space characters) for tab characters.
305 304 """
306 305 font_metrics = QtGui.QFontMetrics(self.font)
307 306 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
308 307
309 308 self._tab_width = tab_width
310 309
311 310 tab_width = property(_get_tab_width, _set_tab_width)
312 311
313 312 def undo(self):
314 313 """ Undo the last operation. If there is no operation to undo, nothing
315 314 happens.
316 315 """
317 316 self._control.undo()
318 317
319 318 #---------------------------------------------------------------------------
320 319 # 'ConsoleWidget' abstract interface
321 320 #---------------------------------------------------------------------------
322 321
323 322 def _is_complete(self, source, interactive):
324 323 """ Returns whether 'source' can be executed. When triggered by an
325 324 Enter/Return key press, 'interactive' is True; otherwise, it is
326 325 False.
327 326 """
328 327 raise NotImplementedError
329 328
330 329 def _execute(self, source, hidden):
331 330 """ Execute 'source'. If 'hidden', do not show any output.
332 331 """
333 332 raise NotImplementedError
334 333
334 def _execute_interrupt(self):
335 """ Attempts to stop execution. Returns whether this method has an
336 implementation.
337 """
338 return False
339
335 340 def _prompt_started_hook(self):
336 341 """ Called immediately after a new prompt is displayed.
337 342 """
338 343 pass
339 344
340 345 def _prompt_finished_hook(self):
341 346 """ Called immediately after a prompt is finished, i.e. when some input
342 347 will be processed and a new prompt displayed.
343 348 """
344 349 pass
345 350
346 351 def _up_pressed(self):
347 352 """ Called when the up key is pressed. Returns whether to continue
348 353 processing the event.
349 354 """
350 355 return True
351 356
352 357 def _down_pressed(self):
353 358 """ Called when the down key is pressed. Returns whether to continue
354 359 processing the event.
355 360 """
356 361 return True
357 362
358 363 def _tab_pressed(self):
359 364 """ Called when the tab key is pressed. Returns whether to continue
360 365 processing the event.
361 366 """
362 367 return False
363 368
364 369 #--------------------------------------------------------------------------
365 370 # 'ConsoleWidget' protected interface
366 371 #--------------------------------------------------------------------------
367 372
368 373 def _append_html(self, html):
369 374 """ Appends html at the end of the console buffer.
370 375 """
371 376 cursor = self._get_end_cursor()
372 377 self._insert_html(cursor, html)
373 378
374 379 def _append_html_fetching_plain_text(self, html):
375 380 """ Appends 'html', then returns the plain text version of it.
376 381 """
377 382 anchor = self._get_end_cursor().position()
378 383 self._append_html(html)
379 384 cursor = self._get_end_cursor()
380 385 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
381 386 return str(cursor.selection().toPlainText())
382 387
383 388 def _append_plain_text(self, text):
384 389 """ Appends plain text at the end of the console buffer, processing
385 390 ANSI codes if enabled.
386 391 """
387 392 cursor = self._get_end_cursor()
388 393 if self.ansi_codes:
389 394 for substring in self._ansi_processor.split_string(text):
390 395 format = self._ansi_processor.get_format()
391 396 cursor.insertText(substring, format)
392 397 else:
393 398 cursor.insertText(text)
394 399
395 400 def _append_plain_text_keeping_prompt(self, text):
396 401 """ Writes 'text' after the current prompt, then restores the old prompt
397 402 with its old input buffer.
398 403 """
399 404 input_buffer = self.input_buffer
400 405 self._append_plain_text('\n')
401 406 self._prompt_finished()
402 407
403 408 self._append_plain_text(text)
404 409 self._show_prompt()
405 410 self.input_buffer = input_buffer
406 411
407 412 def _complete_with_items(self, cursor, items):
408 413 """ Performs completion with 'items' at the specified cursor location.
409 414 """
410 415 if len(items) == 1:
411 416 cursor.setPosition(self._control.textCursor().position(),
412 417 QtGui.QTextCursor.KeepAnchor)
413 418 cursor.insertText(items[0])
414 419 elif len(items) > 1:
415 420 if self.gui_completion:
416 421 self._completion_widget.show_items(cursor, items)
417 422 else:
418 423 text = self._format_as_columns(items)
419 424 self._append_plain_text_keeping_prompt(text)
420 425
421 426 def _control_key_down(self, modifiers):
422 427 """ Given a KeyboardModifiers flags object, return whether the Control
423 428 key is down (on Mac OS, treat the Command key as a synonym for
424 429 Control).
425 430 """
426 431 down = bool(modifiers & QtCore.Qt.ControlModifier)
427 432
428 433 # Note: on Mac OS, ControlModifier corresponds to the Command key while
429 434 # MetaModifier corresponds to the Control key.
430 435 if sys.platform == 'darwin':
431 436 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
432 437
433 438 return down
434 439
435 440 def _create_control(self, kind):
436 441 """ Creates and sets the underlying text widget.
437 442 """
438 443 layout = QtGui.QVBoxLayout(self)
439 444 layout.setMargin(0)
440 445 if kind == 'plain':
441 446 control = QtGui.QPlainTextEdit()
442 447 elif kind == 'rich':
443 448 control = QtGui.QTextEdit()
444 449 else:
445 450 raise ValueError("Kind %s unknown." % repr(kind))
446 451 layout.addWidget(control)
447 452
448 453 control.installEventFilter(self)
449 454 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
450 455 control.customContextMenuRequested.connect(self._show_context_menu)
451 456 control.copyAvailable.connect(self.copy_available)
452 457 control.redoAvailable.connect(self.redo_available)
453 458 control.undoAvailable.connect(self.undo_available)
454 459
455 460 return control
456 461
457 462 def _event_filter_keypress(self, event):
458 463 """ Filter key events for the underlying text widget to create a
459 464 console-like interface.
460 465 """
461 466 key = event.key()
462 467 ctrl_down = self._control_key_down(event.modifiers())
463 468
464 469 # If the key is remapped, return immediately.
465 470 if ctrl_down and key in self._ctrl_down_remap:
466 471 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
467 472 self._ctrl_down_remap[key],
468 473 QtCore.Qt.NoModifier)
469 474 QtGui.qApp.sendEvent(self._control, new_event)
470 475 return True
471 476
472 # If the completion widget accepts the key press, return immediately.
473 if self._completion_widget.isVisible():
474 self._completion_widget.keyPressEvent(event)
475 if event.isAccepted():
476 return True
477
478 477 # Otherwise, proceed normally and do not return early.
479 478 intercepted = False
480 479 cursor = self._control.textCursor()
481 480 position = cursor.position()
482 481 alt_down = event.modifiers() & QtCore.Qt.AltModifier
483 482 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
484 483
485 484 if event.matches(QtGui.QKeySequence.Paste):
486 485 # Call our paste instead of the underlying text widget's.
487 486 self.paste()
488 487 intercepted = True
489 488
490 489 elif ctrl_down:
491 if key == QtCore.Qt.Key_K:
490 if key == QtCore.Qt.Key_C and self._executing:
491 intercepted = self._execute_interrupt()
492
493 elif key == QtCore.Qt.Key_K:
492 494 if self._in_buffer(position):
493 495 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
494 496 QtGui.QTextCursor.KeepAnchor)
495 497 cursor.removeSelectedText()
496 498 intercepted = True
497 499
498 500 elif key == QtCore.Qt.Key_X:
499 501 intercepted = True
500 502
501 503 elif key == QtCore.Qt.Key_Y:
502 504 self.paste()
503 505 intercepted = True
504 506
505 507 elif alt_down:
506 508 if key == QtCore.Qt.Key_B:
507 509 self._set_cursor(self._get_word_start_cursor(position))
508 510 intercepted = True
509 511
510 512 elif key == QtCore.Qt.Key_F:
511 513 self._set_cursor(self._get_word_end_cursor(position))
512 514 intercepted = True
513 515
514 516 elif key == QtCore.Qt.Key_Backspace:
515 517 cursor = self._get_word_start_cursor(position)
516 518 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
517 519 cursor.removeSelectedText()
518 520 intercepted = True
519 521
520 522 elif key == QtCore.Qt.Key_D:
521 523 cursor = self._get_word_end_cursor(position)
522 524 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
523 525 cursor.removeSelectedText()
524 526 intercepted = True
525 527
526 528 else:
527 529 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
528 530 if self._reading:
529 531 self._append_plain_text('\n')
530 532 self._reading = False
531 533 if self._reading_callback:
532 534 self._reading_callback()
533 535 elif not self._executing:
534 536 self.execute(interactive=True)
535 537 intercepted = True
536 538
537 539 elif key == QtCore.Qt.Key_Up:
538 540 if self._reading or not self._up_pressed():
539 541 intercepted = True
540 542 else:
541 543 prompt_line = self._get_prompt_cursor().blockNumber()
542 544 intercepted = cursor.blockNumber() <= prompt_line
543 545
544 546 elif key == QtCore.Qt.Key_Down:
545 547 if self._reading or not self._down_pressed():
546 548 intercepted = True
547 549 else:
548 550 end_line = self._get_end_cursor().blockNumber()
549 551 intercepted = cursor.blockNumber() == end_line
550 552
551 553 elif key == QtCore.Qt.Key_Tab:
552 554 if self._reading:
553 555 intercepted = False
554 556 else:
555 557 intercepted = not self._tab_pressed()
556 558
557 559 elif key == QtCore.Qt.Key_Left:
558 560 intercepted = not self._in_buffer(position - 1)
559 561
560 562 elif key == QtCore.Qt.Key_Home:
561 563 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
562 564 start_line = cursor.blockNumber()
563 565 if start_line == self._get_prompt_cursor().blockNumber():
564 566 start_pos = self._prompt_pos
565 567 else:
566 568 start_pos = cursor.position()
567 569 start_pos += len(self._continuation_prompt)
568 570 if shift_down and self._in_buffer(position):
569 571 self._set_selection(position, start_pos)
570 572 else:
571 573 self._set_position(start_pos)
572 574 intercepted = True
573 575
574 576 elif key == QtCore.Qt.Key_Backspace and not alt_down:
575 577
576 578 # Line deletion (remove continuation prompt)
577 579 len_prompt = len(self._continuation_prompt)
578 580 if not self._reading and \
579 581 cursor.columnNumber() == len_prompt and \
580 582 position != self._prompt_pos:
581 583 cursor.setPosition(position - len_prompt,
582 584 QtGui.QTextCursor.KeepAnchor)
583 585 cursor.removeSelectedText()
584 586
585 587 # Regular backwards deletion
586 588 else:
587 589 anchor = cursor.anchor()
588 590 if anchor == position:
589 591 intercepted = not self._in_buffer(position - 1)
590 592 else:
591 593 intercepted = not self._in_buffer(min(anchor, position))
592 594
593 595 elif key == QtCore.Qt.Key_Delete:
594 596 anchor = cursor.anchor()
595 597 intercepted = not self._in_buffer(min(anchor, position))
596 598
597 599 # Don't move the cursor if control is down to allow copy-paste using
598 600 # the keyboard in any part of the buffer.
599 601 if not ctrl_down:
600 602 self._keep_cursor_in_buffer()
601 603
602 604 return intercepted
603 605
604 606 def _format_as_columns(self, items, separator=' '):
605 607 """ Transform a list of strings into a single string with columns.
606 608
607 609 Parameters
608 610 ----------
609 611 items : sequence of strings
610 612 The strings to process.
611 613
612 614 separator : str, optional [default is two spaces]
613 615 The string that separates columns.
614 616
615 617 Returns
616 618 -------
617 619 The formatted string.
618 620 """
619 621 # Note: this code is adapted from columnize 0.3.2.
620 622 # See http://code.google.com/p/pycolumnize/
621 623
622 624 font_metrics = QtGui.QFontMetrics(self.font)
623 625 displaywidth = max(5, (self.width() / font_metrics.width(' ')) - 1)
624 626
625 627 # Some degenerate cases.
626 628 size = len(items)
627 629 if size == 0:
628 630 return '\n'
629 631 elif size == 1:
630 632 return '%s\n' % str(items[0])
631 633
632 634 # Try every row count from 1 upwards
633 635 array_index = lambda nrows, row, col: nrows*col + row
634 636 for nrows in range(1, size):
635 637 ncols = (size + nrows - 1) // nrows
636 638 colwidths = []
637 639 totwidth = -len(separator)
638 640 for col in range(ncols):
639 641 # Get max column width for this column
640 642 colwidth = 0
641 643 for row in range(nrows):
642 644 i = array_index(nrows, row, col)
643 645 if i >= size: break
644 646 x = items[i]
645 647 colwidth = max(colwidth, len(x))
646 648 colwidths.append(colwidth)
647 649 totwidth += colwidth + len(separator)
648 650 if totwidth > displaywidth:
649 651 break
650 652 if totwidth <= displaywidth:
651 653 break
652 654
653 655 # The smallest number of rows computed and the max widths for each
654 656 # column has been obtained. Now we just have to format each of the rows.
655 657 string = ''
656 658 for row in range(nrows):
657 659 texts = []
658 660 for col in range(ncols):
659 661 i = row + nrows*col
660 662 if i >= size:
661 663 texts.append('')
662 664 else:
663 665 texts.append(items[i])
664 666 while texts and not texts[-1]:
665 667 del texts[-1]
666 668 for col in range(len(texts)):
667 669 texts[col] = texts[col].ljust(colwidths[col])
668 670 string += '%s\n' % str(separator.join(texts))
669 671 return string
670 672
671 673 def _get_block_plain_text(self, block):
672 674 """ Given a QTextBlock, return its unformatted text.
673 675 """
674 676 cursor = QtGui.QTextCursor(block)
675 677 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
676 678 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
677 679 QtGui.QTextCursor.KeepAnchor)
678 680 return str(cursor.selection().toPlainText())
679 681
680 682 def _get_cursor(self):
681 683 """ Convenience method that returns a cursor for the current position.
682 684 """
683 685 return self._control.textCursor()
684 686
685 687 def _get_end_cursor(self):
686 688 """ Convenience method that returns a cursor for the last character.
687 689 """
688 690 cursor = self._control.textCursor()
689 691 cursor.movePosition(QtGui.QTextCursor.End)
690 692 return cursor
691 693
692 694 def _get_prompt_cursor(self):
693 695 """ Convenience method that returns a cursor for the prompt position.
694 696 """
695 697 cursor = self._control.textCursor()
696 698 cursor.setPosition(self._prompt_pos)
697 699 return cursor
698 700
699 701 def _get_selection_cursor(self, start, end):
700 702 """ Convenience method that returns a cursor with text selected between
701 703 the positions 'start' and 'end'.
702 704 """
703 705 cursor = self._control.textCursor()
704 706 cursor.setPosition(start)
705 707 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
706 708 return cursor
707 709
708 710 def _get_word_start_cursor(self, position):
709 711 """ Find the start of the word to the left the given position. If a
710 712 sequence of non-word characters precedes the first word, skip over
711 713 them. (This emulates the behavior of bash, emacs, etc.)
712 714 """
713 715 document = self._control.document()
714 716 position -= 1
715 717 while self._in_buffer(position) and \
716 718 not document.characterAt(position).isLetterOrNumber():
717 719 position -= 1
718 720 while self._in_buffer(position) and \
719 721 document.characterAt(position).isLetterOrNumber():
720 722 position -= 1
721 723 cursor = self._control.textCursor()
722 724 cursor.setPosition(position + 1)
723 725 return cursor
724 726
725 727 def _get_word_end_cursor(self, position):
726 728 """ Find the end of the word to the right the given position. If a
727 729 sequence of non-word characters precedes the first word, skip over
728 730 them. (This emulates the behavior of bash, emacs, etc.)
729 731 """
730 732 document = self._control.document()
731 733 end = self._get_end_cursor().position()
732 734 while position < end and \
733 735 not document.characterAt(position).isLetterOrNumber():
734 736 position += 1
735 737 while position < end and \
736 738 document.characterAt(position).isLetterOrNumber():
737 739 position += 1
738 740 cursor = self._control.textCursor()
739 741 cursor.setPosition(position)
740 742 return cursor
741 743
742 744 def _insert_html(self, cursor, html):
743 745 """ Insert HTML using the specified cursor in such a way that future
744 746 formatting is unaffected.
745 747 """
746 748 cursor.insertHtml(html)
747 749
748 750 # After inserting HTML, the text document "remembers" the current
749 751 # formatting, which means that subsequent calls adding plain text
750 752 # will result in similar formatting, a behavior that we do not want. To
751 753 # prevent this, we make sure that the last character has no formatting.
752 754 cursor.movePosition(QtGui.QTextCursor.Left,
753 755 QtGui.QTextCursor.KeepAnchor)
754 756 if cursor.selection().toPlainText().trimmed().isEmpty():
755 757 # If the last character is whitespace, it doesn't matter how it's
756 758 # formatted, so just clear the formatting.
757 759 cursor.setCharFormat(QtGui.QTextCharFormat())
758 760 else:
759 761 # Otherwise, add an unformatted space.
760 762 cursor.movePosition(QtGui.QTextCursor.Right)
761 763 cursor.insertText(' ', QtGui.QTextCharFormat())
762 764
763 765 def _in_buffer(self, position):
764 766 """ Returns whether the given position is inside the editing region.
765 767 """
766 768 return position >= self._prompt_pos
767 769
768 770 def _keep_cursor_in_buffer(self):
769 771 """ Ensures that the cursor is inside the editing region. Returns
770 772 whether the cursor was moved.
771 773 """
772 774 cursor = self._control.textCursor()
773 775 if cursor.position() < self._prompt_pos:
774 776 cursor.movePosition(QtGui.QTextCursor.End)
775 777 self._control.setTextCursor(cursor)
776 778 return True
777 779 else:
778 780 return False
779 781
780 782 def _prompt_started(self):
781 783 """ Called immediately after a new prompt is displayed.
782 784 """
783 785 # Temporarily disable the maximum block count to permit undo/redo and
784 786 # to ensure that the prompt position does not change due to truncation.
785 787 self._control.document().setMaximumBlockCount(0)
786 788 self._control.setUndoRedoEnabled(True)
787 789
788 790 self._control.setReadOnly(False)
789 791 self._control.moveCursor(QtGui.QTextCursor.End)
790 792
791 793 self._executing = False
792 794 self._prompt_started_hook()
793 795
794 796 def _prompt_finished(self):
795 797 """ Called immediately after a prompt is finished, i.e. when some input
796 798 will be processed and a new prompt displayed.
797 799 """
798 800 self._control.setUndoRedoEnabled(False)
799 801 self._control.setReadOnly(True)
800 802 self._prompt_finished_hook()
801 803
802 804 def _readline(self, prompt='', callback=None):
803 805 """ Reads one line of input from the user.
804 806
805 807 Parameters
806 808 ----------
807 809 prompt : str, optional
808 810 The prompt to print before reading the line.
809 811
810 812 callback : callable, optional
811 813 A callback to execute with the read line. If not specified, input is
812 814 read *synchronously* and this method does not return until it has
813 815 been read.
814 816
815 817 Returns
816 818 -------
817 819 If a callback is specified, returns nothing. Otherwise, returns the
818 820 input string with the trailing newline stripped.
819 821 """
820 822 if self._reading:
821 823 raise RuntimeError('Cannot read a line. Widget is already reading.')
822 824
823 825 if not callback and not self.isVisible():
824 826 # If the user cannot see the widget, this function cannot return.
825 827 raise RuntimeError('Cannot synchronously read a line if the widget'
826 828 'is not visible!')
827 829
828 830 self._reading = True
829 831 self._show_prompt(prompt, newline=False)
830 832
831 833 if callback is None:
832 834 self._reading_callback = None
833 835 while self._reading:
834 836 QtCore.QCoreApplication.processEvents()
835 837 return self.input_buffer.rstrip('\n')
836 838
837 839 else:
838 840 self._reading_callback = lambda: \
839 841 callback(self.input_buffer.rstrip('\n'))
840 842
841 843 def _reset(self):
842 844 """ Clears the console and resets internal state variables.
843 845 """
844 846 self._control.clear()
845 847 self._executing = self._reading = False
846 848
847 849 def _set_continuation_prompt(self, prompt, html=False):
848 850 """ Sets the continuation prompt.
849 851
850 852 Parameters
851 853 ----------
852 854 prompt : str
853 855 The prompt to show when more input is needed.
854 856
855 857 html : bool, optional (default False)
856 858 If set, the prompt will be inserted as formatted HTML. Otherwise,
857 859 the prompt will be treated as plain text, though ANSI color codes
858 860 will be handled.
859 861 """
860 862 if html:
861 863 self._continuation_prompt_html = prompt
862 864 else:
863 865 self._continuation_prompt = prompt
864 866 self._continuation_prompt_html = None
865 867
866 868 def _set_cursor(self, cursor):
867 869 """ Convenience method to set the current cursor.
868 870 """
869 871 self._control.setTextCursor(cursor)
870 872
871 873 def _set_position(self, position):
872 874 """ Convenience method to set the position of the cursor.
873 875 """
874 876 cursor = self._control.textCursor()
875 877 cursor.setPosition(position)
876 878 self._control.setTextCursor(cursor)
877 879
878 880 def _set_selection(self, start, end):
879 881 """ Convenience method to set the current selected text.
880 882 """
881 883 self._control.setTextCursor(self._get_selection_cursor(start, end))
882 884
883 885 def _show_context_menu(self, pos):
884 886 """ Shows a context menu at the given QPoint (in widget coordinates).
885 887 """
886 888 menu = QtGui.QMenu()
887 889
888 890 copy_action = QtGui.QAction('Copy', menu)
889 891 copy_action.triggered.connect(self.copy)
890 892 copy_action.setEnabled(self._get_cursor().hasSelection())
891 893 copy_action.setShortcut(QtGui.QKeySequence.Copy)
892 894 menu.addAction(copy_action)
893 895
894 896 paste_action = QtGui.QAction('Paste', menu)
895 897 paste_action.triggered.connect(self.paste)
896 898 paste_action.setEnabled(self._control.canPaste())
897 899 paste_action.setShortcut(QtGui.QKeySequence.Paste)
898 900 menu.addAction(paste_action)
899 901 menu.addSeparator()
900 902
901 903 select_all_action = QtGui.QAction('Select All', menu)
902 904 select_all_action.triggered.connect(self.select_all)
903 905 menu.addAction(select_all_action)
904 906
905 907 menu.exec_(self._control.mapToGlobal(pos))
906 908
907 909 def _show_prompt(self, prompt=None, html=False, newline=True):
908 910 """ Writes a new prompt at the end of the buffer.
909 911
910 912 Parameters
911 913 ----------
912 914 prompt : str, optional
913 915 The prompt to show. If not specified, the previous prompt is used.
914 916
915 917 html : bool, optional (default False)
916 918 Only relevant when a prompt is specified. If set, the prompt will
917 919 be inserted as formatted HTML. Otherwise, the prompt will be treated
918 920 as plain text, though ANSI color codes will be handled.
919 921
920 922 newline : bool, optional (default True)
921 923 If set, a new line will be written before showing the prompt if
922 924 there is not already a newline at the end of the buffer.
923 925 """
924 926 # Insert a preliminary newline, if necessary.
925 927 if newline:
926 928 cursor = self._get_end_cursor()
927 929 if cursor.position() > 0:
928 930 cursor.movePosition(QtGui.QTextCursor.Left,
929 931 QtGui.QTextCursor.KeepAnchor)
930 932 if str(cursor.selection().toPlainText()) != '\n':
931 933 self._append_plain_text('\n')
932 934
933 935 # Write the prompt.
934 936 if prompt is None:
935 937 if self._prompt_html is None:
936 938 self._append_plain_text(self._prompt)
937 939 else:
938 940 self._append_html(self._prompt_html)
939 941 else:
940 942 if html:
941 943 self._prompt = self._append_html_fetching_plain_text(prompt)
942 944 self._prompt_html = prompt
943 945 else:
944 946 self._append_plain_text(prompt)
945 947 self._prompt = prompt
946 948 self._prompt_html = None
947 949
948 950 self._prompt_pos = self._get_end_cursor().position()
949 951 self._prompt_started()
950 952
951 953 def _show_continuation_prompt(self):
952 954 """ Writes a new continuation prompt at the end of the buffer.
953 955 """
954 956 if self._continuation_prompt_html is None:
955 957 self._append_plain_text(self._continuation_prompt)
956 958 else:
957 959 self._continuation_prompt = self._append_html_fetching_plain_text(
958 960 self._continuation_prompt_html)
959 961
960 962 self._prompt_started()
961 963
962 964
963 965 class HistoryConsoleWidget(ConsoleWidget):
964 966 """ A ConsoleWidget that keeps a history of the commands that have been
965 967 executed.
966 968 """
967 969
968 970 #---------------------------------------------------------------------------
969 971 # 'object' interface
970 972 #---------------------------------------------------------------------------
971 973
972 974 def __init__(self, *args, **kw):
973 975 super(HistoryConsoleWidget, self).__init__(*args, **kw)
974 976 self._history = []
975 977 self._history_index = 0
976 978
977 979 #---------------------------------------------------------------------------
978 980 # 'ConsoleWidget' public interface
979 981 #---------------------------------------------------------------------------
980 982
981 983 def execute(self, source=None, hidden=False, interactive=False):
982 984 """ Reimplemented to the store history.
983 985 """
984 986 if not hidden:
985 987 history = self.input_buffer if source is None else source
986 988
987 989 executed = super(HistoryConsoleWidget, self).execute(
988 990 source, hidden, interactive)
989 991
990 992 if executed and not hidden:
991 993 self._history.append(history.rstrip())
992 994 self._history_index = len(self._history)
993 995
994 996 return executed
995 997
996 998 #---------------------------------------------------------------------------
997 999 # 'ConsoleWidget' abstract interface
998 1000 #---------------------------------------------------------------------------
999 1001
1000 1002 def _up_pressed(self):
1001 1003 """ Called when the up key is pressed. Returns whether to continue
1002 1004 processing the event.
1003 1005 """
1004 1006 prompt_cursor = self._get_prompt_cursor()
1005 1007 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1006 1008 self.history_previous()
1007 1009
1008 1010 # Go to the first line of prompt for seemless history scrolling.
1009 1011 cursor = self._get_prompt_cursor()
1010 1012 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1011 1013 self._set_cursor(cursor)
1012 1014
1013 1015 return False
1014 1016 return True
1015 1017
1016 1018 def _down_pressed(self):
1017 1019 """ Called when the down key is pressed. Returns whether to continue
1018 1020 processing the event.
1019 1021 """
1020 1022 end_cursor = self._get_end_cursor()
1021 1023 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1022 1024 self.history_next()
1023 1025 return False
1024 1026 return True
1025 1027
1026 1028 #---------------------------------------------------------------------------
1027 1029 # 'HistoryConsoleWidget' interface
1028 1030 #---------------------------------------------------------------------------
1029 1031
1030 1032 def history_previous(self):
1031 1033 """ If possible, set the input buffer to the previous item in the
1032 1034 history.
1033 1035 """
1034 1036 if self._history_index > 0:
1035 1037 self._history_index -= 1
1036 1038 self.input_buffer = self._history[self._history_index]
1037 1039
1038 1040 def history_next(self):
1039 1041 """ Set the input buffer to the next item in the history, or a blank
1040 1042 line if there is no subsequent item.
1041 1043 """
1042 1044 if self._history_index < len(self._history):
1043 1045 self._history_index += 1
1044 1046 if self._history_index < len(self._history):
1045 1047 self.input_buffer = self._history[self._history_index]
1046 1048 else:
1047 1049 self.input_buffer = ''
@@ -1,384 +1,369 b''
1 1 # Standard library imports
2 2 import signal
3 3 import sys
4 4
5 5 # System library imports
6 6 from pygments.lexers import PythonLexer
7 7 from PyQt4 import QtCore, QtGui
8 8 import zmq
9 9
10 10 # Local imports
11 11 from IPython.core.inputsplitter import InputSplitter
12 12 from call_tip_widget import CallTipWidget
13 13 from completion_lexer import CompletionLexer
14 14 from console_widget import HistoryConsoleWidget
15 15 from pygments_highlighter import PygmentsHighlighter
16 16
17 17
18 18 class FrontendHighlighter(PygmentsHighlighter):
19 19 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 20 prompts.
21 21 """
22 22
23 23 def __init__(self, frontend):
24 24 super(FrontendHighlighter, self).__init__(frontend._control.document())
25 25 self._current_offset = 0
26 26 self._frontend = frontend
27 27 self.highlighting_on = False
28 28
29 29 def highlightBlock(self, qstring):
30 30 """ Highlight a block of text. Reimplemented to highlight selectively.
31 31 """
32 32 if not self.highlighting_on:
33 33 return
34 34
35 35 # The input to this function is unicode string that may contain
36 36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 37 # the string as plain text so we can compare it.
38 38 current_block = self.currentBlock()
39 39 string = self._frontend._get_block_plain_text(current_block)
40 40
41 41 # Decide whether to check for the regular or continuation prompt.
42 42 if current_block.contains(self._frontend._prompt_pos):
43 43 prompt = self._frontend._prompt
44 44 else:
45 45 prompt = self._frontend._continuation_prompt
46 46
47 47 # Don't highlight the part of the string that contains the prompt.
48 48 if string.startswith(prompt):
49 49 self._current_offset = len(prompt)
50 50 qstring.remove(0, len(prompt))
51 51 else:
52 52 self._current_offset = 0
53 53
54 54 PygmentsHighlighter.highlightBlock(self, qstring)
55 55
56 56 def setFormat(self, start, count, format):
57 57 """ Reimplemented to highlight selectively.
58 58 """
59 59 start += self._current_offset
60 60 PygmentsHighlighter.setFormat(self, start, count, format)
61 61
62 62
63 63 class FrontendWidget(HistoryConsoleWidget):
64 64 """ A Qt frontend for a generic Python kernel.
65 65 """
66 66
67 67 # Emitted when an 'execute_reply' is received from the kernel.
68 68 executed = QtCore.pyqtSignal(object)
69 69
70 70 #---------------------------------------------------------------------------
71 71 # 'object' interface
72 72 #---------------------------------------------------------------------------
73 73
74 74 def __init__(self, *args, **kw):
75 75 super(FrontendWidget, self).__init__(*args, **kw)
76 76
77 77 # FrontendWidget protected variables.
78 78 self._call_tip_widget = CallTipWidget(self._control)
79 79 self._completion_lexer = CompletionLexer(PythonLexer())
80 80 self._hidden = True
81 81 self._highlighter = FrontendHighlighter(self)
82 82 self._input_splitter = InputSplitter(input_mode='replace')
83 83 self._kernel_manager = None
84 84
85 85 # Configure the ConsoleWidget.
86 86 self.tab_width = 4
87 87 self._set_continuation_prompt('... ')
88 88
89 89 # Connect signal handlers.
90 90 document = self._control.document()
91 91 document.contentsChange.connect(self._document_contents_change)
92 92
93 93 #---------------------------------------------------------------------------
94 # 'QWidget' interface
95 #---------------------------------------------------------------------------
96
97 def focusOutEvent(self, event):
98 """ Reimplemented to hide calltips.
99 """
100 self._call_tip_widget.hide()
101 super(FrontendWidget, self).focusOutEvent(event)
102
103 def keyPressEvent(self, event):
104 """ Reimplemented to allow calltips to process events and to send
105 signals to the kernel.
106 """
107 if self._executing and event.key() == QtCore.Qt.Key_C and \
108 self._control_down(event.modifiers()):
109 self._interrupt_kernel()
110 else:
111 if self._call_tip_widget.isVisible():
112 self._call_tip_widget.keyPressEvent(event)
113 super(FrontendWidget, self).keyPressEvent(event)
114
115 #---------------------------------------------------------------------------
116 94 # 'ConsoleWidget' abstract interface
117 95 #---------------------------------------------------------------------------
118 96
119 97 def _is_complete(self, source, interactive):
120 98 """ Returns whether 'source' can be completely processed and a new
121 99 prompt created. When triggered by an Enter/Return key press,
122 100 'interactive' is True; otherwise, it is False.
123 101 """
124 102 complete = self._input_splitter.push(source.expandtabs(4))
125 103 if interactive:
126 104 complete = not self._input_splitter.push_accepts_more()
127 105 return complete
128 106
129 107 def _execute(self, source, hidden):
130 108 """ Execute 'source'. If 'hidden', do not show any output.
131 109 """
132 110 self.kernel_manager.xreq_channel.execute(source)
133 111 self._hidden = hidden
112
113 def _execute_interrupt(self):
114 """ Attempts to stop execution. Returns whether this method has an
115 implementation.
116 """
117 self._interrupt_kernel()
118 return True
134 119
135 120 def _prompt_started_hook(self):
136 121 """ Called immediately after a new prompt is displayed.
137 122 """
138 123 if not self._reading:
139 124 self._highlighter.highlighting_on = True
140 125
141 126 # Auto-indent if this is a continuation prompt.
142 127 if self._get_prompt_cursor().blockNumber() != \
143 128 self._get_end_cursor().blockNumber():
144 129 spaces = self._input_splitter.indent_spaces
145 130 self._append_plain_text('\t' * (spaces / self.tab_width))
146 131 self._append_plain_text(' ' * (spaces % self.tab_width))
147 132
148 133 def _prompt_finished_hook(self):
149 134 """ Called immediately after a prompt is finished, i.e. when some input
150 135 will be processed and a new prompt displayed.
151 136 """
152 137 if not self._reading:
153 138 self._highlighter.highlighting_on = False
154 139
155 140 def _tab_pressed(self):
156 141 """ Called when the tab key is pressed. Returns whether to continue
157 142 processing the event.
158 143 """
159 144 self._keep_cursor_in_buffer()
160 145 cursor = self._get_cursor()
161 146 return not self._complete()
162 147
163 148 #---------------------------------------------------------------------------
164 149 # 'FrontendWidget' interface
165 150 #---------------------------------------------------------------------------
166 151
167 152 def execute_file(self, path, hidden=False):
168 153 """ Attempts to execute file with 'path'. If 'hidden', no output is
169 154 shown.
170 155 """
171 156 self.execute('execfile("%s")' % path, hidden=hidden)
172 157
173 158 def _get_kernel_manager(self):
174 159 """ Returns the current kernel manager.
175 160 """
176 161 return self._kernel_manager
177 162
178 163 def _set_kernel_manager(self, kernel_manager):
179 164 """ Disconnect from the current kernel manager (if any) and set a new
180 165 kernel manager.
181 166 """
182 167 # Disconnect the old kernel manager, if necessary.
183 168 if self._kernel_manager is not None:
184 169 self._kernel_manager.started_channels.disconnect(
185 170 self._started_channels)
186 171 self._kernel_manager.stopped_channels.disconnect(
187 172 self._stopped_channels)
188 173
189 174 # Disconnect the old kernel manager's channels.
190 175 sub = self._kernel_manager.sub_channel
191 176 xreq = self._kernel_manager.xreq_channel
192 177 rep = self._kernel_manager.rep_channel
193 178 sub.message_received.disconnect(self._handle_sub)
194 179 xreq.execute_reply.disconnect(self._handle_execute_reply)
195 180 xreq.complete_reply.disconnect(self._handle_complete_reply)
196 181 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
197 182 rep.input_requested.disconnect(self._handle_req)
198 183
199 184 # Handle the case where the old kernel manager is still listening.
200 185 if self._kernel_manager.channels_running:
201 186 self._stopped_channels()
202 187
203 188 # Set the new kernel manager.
204 189 self._kernel_manager = kernel_manager
205 190 if kernel_manager is None:
206 191 return
207 192
208 193 # Connect the new kernel manager.
209 194 kernel_manager.started_channels.connect(self._started_channels)
210 195 kernel_manager.stopped_channels.connect(self._stopped_channels)
211 196
212 197 # Connect the new kernel manager's channels.
213 198 sub = kernel_manager.sub_channel
214 199 xreq = kernel_manager.xreq_channel
215 200 rep = kernel_manager.rep_channel
216 201 sub.message_received.connect(self._handle_sub)
217 202 xreq.execute_reply.connect(self._handle_execute_reply)
218 203 xreq.complete_reply.connect(self._handle_complete_reply)
219 204 xreq.object_info_reply.connect(self._handle_object_info_reply)
220 205 rep.input_requested.connect(self._handle_req)
221 206
222 207 # Handle the case where the kernel manager started channels before
223 208 # we connected.
224 209 if kernel_manager.channels_running:
225 210 self._started_channels()
226 211
227 212 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
228 213
229 214 #---------------------------------------------------------------------------
230 215 # 'FrontendWidget' protected interface
231 216 #---------------------------------------------------------------------------
232 217
233 218 def _call_tip(self):
234 219 """ Shows a call tip, if appropriate, at the current cursor location.
235 220 """
236 221 # Decide if it makes sense to show a call tip
237 222 cursor = self._get_cursor()
238 223 cursor.movePosition(QtGui.QTextCursor.Left)
239 224 document = self._control.document()
240 225 if document.characterAt(cursor.position()).toAscii() != '(':
241 226 return False
242 227 context = self._get_context(cursor)
243 228 if not context:
244 229 return False
245 230
246 231 # Send the metadata request to the kernel
247 232 name = '.'.join(context)
248 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
249 self._calltip_pos = self._get_cursor().position()
233 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
234 self._call_tip_pos = self._get_cursor().position()
250 235 return True
251 236
252 237 def _complete(self):
253 238 """ Performs completion at the current cursor location.
254 239 """
255 240 # Decide if it makes sense to do completion
256 241 context = self._get_context()
257 242 if not context:
258 243 return False
259 244
260 245 # Send the completion request to the kernel
261 246 text = '.'.join(context)
262 247 self._complete_id = self.kernel_manager.xreq_channel.complete(
263 248 text, self.input_buffer_cursor_line, self.input_buffer)
264 249 self._complete_pos = self._get_cursor().position()
265 250 return True
266 251
267 252 def _get_banner(self):
268 253 """ Gets a banner to display at the beginning of a session.
269 254 """
270 255 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
271 256 '"license" for more information.'
272 257 return banner % (sys.version, sys.platform)
273 258
274 259 def _get_context(self, cursor=None):
275 260 """ Gets the context at the current cursor location.
276 261 """
277 262 if cursor is None:
278 263 cursor = self._get_cursor()
279 264 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
280 265 QtGui.QTextCursor.KeepAnchor)
281 266 text = str(cursor.selection().toPlainText())
282 267 return self._completion_lexer.get_context(text)
283 268
284 269 def _interrupt_kernel(self):
285 270 """ Attempts to the interrupt the kernel.
286 271 """
287 272 if self.kernel_manager.has_kernel:
288 273 self.kernel_manager.signal_kernel(signal.SIGINT)
289 274 else:
290 275 self._append_plain_text('Kernel process is either remote or '
291 276 'unspecified. Cannot interrupt.\n')
292 277
293 278 def _show_interpreter_prompt(self):
294 279 """ Shows a prompt for the interpreter.
295 280 """
296 281 self._show_prompt('>>> ')
297 282
298 283 #------ Signal handlers ----------------------------------------------------
299 284
300 285 def _started_channels(self):
301 286 """ Called when the kernel manager has started listening.
302 287 """
303 288 self._reset()
304 289 self._append_plain_text(self._get_banner())
305 290 self._show_interpreter_prompt()
306 291
307 292 def _stopped_channels(self):
308 293 """ Called when the kernel manager has stopped listening.
309 294 """
310 295 # FIXME: Print a message here?
311 296 pass
312 297
313 298 def _document_contents_change(self, position, removed, added):
314 """ Called whenever the document's content changes. Display a calltip
299 """ Called whenever the document's content changes. Display a call tip
315 300 if appropriate.
316 301 """
317 302 # Calculate where the cursor should be *after* the change:
318 303 position += added
319 304
320 305 document = self._control.document()
321 306 if position == self._get_cursor().position():
322 307 self._call_tip()
323 308
324 309 def _handle_req(self, req):
325 310 # Make sure that all output from the SUB channel has been processed
326 311 # before entering readline mode.
327 312 self.kernel_manager.sub_channel.flush()
328 313
329 314 def callback(line):
330 315 self.kernel_manager.rep_channel.input(line)
331 316 self._readline(req['content']['prompt'], callback=callback)
332 317
333 318 def _handle_sub(self, omsg):
334 319 if self._hidden:
335 320 return
336 321 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
337 322 if handler is not None:
338 323 handler(omsg)
339 324
340 325 def _handle_pyout(self, omsg):
341 326 self._append_plain_text(omsg['content']['data'] + '\n')
342 327
343 328 def _handle_stream(self, omsg):
344 329 self._append_plain_text(omsg['content']['data'])
345 330 self._control.moveCursor(QtGui.QTextCursor.End)
346 331
347 332 def _handle_execute_reply(self, reply):
348 333 if self._hidden:
349 334 return
350 335
351 336 # Make sure that all output from the SUB channel has been processed
352 337 # before writing a new prompt.
353 338 self.kernel_manager.sub_channel.flush()
354 339
355 340 status = reply['content']['status']
356 341 if status == 'error':
357 342 self._handle_execute_error(reply)
358 343 elif status == 'aborted':
359 344 text = "ERROR: ABORTED\n"
360 345 self._append_plain_text(text)
361 346 self._hidden = True
362 347 self._show_interpreter_prompt()
363 348 self.executed.emit(reply)
364 349
365 350 def _handle_execute_error(self, reply):
366 351 content = reply['content']
367 352 traceback = ''.join(content['traceback'])
368 353 self._append_plain_text(traceback)
369 354
370 355 def _handle_complete_reply(self, rep):
371 356 cursor = self._get_cursor()
372 357 if rep['parent_header']['msg_id'] == self._complete_id and \
373 358 cursor.position() == self._complete_pos:
374 359 text = '.'.join(self._get_context())
375 360 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
376 361 self._complete_with_items(cursor, rep['content']['matches'])
377 362
378 363 def _handle_object_info_reply(self, rep):
379 364 cursor = self._get_cursor()
380 if rep['parent_header']['msg_id'] == self._calltip_id and \
381 cursor.position() == self._calltip_pos:
365 if rep['parent_header']['msg_id'] == self._call_tip_id and \
366 cursor.position() == self._call_tip_pos:
382 367 doc = rep['content']['docstring']
383 368 if doc:
384 369 self._call_tip_widget.show_docstring(doc)
General Comments 0
You need to be logged in to leave comments. Login now