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