##// END OF EJS Templates
* Added ability to interrupt a kernel to FrontendWidget...
epatters -
Show More
@@ -1,171 +1,182 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 # 'QWidget' 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 #--------------------------------------------------------------------------
38 # 'QWidget' interface
39 #--------------------------------------------------------------------------
40
37 def hideEvent(self, event):
41 def hideEvent(self, event):
38 """ Reimplemented to disconnect the cursor movement handler.
42 """ Reimplemented to disconnect the cursor movement handler.
39 """
43 """
40 QtGui.QLabel.hideEvent(self, event)
44 QtGui.QLabel.hideEvent(self, event)
41 self.parent().cursorPositionChanged.disconnect(self._update_tip)
45 self.parent().cursorPositionChanged.disconnect(self._update_tip)
42
46
47 def keyPressEvent(self, event):
48 """ Reimplemented to hide on certain key presses.
49 """
50 if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
51 QtCore.Qt.Key_Escape):
52 self.hide()
53
43 def paintEvent(self, event):
54 def paintEvent(self, event):
44 """ Reimplemented to paint the background panel.
55 """ Reimplemented to paint the background panel.
45 """
56 """
46 painter = QtGui.QStylePainter(self)
57 painter = QtGui.QStylePainter(self)
47 option = QtGui.QStyleOptionFrame()
58 option = QtGui.QStyleOptionFrame()
48 option.init(self)
59 option.init(self)
49 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
60 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
50 painter.end()
61 painter.end()
51
62
52 QtGui.QLabel.paintEvent(self, event)
63 QtGui.QLabel.paintEvent(self, event)
53
64
54 def showEvent(self, event):
65 def showEvent(self, event):
55 """ Reimplemented to connect the cursor movement handler.
66 """ Reimplemented to connect the cursor movement handler.
56 """
67 """
57 QtGui.QLabel.showEvent(self, event)
68 QtGui.QLabel.showEvent(self, event)
58 self.parent().cursorPositionChanged.connect(self._update_tip)
69 self.parent().cursorPositionChanged.connect(self._update_tip)
59
70
60 #--------------------------------------------------------------------------
71 #--------------------------------------------------------------------------
61 # 'CallTipWidget' interface
72 # 'CallTipWidget' interface
62 #--------------------------------------------------------------------------
73 #--------------------------------------------------------------------------
63
74
64 def show_docstring(self, doc, maxlines=20):
75 def show_docstring(self, doc, maxlines=20):
65 """ Attempts to show the specified docstring at the current cursor
76 """ Attempts to show the specified docstring at the current cursor
66 location. The docstring is dedented and possibly truncated for
77 location. The docstring is dedented and possibly truncated for
67 length.
78 length.
68 """
79 """
69 doc = dedent(doc.rstrip()).lstrip()
80 doc = dedent(doc.rstrip()).lstrip()
70 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
81 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
71 if match:
82 if match:
72 doc = doc[:match.end()] + '\n[Documentation continues...]'
83 doc = doc[:match.end()] + '\n[Documentation continues...]'
73 return self.show_tip(doc)
84 return self.show_tip(doc)
74
85
75 def show_tip(self, tip):
86 def show_tip(self, tip):
76 """ Attempts to show the specified tip at the current cursor location.
87 """ Attempts to show the specified tip at the current cursor location.
77 """
88 """
78 text_edit = self.parent()
89 text_edit = self.parent()
79 document = text_edit.document()
90 document = text_edit.document()
80 cursor = text_edit.textCursor()
91 cursor = text_edit.textCursor()
81 search_pos = cursor.position() - 1
92 search_pos = cursor.position() - 1
82 self._start_position, _ = self._find_parenthesis(search_pos,
93 self._start_position, _ = self._find_parenthesis(search_pos,
83 forward=False)
94 forward=False)
84 if self._start_position == -1:
95 if self._start_position == -1:
85 return False
96 return False
86
97
87 point = text_edit.cursorRect(cursor).bottomRight()
98 point = text_edit.cursorRect(cursor).bottomRight()
88 point = text_edit.mapToGlobal(point)
99 point = text_edit.mapToGlobal(point)
89 self.move(point)
100 self.move(point)
90 self.setText(tip)
101 self.setText(tip)
91 if self.isVisible():
102 if self.isVisible():
92 self.resize(self.sizeHint())
103 self.resize(self.sizeHint())
93 else:
104 else:
94 self.show()
105 self.show()
95 return True
106 return True
96
107
97 #--------------------------------------------------------------------------
108 #--------------------------------------------------------------------------
98 # Protected interface
109 # Protected interface
99 #--------------------------------------------------------------------------
110 #--------------------------------------------------------------------------
100
111
101 def _find_parenthesis(self, position, forward=True):
112 def _find_parenthesis(self, position, forward=True):
102 """ If 'forward' is True (resp. False), proceed forwards
113 """ If 'forward' is True (resp. False), proceed forwards
103 (resp. backwards) through the line that contains 'position' until an
114 (resp. backwards) through the line that contains 'position' until an
104 unmatched closing (resp. opening) parenthesis is found. Returns a
115 unmatched closing (resp. opening) parenthesis is found. Returns a
105 tuple containing the position of this parenthesis (or -1 if it is
116 tuple containing the position of this parenthesis (or -1 if it is
106 not found) and the number commas (at depth 0) found along the way.
117 not found) and the number commas (at depth 0) found along the way.
107 """
118 """
108 commas = depth = 0
119 commas = depth = 0
109 document = self.parent().document()
120 document = self.parent().document()
110 qchar = document.characterAt(position)
121 qchar = document.characterAt(position)
111 while (position > 0 and qchar.isPrint() and
122 while (position > 0 and qchar.isPrint() and
112 # Need to check explicitly for line/paragraph separators:
123 # Need to check explicitly for line/paragraph separators:
113 qchar.unicode() not in (0x2028, 0x2029)):
124 qchar.unicode() not in (0x2028, 0x2029)):
114 char = qchar.toAscii()
125 char = qchar.toAscii()
115 if char == ',' and depth == 0:
126 if char == ',' and depth == 0:
116 commas += 1
127 commas += 1
117 elif char == ')':
128 elif char == ')':
118 if forward and depth == 0:
129 if forward and depth == 0:
119 break
130 break
120 depth += 1
131 depth += 1
121 elif char == '(':
132 elif char == '(':
122 if not forward and depth == 0:
133 if not forward and depth == 0:
123 break
134 break
124 depth -= 1
135 depth -= 1
125 position += 1 if forward else -1
136 position += 1 if forward else -1
126 qchar = document.characterAt(position)
137 qchar = document.characterAt(position)
127 else:
138 else:
128 position = -1
139 position = -1
129 return position, commas
140 return position, commas
130
141
131 def _highlight_tip(self, tip, current_argument):
142 def _highlight_tip(self, tip, current_argument):
132 """ Highlight the current argument (arguments start at 0), ending at the
143 """ Highlight the current argument (arguments start at 0), ending at the
133 next comma or unmatched closing parenthesis.
144 next comma or unmatched closing parenthesis.
134
145
135 FIXME: This is an unreliable way to do things and it isn't being
146 FIXME: This is an unreliable way to do things and it isn't being
136 used right now. Instead, we should use inspect.getargspec
147 used right now. Instead, we should use inspect.getargspec
137 metadata for this purpose.
148 metadata for this purpose.
138 """
149 """
139 start = tip.find('(')
150 start = tip.find('(')
140 if start != -1:
151 if start != -1:
141 for i in xrange(current_argument):
152 for i in xrange(current_argument):
142 start = tip.find(',', start)
153 start = tip.find(',', start)
143 if start != -1:
154 if start != -1:
144 end = start + 1
155 end = start + 1
145 while end < len(tip):
156 while end < len(tip):
146 char = tip[end]
157 char = tip[end]
147 depth = 0
158 depth = 0
148 if (char == ',' and depth == 0):
159 if (char == ',' and depth == 0):
149 break
160 break
150 elif char == '(':
161 elif char == '(':
151 depth += 1
162 depth += 1
152 elif char == ')':
163 elif char == ')':
153 if depth == 0:
164 if depth == 0:
154 break
165 break
155 depth -= 1
166 depth -= 1
156 end += 1
167 end += 1
157 tip = tip[:start+1] + '<font color="blue">' + \
168 tip = tip[:start+1] + '<font color="blue">' + \
158 tip[start+1:end] + '</font>' + tip[end:]
169 tip[start+1:end] + '</font>' + tip[end:]
159 tip = tip.replace('\n', '<br/>')
170 tip = tip.replace('\n', '<br/>')
160 return tip
171 return tip
161
172
162 def _update_tip(self):
173 def _update_tip(self):
163 """ Updates the tip based on user cursor movement.
174 """ Updates the tip based on user cursor movement.
164 """
175 """
165 cursor = self.parent().textCursor()
176 cursor = self.parent().textCursor()
166 if cursor.position() <= self._start_position:
177 if cursor.position() <= self._start_position:
167 self.hide()
178 self.hide()
168 else:
179 else:
169 position, commas = self._find_parenthesis(self._start_position + 1)
180 position, commas = self._find_parenthesis(self._start_position + 1)
170 if position != -1:
181 if position != -1:
171 self.hide()
182 self.hide()
@@ -1,761 +1,766 b''
1 # Standard library imports
1 # Standard library imports
2 import re
2 import re
3 import sys
3 import sys
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 # Local imports
8 # Local imports
9 from completion_widget import CompletionWidget
9 from completion_widget import CompletionWidget
10
10
11
11
12 class AnsiCodeProcessor(object):
12 class AnsiCodeProcessor(object):
13 """ Translates ANSI escape codes into readable attributes.
13 """ Translates ANSI escape codes into readable attributes.
14 """
14 """
15
15
16 def __init__(self):
16 def __init__(self):
17 self.ansi_colors = ( # Normal, Bright/Light
17 self.ansi_colors = ( # Normal, Bright/Light
18 ('#000000', '#7f7f7f'), # 0: black
18 ('#000000', '#7f7f7f'), # 0: black
19 ('#cd0000', '#ff0000'), # 1: red
19 ('#cd0000', '#ff0000'), # 1: red
20 ('#00cd00', '#00ff00'), # 2: green
20 ('#00cd00', '#00ff00'), # 2: green
21 ('#cdcd00', '#ffff00'), # 3: yellow
21 ('#cdcd00', '#ffff00'), # 3: yellow
22 ('#0000ee', '#0000ff'), # 4: blue
22 ('#0000ee', '#0000ff'), # 4: blue
23 ('#cd00cd', '#ff00ff'), # 5: magenta
23 ('#cd00cd', '#ff00ff'), # 5: magenta
24 ('#00cdcd', '#00ffff'), # 6: cyan
24 ('#00cdcd', '#00ffff'), # 6: cyan
25 ('#e5e5e5', '#ffffff')) # 7: white
25 ('#e5e5e5', '#ffffff')) # 7: white
26 self.reset()
26 self.reset()
27
27
28 def set_code(self, code):
28 def set_code(self, code):
29 """ Set attributes based on code.
29 """ Set attributes based on code.
30 """
30 """
31 if code == 0:
31 if code == 0:
32 self.reset()
32 self.reset()
33 elif code == 1:
33 elif code == 1:
34 self.intensity = 1
34 self.intensity = 1
35 self.bold = True
35 self.bold = True
36 elif code == 3:
36 elif code == 3:
37 self.italic = True
37 self.italic = True
38 elif code == 4:
38 elif code == 4:
39 self.underline = True
39 self.underline = True
40 elif code == 22:
40 elif code == 22:
41 self.intensity = 0
41 self.intensity = 0
42 self.bold = False
42 self.bold = False
43 elif code == 23:
43 elif code == 23:
44 self.italic = False
44 self.italic = False
45 elif code == 24:
45 elif code == 24:
46 self.underline = False
46 self.underline = False
47 elif code >= 30 and code <= 37:
47 elif code >= 30 and code <= 37:
48 self.foreground_color = code - 30
48 self.foreground_color = code - 30
49 elif code == 39:
49 elif code == 39:
50 self.foreground_color = None
50 self.foreground_color = None
51 elif code >= 40 and code <= 47:
51 elif code >= 40 and code <= 47:
52 self.background_color = code - 40
52 self.background_color = code - 40
53 elif code == 49:
53 elif code == 49:
54 self.background_color = None
54 self.background_color = None
55
55
56 def reset(self):
56 def reset(self):
57 """ Reset attributs to their default values.
57 """ Reset attributs to their default values.
58 """
58 """
59 self.intensity = 0
59 self.intensity = 0
60 self.italic = False
60 self.italic = False
61 self.bold = False
61 self.bold = False
62 self.underline = False
62 self.underline = False
63 self.foreground_color = None
63 self.foreground_color = None
64 self.background_color = None
64 self.background_color = None
65
65
66
66
67 class QtAnsiCodeProcessor(AnsiCodeProcessor):
67 class QtAnsiCodeProcessor(AnsiCodeProcessor):
68 """ Translates ANSI escape codes into QTextCharFormats.
68 """ Translates ANSI escape codes into QTextCharFormats.
69 """
69 """
70
70
71 def get_format(self):
71 def get_format(self):
72 """ Returns a QTextCharFormat that encodes the current style attributes.
72 """ Returns a QTextCharFormat that encodes the current style attributes.
73 """
73 """
74 format = QtGui.QTextCharFormat()
74 format = QtGui.QTextCharFormat()
75
75
76 # Set foreground color
76 # Set foreground color
77 if self.foreground_color is not None:
77 if self.foreground_color is not None:
78 color = self.ansi_colors[self.foreground_color][self.intensity]
78 color = self.ansi_colors[self.foreground_color][self.intensity]
79 format.setForeground(QtGui.QColor(color))
79 format.setForeground(QtGui.QColor(color))
80
80
81 # Set background color
81 # Set background color
82 if self.background_color is not None:
82 if self.background_color is not None:
83 color = self.ansi_colors[self.background_color][self.intensity]
83 color = self.ansi_colors[self.background_color][self.intensity]
84 format.setBackground(QtGui.QColor(color))
84 format.setBackground(QtGui.QColor(color))
85
85
86 # Set font weight/style options
86 # Set font weight/style options
87 if self.bold:
87 if self.bold:
88 format.setFontWeight(QtGui.QFont.Bold)
88 format.setFontWeight(QtGui.QFont.Bold)
89 else:
89 else:
90 format.setFontWeight(QtGui.QFont.Normal)
90 format.setFontWeight(QtGui.QFont.Normal)
91 format.setFontItalic(self.italic)
91 format.setFontItalic(self.italic)
92 format.setFontUnderline(self.underline)
92 format.setFontUnderline(self.underline)
93
93
94 return format
94 return format
95
95
96
96
97 class ConsoleWidget(QtGui.QPlainTextEdit):
97 class ConsoleWidget(QtGui.QPlainTextEdit):
98 """ Base class for console-type widgets. This class is mainly concerned with
98 """ Base class for console-type widgets. This class is mainly concerned with
99 dealing with the prompt, keeping the cursor inside the editing line, and
99 dealing with the prompt, keeping the cursor inside the editing line, and
100 handling ANSI escape sequences.
100 handling ANSI escape sequences.
101 """
101 """
102
102
103 # Whether to process ANSI escape codes.
103 # Whether to process ANSI escape codes.
104 ansi_codes = True
104 ansi_codes = True
105
105
106 # The maximum number of lines of text before truncation.
106 # The maximum number of lines of text before truncation.
107 buffer_size = 500
107 buffer_size = 500
108
108
109 # Whether to use a CompletionWidget or plain text output for tab completion.
109 # Whether to use a CompletionWidget or plain text output for tab completion.
110 gui_completion = True
110 gui_completion = True
111
111
112 # Whether to override ShortcutEvents for the keybindings defined by this
112 # Whether to override ShortcutEvents for the keybindings defined by this
113 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
113 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
114 # priority (when it has focus) over, e.g., window-level menu shortcuts.
114 # priority (when it has focus) over, e.g., window-level menu shortcuts.
115 override_shortcuts = False
115 override_shortcuts = False
116
116
117 # Protected class variables.
117 # Protected class variables.
118 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
118 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
119 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
119 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
120 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
120 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
121 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
121 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
122 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
122 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
123 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
123 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
124 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
124 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
125 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
125 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
126 _shortcuts = set(_ctrl_down_remap.keys() +
127 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
126
128
127 #---------------------------------------------------------------------------
129 #---------------------------------------------------------------------------
128 # 'QObject' interface
130 # 'QObject' interface
129 #---------------------------------------------------------------------------
131 #---------------------------------------------------------------------------
130
132
131 def __init__(self, parent=None):
133 def __init__(self, parent=None):
132 QtGui.QPlainTextEdit.__init__(self, parent)
134 QtGui.QPlainTextEdit.__init__(self, parent)
133
135
134 # Initialize protected variables.
136 # Initialize protected variables.
135 self._ansi_processor = QtAnsiCodeProcessor()
137 self._ansi_processor = QtAnsiCodeProcessor()
136 self._completion_widget = CompletionWidget(self)
138 self._completion_widget = CompletionWidget(self)
137 self._continuation_prompt = '> '
139 self._continuation_prompt = '> '
138 self._executing = False
140 self._executing = False
139 self._prompt = ''
141 self._prompt = ''
140 self._prompt_pos = 0
142 self._prompt_pos = 0
141 self._reading = False
143 self._reading = False
142
144
143 # Set a monospaced font.
145 # Set a monospaced font.
144 self.reset_font()
146 self.reset_font()
145
147
146 # Define a custom context menu.
148 # Define a custom context menu.
147 self._context_menu = QtGui.QMenu(self)
149 self._context_menu = QtGui.QMenu(self)
148
150
149 copy_action = QtGui.QAction('Copy', self)
151 copy_action = QtGui.QAction('Copy', self)
150 copy_action.triggered.connect(self.copy)
152 copy_action.triggered.connect(self.copy)
151 self.copyAvailable.connect(copy_action.setEnabled)
153 self.copyAvailable.connect(copy_action.setEnabled)
152 self._context_menu.addAction(copy_action)
154 self._context_menu.addAction(copy_action)
153
155
154 self._paste_action = QtGui.QAction('Paste', self)
156 self._paste_action = QtGui.QAction('Paste', self)
155 self._paste_action.triggered.connect(self.paste)
157 self._paste_action.triggered.connect(self.paste)
156 self._context_menu.addAction(self._paste_action)
158 self._context_menu.addAction(self._paste_action)
157 self._context_menu.addSeparator()
159 self._context_menu.addSeparator()
158
160
159 select_all_action = QtGui.QAction('Select All', self)
161 select_all_action = QtGui.QAction('Select All', self)
160 select_all_action.triggered.connect(self.selectAll)
162 select_all_action.triggered.connect(self.selectAll)
161 self._context_menu.addAction(select_all_action)
163 self._context_menu.addAction(select_all_action)
162
164
163 def event(self, event):
165 def event(self, event):
164 """ Reimplemented to override shortcuts, if necessary.
166 """ Reimplemented to override shortcuts, if necessary.
165 """
167 """
166 # On Mac OS, it is always unnecessary to override shortcuts, hence the
168 # On Mac OS, it is always unnecessary to override shortcuts, hence the
167 # check below. Users should just use the Control key instead of the
169 # check below. Users should just use the Control key instead of the
168 # Command key.
170 # Command key.
169 if self.override_shortcuts and \
171 if self.override_shortcuts and \
170 sys.platform != 'darwin' and \
172 sys.platform != 'darwin' and \
171 event.type() == QtCore.QEvent.ShortcutOverride and \
173 event.type() == QtCore.QEvent.ShortcutOverride and \
172 self._control_down(event.modifiers()) and \
174 self._control_down(event.modifiers()) and \
173 event.key() in self._ctrl_down_remap:
175 event.key() in self._shortcuts:
174 event.accept()
176 event.accept()
175 return True
177 return True
176 else:
178 else:
177 return QtGui.QPlainTextEdit.event(self, event)
179 return QtGui.QPlainTextEdit.event(self, event)
178
180
179 #---------------------------------------------------------------------------
181 #---------------------------------------------------------------------------
180 # 'QWidget' interface
182 # 'QWidget' interface
181 #---------------------------------------------------------------------------
183 #---------------------------------------------------------------------------
182
184
183 def contextMenuEvent(self, event):
185 def contextMenuEvent(self, event):
184 """ Reimplemented to create a menu without destructive actions like
186 """ Reimplemented to create a menu without destructive actions like
185 'Cut' and 'Delete'.
187 'Cut' and 'Delete'.
186 """
188 """
187 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
189 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
188 self._paste_action.setEnabled(not clipboard_empty)
190 self._paste_action.setEnabled(not clipboard_empty)
189
191
190 self._context_menu.exec_(event.globalPos())
192 self._context_menu.exec_(event.globalPos())
191
193
192 def keyPressEvent(self, event):
194 def keyPressEvent(self, event):
193 """ Reimplemented to create a console-like interface.
195 """ Reimplemented to create a console-like interface.
194 """
196 """
195 intercepted = False
197 intercepted = False
196 cursor = self.textCursor()
198 cursor = self.textCursor()
197 position = cursor.position()
199 position = cursor.position()
198 key = event.key()
200 key = event.key()
199 ctrl_down = self._control_down(event.modifiers())
201 ctrl_down = self._control_down(event.modifiers())
200 alt_down = event.modifiers() & QtCore.Qt.AltModifier
202 alt_down = event.modifiers() & QtCore.Qt.AltModifier
201 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
203 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
202
204
203 # Even though we have reimplemented 'paste', the C++ level slot is still
205 # Even though we have reimplemented 'paste', the C++ level slot is still
204 # called by Qt. So we intercept the key press here.
206 # called by Qt. So we intercept the key press here.
205 if event.matches(QtGui.QKeySequence.Paste):
207 if event.matches(QtGui.QKeySequence.Paste):
206 self.paste()
208 self.paste()
207 intercepted = True
209 intercepted = True
208
210
209 elif ctrl_down:
211 elif ctrl_down:
210 if key in self._ctrl_down_remap:
212 if key in self._ctrl_down_remap:
211 ctrl_down = False
213 ctrl_down = False
212 key = self._ctrl_down_remap[key]
214 key = self._ctrl_down_remap[key]
213 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
215 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
214 QtCore.Qt.NoModifier)
216 QtCore.Qt.NoModifier)
215
217
216 elif key == QtCore.Qt.Key_K:
218 elif key == QtCore.Qt.Key_K:
217 if self._in_buffer(position):
219 if self._in_buffer(position):
218 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
220 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
219 QtGui.QTextCursor.KeepAnchor)
221 QtGui.QTextCursor.KeepAnchor)
220 cursor.removeSelectedText()
222 cursor.removeSelectedText()
221 intercepted = True
223 intercepted = True
222
224
225 elif key == QtCore.Qt.Key_X:
226 intercepted = True
227
223 elif key == QtCore.Qt.Key_Y:
228 elif key == QtCore.Qt.Key_Y:
224 self.paste()
229 self.paste()
225 intercepted = True
230 intercepted = True
226
231
227 elif alt_down:
232 elif alt_down:
228 if key == QtCore.Qt.Key_B:
233 if key == QtCore.Qt.Key_B:
229 self.setTextCursor(self._get_word_start_cursor(position))
234 self.setTextCursor(self._get_word_start_cursor(position))
230 intercepted = True
235 intercepted = True
231
236
232 elif key == QtCore.Qt.Key_F:
237 elif key == QtCore.Qt.Key_F:
233 self.setTextCursor(self._get_word_end_cursor(position))
238 self.setTextCursor(self._get_word_end_cursor(position))
234 intercepted = True
239 intercepted = True
235
240
236 elif key == QtCore.Qt.Key_Backspace:
241 elif key == QtCore.Qt.Key_Backspace:
237 cursor = self._get_word_start_cursor(position)
242 cursor = self._get_word_start_cursor(position)
238 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
243 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
239 cursor.removeSelectedText()
244 cursor.removeSelectedText()
240 intercepted = True
245 intercepted = True
241
246
242 elif key == QtCore.Qt.Key_D:
247 elif key == QtCore.Qt.Key_D:
243 cursor = self._get_word_end_cursor(position)
248 cursor = self._get_word_end_cursor(position)
244 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
249 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
245 cursor.removeSelectedText()
250 cursor.removeSelectedText()
246 intercepted = True
251 intercepted = True
247
252
248 if self._completion_widget.isVisible():
253 if self._completion_widget.isVisible():
249 self._completion_widget.keyPressEvent(event)
254 self._completion_widget.keyPressEvent(event)
250 intercepted = event.isAccepted()
255 intercepted = event.isAccepted()
251
256
252 else:
257 else:
253 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
258 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
254 if self._reading:
259 if self._reading:
255 self._reading = False
260 self._reading = False
256 elif not self._executing:
261 elif not self._executing:
257 self.execute(interactive=True)
262 self.execute(interactive=True)
258 intercepted = True
263 intercepted = True
259
264
260 elif key == QtCore.Qt.Key_Up:
265 elif key == QtCore.Qt.Key_Up:
261 if self._reading or not self._up_pressed():
266 if self._reading or not self._up_pressed():
262 intercepted = True
267 intercepted = True
263 else:
268 else:
264 prompt_line = self._get_prompt_cursor().blockNumber()
269 prompt_line = self._get_prompt_cursor().blockNumber()
265 intercepted = cursor.blockNumber() <= prompt_line
270 intercepted = cursor.blockNumber() <= prompt_line
266
271
267 elif key == QtCore.Qt.Key_Down:
272 elif key == QtCore.Qt.Key_Down:
268 if self._reading or not self._down_pressed():
273 if self._reading or not self._down_pressed():
269 intercepted = True
274 intercepted = True
270 else:
275 else:
271 end_line = self._get_end_cursor().blockNumber()
276 end_line = self._get_end_cursor().blockNumber()
272 intercepted = cursor.blockNumber() == end_line
277 intercepted = cursor.blockNumber() == end_line
273
278
274 elif key == QtCore.Qt.Key_Tab:
279 elif key == QtCore.Qt.Key_Tab:
275 if self._reading:
280 if self._reading:
276 intercepted = False
281 intercepted = False
277 else:
282 else:
278 intercepted = not self._tab_pressed()
283 intercepted = not self._tab_pressed()
279
284
280 elif key == QtCore.Qt.Key_Left:
285 elif key == QtCore.Qt.Key_Left:
281 intercepted = not self._in_buffer(position - 1)
286 intercepted = not self._in_buffer(position - 1)
282
287
283 elif key == QtCore.Qt.Key_Home:
288 elif key == QtCore.Qt.Key_Home:
284 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
289 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
285 start_pos = cursor.position()
290 start_pos = cursor.position()
286 start_line = cursor.blockNumber()
291 start_line = cursor.blockNumber()
287 if start_line == self._get_prompt_cursor().blockNumber():
292 if start_line == self._get_prompt_cursor().blockNumber():
288 start_pos += len(self._prompt)
293 start_pos += len(self._prompt)
289 else:
294 else:
290 start_pos += len(self._continuation_prompt)
295 start_pos += len(self._continuation_prompt)
291 if shift_down and self._in_buffer(position):
296 if shift_down and self._in_buffer(position):
292 self._set_selection(position, start_pos)
297 self._set_selection(position, start_pos)
293 else:
298 else:
294 self._set_position(start_pos)
299 self._set_position(start_pos)
295 intercepted = True
300 intercepted = True
296
301
297 elif key == QtCore.Qt.Key_Backspace and not alt_down:
302 elif key == QtCore.Qt.Key_Backspace and not alt_down:
298
303
299 # Line deletion (remove continuation prompt)
304 # Line deletion (remove continuation prompt)
300 len_prompt = len(self._continuation_prompt)
305 len_prompt = len(self._continuation_prompt)
301 if cursor.columnNumber() == len_prompt and \
306 if cursor.columnNumber() == len_prompt and \
302 position != self._prompt_pos:
307 position != self._prompt_pos:
303 cursor.setPosition(position - len_prompt,
308 cursor.setPosition(position - len_prompt,
304 QtGui.QTextCursor.KeepAnchor)
309 QtGui.QTextCursor.KeepAnchor)
305 cursor.removeSelectedText()
310 cursor.removeSelectedText()
306
311
307 # Regular backwards deletion
312 # Regular backwards deletion
308 else:
313 else:
309 anchor = cursor.anchor()
314 anchor = cursor.anchor()
310 if anchor == position:
315 if anchor == position:
311 intercepted = not self._in_buffer(position - 1)
316 intercepted = not self._in_buffer(position - 1)
312 else:
317 else:
313 intercepted = not self._in_buffer(min(anchor, position))
318 intercepted = not self._in_buffer(min(anchor, position))
314
319
315 elif key == QtCore.Qt.Key_Delete:
320 elif key == QtCore.Qt.Key_Delete:
316 anchor = cursor.anchor()
321 anchor = cursor.anchor()
317 intercepted = not self._in_buffer(min(anchor, position))
322 intercepted = not self._in_buffer(min(anchor, position))
318
323
319 # Don't move cursor if control is down to allow copy-paste using
324 # Don't move cursor if control is down to allow copy-paste using
320 # the keyboard in any part of the buffer.
325 # the keyboard in any part of the buffer.
321 if not ctrl_down:
326 if not ctrl_down:
322 self._keep_cursor_in_buffer()
327 self._keep_cursor_in_buffer()
323
328
324 if not intercepted:
329 if not intercepted:
325 QtGui.QPlainTextEdit.keyPressEvent(self, event)
330 QtGui.QPlainTextEdit.keyPressEvent(self, event)
326
331
327 #--------------------------------------------------------------------------
332 #--------------------------------------------------------------------------
328 # 'QPlainTextEdit' interface
333 # 'QPlainTextEdit' interface
329 #--------------------------------------------------------------------------
334 #--------------------------------------------------------------------------
330
335
331 def appendPlainText(self, text):
336 def appendPlainText(self, text):
332 """ Reimplemented to not append text as a new paragraph, which doesn't
337 """ Reimplemented to not append text as a new paragraph, which doesn't
333 make sense for a console widget. Also, if enabled, handle ANSI
338 make sense for a console widget. Also, if enabled, handle ANSI
334 codes.
339 codes.
335 """
340 """
336 cursor = self.textCursor()
341 cursor = self.textCursor()
337 cursor.movePosition(QtGui.QTextCursor.End)
342 cursor.movePosition(QtGui.QTextCursor.End)
338
343
339 if self.ansi_codes:
344 if self.ansi_codes:
340 format = QtGui.QTextCharFormat()
345 format = QtGui.QTextCharFormat()
341 previous_end = 0
346 previous_end = 0
342 for match in self._ansi_pattern.finditer(text):
347 for match in self._ansi_pattern.finditer(text):
343 cursor.insertText(text[previous_end:match.start()], format)
348 cursor.insertText(text[previous_end:match.start()], format)
344 previous_end = match.end()
349 previous_end = match.end()
345 for code in match.group(1).split(';'):
350 for code in match.group(1).split(';'):
346 self._ansi_processor.set_code(int(code))
351 self._ansi_processor.set_code(int(code))
347 format = self._ansi_processor.get_format()
352 format = self._ansi_processor.get_format()
348 cursor.insertText(text[previous_end:], format)
353 cursor.insertText(text[previous_end:], format)
349 else:
354 else:
350 cursor.insertText(text)
355 cursor.insertText(text)
351
356
352 def clear(self, keep_input=False):
357 def clear(self, keep_input=False):
353 """ Reimplemented to write a new prompt. If 'keep_input' is set,
358 """ Reimplemented to write a new prompt. If 'keep_input' is set,
354 restores the old input buffer when the new prompt is written.
359 restores the old input buffer when the new prompt is written.
355 """
360 """
356 super(ConsoleWidget, self).clear()
361 super(ConsoleWidget, self).clear()
357
362
358 if keep_input:
363 if keep_input:
359 input_buffer = self.input_buffer
364 input_buffer = self.input_buffer
360 self._show_prompt()
365 self._show_prompt()
361 if keep_input:
366 if keep_input:
362 self.input_buffer = input_buffer
367 self.input_buffer = input_buffer
363
368
364 def paste(self):
369 def paste(self):
365 """ Reimplemented to ensure that text is pasted in the editing region.
370 """ Reimplemented to ensure that text is pasted in the editing region.
366 """
371 """
367 self._keep_cursor_in_buffer()
372 self._keep_cursor_in_buffer()
368 QtGui.QPlainTextEdit.paste(self)
373 QtGui.QPlainTextEdit.paste(self)
369
374
370 def print_(self, printer):
375 def print_(self, printer):
371 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
376 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
372 slot has the wrong signature.
377 slot has the wrong signature.
373 """
378 """
374 QtGui.QPlainTextEdit.print_(self, printer)
379 QtGui.QPlainTextEdit.print_(self, printer)
375
380
376 #---------------------------------------------------------------------------
381 #---------------------------------------------------------------------------
377 # 'ConsoleWidget' public interface
382 # 'ConsoleWidget' public interface
378 #---------------------------------------------------------------------------
383 #---------------------------------------------------------------------------
379
384
380 def execute(self, interactive=False):
385 def execute(self, interactive=False):
381 """ Execute the text in the input buffer. Returns whether the input
386 """ Execute the text in the input buffer. Returns whether the input
382 buffer was completely processed and a new prompt created.
387 buffer was completely processed and a new prompt created.
383 """
388 """
384 self.appendPlainText('\n')
389 self.appendPlainText('\n')
385 self._executing_input_buffer = self.input_buffer
390 self._executing_input_buffer = self.input_buffer
386 self._executing = True
391 self._executing = True
387 self._prompt_finished()
392 self._prompt_finished()
388 return self._execute(interactive=interactive)
393 return self._execute(interactive=interactive)
389
394
390 def _get_input_buffer(self):
395 def _get_input_buffer(self):
391 """ The text that the user has entered entered at the current prompt.
396 """ The text that the user has entered entered at the current prompt.
392 """
397 """
393 # If we're executing, the input buffer may not even exist anymore due to
398 # If we're executing, the input buffer may not even exist anymore due to
394 # the limit imposed by 'buffer_size'. Therefore, we store it.
399 # the limit imposed by 'buffer_size'. Therefore, we store it.
395 if self._executing:
400 if self._executing:
396 return self._executing_input_buffer
401 return self._executing_input_buffer
397
402
398 cursor = self._get_end_cursor()
403 cursor = self._get_end_cursor()
399 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
404 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
400
405
401 # Use QTextDocumentFragment intermediate object because it strips
406 # Use QTextDocumentFragment intermediate object because it strips
402 # out the Unicode line break characters that Qt insists on inserting.
407 # out the Unicode line break characters that Qt insists on inserting.
403 input_buffer = str(cursor.selection().toPlainText())
408 input_buffer = str(cursor.selection().toPlainText())
404
409
405 # Strip out continuation prompts.
410 # Strip out continuation prompts.
406 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
411 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
407
412
408 def _set_input_buffer(self, string):
413 def _set_input_buffer(self, string):
409 """ Replaces the text in the input buffer with 'string'.
414 """ Replaces the text in the input buffer with 'string'.
410 """
415 """
411 # Add continuation prompts where necessary.
416 # Add continuation prompts where necessary.
412 lines = string.splitlines()
417 lines = string.splitlines()
413 for i in xrange(1, len(lines)):
418 for i in xrange(1, len(lines)):
414 lines[i] = self._continuation_prompt + lines[i]
419 lines[i] = self._continuation_prompt + lines[i]
415 string = '\n'.join(lines)
420 string = '\n'.join(lines)
416
421
417 # Replace buffer with new text.
422 # Replace buffer with new text.
418 cursor = self._get_end_cursor()
423 cursor = self._get_end_cursor()
419 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
424 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
420 cursor.insertText(string)
425 cursor.insertText(string)
421 self.moveCursor(QtGui.QTextCursor.End)
426 self.moveCursor(QtGui.QTextCursor.End)
422
427
423 input_buffer = property(_get_input_buffer, _set_input_buffer)
428 input_buffer = property(_get_input_buffer, _set_input_buffer)
424
429
425 def _get_input_buffer_cursor_line(self):
430 def _get_input_buffer_cursor_line(self):
426 """ The text in the line of the input buffer in which the user's cursor
431 """ The text in the line of the input buffer in which the user's cursor
427 rests. Returns a string if there is such a line; otherwise, None.
432 rests. Returns a string if there is such a line; otherwise, None.
428 """
433 """
429 if self._executing:
434 if self._executing:
430 return None
435 return None
431 cursor = self.textCursor()
436 cursor = self.textCursor()
432 if cursor.position() >= self._prompt_pos:
437 if cursor.position() >= self._prompt_pos:
433 text = str(cursor.block().text())
438 text = str(cursor.block().text())
434 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
439 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
435 return text[len(self._prompt):]
440 return text[len(self._prompt):]
436 else:
441 else:
437 return text[len(self._continuation_prompt):]
442 return text[len(self._continuation_prompt):]
438 else:
443 else:
439 return None
444 return None
440
445
441 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
446 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
442
447
443 def _get_font(self):
448 def _get_font(self):
444 """ The base font being used by the ConsoleWidget.
449 """ The base font being used by the ConsoleWidget.
445 """
450 """
446 return self.document().defaultFont()
451 return self.document().defaultFont()
447
452
448 def _set_font(self, font):
453 def _set_font(self, font):
449 """ Sets the base font for the ConsoleWidget to the specified QFont.
454 """ Sets the base font for the ConsoleWidget to the specified QFont.
450 """
455 """
451 self._completion_widget.setFont(font)
456 self._completion_widget.setFont(font)
452 self.document().setDefaultFont(font)
457 self.document().setDefaultFont(font)
453
458
454 font = property(_get_font, _set_font)
459 font = property(_get_font, _set_font)
455
460
456 def reset_font(self):
461 def reset_font(self):
457 """ Sets the font to the default fixed-width font for this platform.
462 """ Sets the font to the default fixed-width font for this platform.
458 """
463 """
459 if sys.platform == 'win32':
464 if sys.platform == 'win32':
460 name = 'Courier'
465 name = 'Courier'
461 elif sys.platform == 'darwin':
466 elif sys.platform == 'darwin':
462 name = 'Monaco'
467 name = 'Monaco'
463 else:
468 else:
464 name = 'Monospace'
469 name = 'Monospace'
465 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
470 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
466 font.setStyleHint(QtGui.QFont.TypeWriter)
471 font.setStyleHint(QtGui.QFont.TypeWriter)
467 self._set_font(font)
472 self._set_font(font)
468
473
469 #---------------------------------------------------------------------------
474 #---------------------------------------------------------------------------
470 # 'ConsoleWidget' abstract interface
475 # 'ConsoleWidget' abstract interface
471 #---------------------------------------------------------------------------
476 #---------------------------------------------------------------------------
472
477
473 def _execute(self, interactive):
478 def _execute(self, interactive):
474 """ Called to execute the input buffer. When triggered by an the enter
479 """ Called to execute the input buffer. When triggered by an the enter
475 key press, 'interactive' is True; otherwise, it is False. Returns
480 key press, 'interactive' is True; otherwise, it is False. Returns
476 whether the input buffer was completely processed and a new prompt
481 whether the input buffer was completely processed and a new prompt
477 created.
482 created.
478 """
483 """
479 raise NotImplementedError
484 raise NotImplementedError
480
485
481 def _prompt_started_hook(self):
486 def _prompt_started_hook(self):
482 """ Called immediately after a new prompt is displayed.
487 """ Called immediately after a new prompt is displayed.
483 """
488 """
484 pass
489 pass
485
490
486 def _prompt_finished_hook(self):
491 def _prompt_finished_hook(self):
487 """ Called immediately after a prompt is finished, i.e. when some input
492 """ Called immediately after a prompt is finished, i.e. when some input
488 will be processed and a new prompt displayed.
493 will be processed and a new prompt displayed.
489 """
494 """
490 pass
495 pass
491
496
492 def _up_pressed(self):
497 def _up_pressed(self):
493 """ Called when the up key is pressed. Returns whether to continue
498 """ Called when the up key is pressed. Returns whether to continue
494 processing the event.
499 processing the event.
495 """
500 """
496 return True
501 return True
497
502
498 def _down_pressed(self):
503 def _down_pressed(self):
499 """ Called when the down key is pressed. Returns whether to continue
504 """ Called when the down key is pressed. Returns whether to continue
500 processing the event.
505 processing the event.
501 """
506 """
502 return True
507 return True
503
508
504 def _tab_pressed(self):
509 def _tab_pressed(self):
505 """ Called when the tab key is pressed. Returns whether to continue
510 """ Called when the tab key is pressed. Returns whether to continue
506 processing the event.
511 processing the event.
507 """
512 """
508 return False
513 return False
509
514
510 #--------------------------------------------------------------------------
515 #--------------------------------------------------------------------------
511 # 'ConsoleWidget' protected interface
516 # 'ConsoleWidget' protected interface
512 #--------------------------------------------------------------------------
517 #--------------------------------------------------------------------------
513
518
514 def _control_down(self, modifiers):
519 def _control_down(self, modifiers):
515 """ Given a KeyboardModifiers flags object, return whether the Control
520 """ Given a KeyboardModifiers flags object, return whether the Control
516 key is down (on Mac OS, treat the Command key as a synonym for
521 key is down (on Mac OS, treat the Command key as a synonym for
517 Control).
522 Control).
518 """
523 """
519 down = bool(modifiers & QtCore.Qt.ControlModifier)
524 down = bool(modifiers & QtCore.Qt.ControlModifier)
520
525
521 # Note: on Mac OS, ControlModifier corresponds to the Command key while
526 # Note: on Mac OS, ControlModifier corresponds to the Command key while
522 # MetaModifier corresponds to the Control key.
527 # MetaModifier corresponds to the Control key.
523 if sys.platform == 'darwin':
528 if sys.platform == 'darwin':
524 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
529 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
525
530
526 return down
531 return down
527
532
528 def _complete_with_items(self, cursor, items):
533 def _complete_with_items(self, cursor, items):
529 """ Performs completion with 'items' at the specified cursor location.
534 """ Performs completion with 'items' at the specified cursor location.
530 """
535 """
531 if len(items) == 1:
536 if len(items) == 1:
532 cursor.setPosition(self.textCursor().position(),
537 cursor.setPosition(self.textCursor().position(),
533 QtGui.QTextCursor.KeepAnchor)
538 QtGui.QTextCursor.KeepAnchor)
534 cursor.insertText(items[0])
539 cursor.insertText(items[0])
535 elif len(items) > 1:
540 elif len(items) > 1:
536 if self.gui_completion:
541 if self.gui_completion:
537 self._completion_widget.show_items(cursor, items)
542 self._completion_widget.show_items(cursor, items)
538 else:
543 else:
539 text = '\n'.join(items) + '\n'
544 text = '\n'.join(items) + '\n'
540 self._write_text_keeping_prompt(text)
545 self._write_text_keeping_prompt(text)
541
546
542 def _get_end_cursor(self):
547 def _get_end_cursor(self):
543 """ Convenience method that returns a cursor for the last character.
548 """ Convenience method that returns a cursor for the last character.
544 """
549 """
545 cursor = self.textCursor()
550 cursor = self.textCursor()
546 cursor.movePosition(QtGui.QTextCursor.End)
551 cursor.movePosition(QtGui.QTextCursor.End)
547 return cursor
552 return cursor
548
553
549 def _get_prompt_cursor(self):
554 def _get_prompt_cursor(self):
550 """ Convenience method that returns a cursor for the prompt position.
555 """ Convenience method that returns a cursor for the prompt position.
551 """
556 """
552 cursor = self.textCursor()
557 cursor = self.textCursor()
553 cursor.setPosition(self._prompt_pos)
558 cursor.setPosition(self._prompt_pos)
554 return cursor
559 return cursor
555
560
556 def _get_selection_cursor(self, start, end):
561 def _get_selection_cursor(self, start, end):
557 """ Convenience method that returns a cursor with text selected between
562 """ Convenience method that returns a cursor with text selected between
558 the positions 'start' and 'end'.
563 the positions 'start' and 'end'.
559 """
564 """
560 cursor = self.textCursor()
565 cursor = self.textCursor()
561 cursor.setPosition(start)
566 cursor.setPosition(start)
562 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
567 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
563 return cursor
568 return cursor
564
569
565 def _get_word_start_cursor(self, position):
570 def _get_word_start_cursor(self, position):
566 """ Find the start of the word to the left the given position. If a
571 """ Find the start of the word to the left the given position. If a
567 sequence of non-word characters precedes the first word, skip over
572 sequence of non-word characters precedes the first word, skip over
568 them. (This emulates the behavior of bash, emacs, etc.)
573 them. (This emulates the behavior of bash, emacs, etc.)
569 """
574 """
570 document = self.document()
575 document = self.document()
571 position -= 1
576 position -= 1
572 while self._in_buffer(position) and \
577 while self._in_buffer(position) and \
573 not document.characterAt(position).isLetterOrNumber():
578 not document.characterAt(position).isLetterOrNumber():
574 position -= 1
579 position -= 1
575 while self._in_buffer(position) and \
580 while self._in_buffer(position) and \
576 document.characterAt(position).isLetterOrNumber():
581 document.characterAt(position).isLetterOrNumber():
577 position -= 1
582 position -= 1
578 cursor = self.textCursor()
583 cursor = self.textCursor()
579 cursor.setPosition(position + 1)
584 cursor.setPosition(position + 1)
580 return cursor
585 return cursor
581
586
582 def _get_word_end_cursor(self, position):
587 def _get_word_end_cursor(self, position):
583 """ Find the end of the word to the right the given position. If a
588 """ Find the end of the word to the right the given position. If a
584 sequence of non-word characters precedes the first word, skip over
589 sequence of non-word characters precedes the first word, skip over
585 them. (This emulates the behavior of bash, emacs, etc.)
590 them. (This emulates the behavior of bash, emacs, etc.)
586 """
591 """
587 document = self.document()
592 document = self.document()
588 end = self._get_end_cursor().position()
593 end = self._get_end_cursor().position()
589 while position < end and \
594 while position < end and \
590 not document.characterAt(position).isLetterOrNumber():
595 not document.characterAt(position).isLetterOrNumber():
591 position += 1
596 position += 1
592 while position < end and \
597 while position < end and \
593 document.characterAt(position).isLetterOrNumber():
598 document.characterAt(position).isLetterOrNumber():
594 position += 1
599 position += 1
595 cursor = self.textCursor()
600 cursor = self.textCursor()
596 cursor.setPosition(position)
601 cursor.setPosition(position)
597 return cursor
602 return cursor
598
603
599 def _prompt_started(self):
604 def _prompt_started(self):
600 """ Called immediately after a new prompt is displayed.
605 """ Called immediately after a new prompt is displayed.
601 """
606 """
602 # Temporarily disable the maximum block count to permit undo/redo.
607 # Temporarily disable the maximum block count to permit undo/redo.
603 self.setMaximumBlockCount(0)
608 self.setMaximumBlockCount(0)
604 self.setUndoRedoEnabled(True)
609 self.setUndoRedoEnabled(True)
605
610
606 self.setReadOnly(False)
611 self.setReadOnly(False)
607 self.moveCursor(QtGui.QTextCursor.End)
612 self.moveCursor(QtGui.QTextCursor.End)
608 self.centerCursor()
613 self.centerCursor()
609
614
610 self._executing = False
615 self._executing = False
611 self._prompt_started_hook()
616 self._prompt_started_hook()
612
617
613 def _prompt_finished(self):
618 def _prompt_finished(self):
614 """ Called immediately after a prompt is finished, i.e. when some input
619 """ Called immediately after a prompt is finished, i.e. when some input
615 will be processed and a new prompt displayed.
620 will be processed and a new prompt displayed.
616 """
621 """
617 # This has the (desired) side effect of disabling the undo/redo history.
622 # This has the (desired) side effect of disabling the undo/redo history.
618 self.setMaximumBlockCount(self.buffer_size)
623 self.setMaximumBlockCount(self.buffer_size)
619
624
620 self.setReadOnly(True)
625 self.setReadOnly(True)
621 self._prompt_finished_hook()
626 self._prompt_finished_hook()
622
627
623 def _set_position(self, position):
628 def _set_position(self, position):
624 """ Convenience method to set the position of the cursor.
629 """ Convenience method to set the position of the cursor.
625 """
630 """
626 cursor = self.textCursor()
631 cursor = self.textCursor()
627 cursor.setPosition(position)
632 cursor.setPosition(position)
628 self.setTextCursor(cursor)
633 self.setTextCursor(cursor)
629
634
630 def _set_selection(self, start, end):
635 def _set_selection(self, start, end):
631 """ Convenience method to set the current selected text.
636 """ Convenience method to set the current selected text.
632 """
637 """
633 self.setTextCursor(self._get_selection_cursor(start, end))
638 self.setTextCursor(self._get_selection_cursor(start, end))
634
639
635 def _show_prompt(self, prompt=None):
640 def _show_prompt(self, prompt=None):
636 """ Writes a new prompt at the end of the buffer. If 'prompt' is not
641 """ Writes a new prompt at the end of the buffer. If 'prompt' is not
637 specified, uses the previous prompt.
642 specified, uses the previous prompt.
638 """
643 """
639 if prompt is not None:
644 if prompt is not None:
640 self._prompt = prompt
645 self._prompt = prompt
641 self.appendPlainText('\n' + self._prompt)
646 self.appendPlainText('\n' + self._prompt)
642 self._prompt_pos = self._get_end_cursor().position()
647 self._prompt_pos = self._get_end_cursor().position()
643 self._prompt_started()
648 self._prompt_started()
644
649
645 def _show_continuation_prompt(self):
650 def _show_continuation_prompt(self):
646 """ Writes a new continuation prompt at the end of the buffer.
651 """ Writes a new continuation prompt at the end of the buffer.
647 """
652 """
648 self.appendPlainText(self._continuation_prompt)
653 self.appendPlainText(self._continuation_prompt)
649 self._prompt_started()
654 self._prompt_started()
650
655
651 def _write_text_keeping_prompt(self, text):
656 def _write_text_keeping_prompt(self, text):
652 """ Writes 'text' after the current prompt, then restores the old prompt
657 """ Writes 'text' after the current prompt, then restores the old prompt
653 with its old input buffer.
658 with its old input buffer.
654 """
659 """
655 input_buffer = self.input_buffer
660 input_buffer = self.input_buffer
656 self.appendPlainText('\n')
661 self.appendPlainText('\n')
657 self._prompt_finished()
662 self._prompt_finished()
658
663
659 self.appendPlainText(text)
664 self.appendPlainText(text)
660 self._show_prompt()
665 self._show_prompt()
661 self.input_buffer = input_buffer
666 self.input_buffer = input_buffer
662
667
663 def _in_buffer(self, position):
668 def _in_buffer(self, position):
664 """ Returns whether the given position is inside the editing region.
669 """ Returns whether the given position is inside the editing region.
665 """
670 """
666 return position >= self._prompt_pos
671 return position >= self._prompt_pos
667
672
668 def _keep_cursor_in_buffer(self):
673 def _keep_cursor_in_buffer(self):
669 """ Ensures that the cursor is inside the editing region. Returns
674 """ Ensures that the cursor is inside the editing region. Returns
670 whether the cursor was moved.
675 whether the cursor was moved.
671 """
676 """
672 cursor = self.textCursor()
677 cursor = self.textCursor()
673 if cursor.position() < self._prompt_pos:
678 if cursor.position() < self._prompt_pos:
674 cursor.movePosition(QtGui.QTextCursor.End)
679 cursor.movePosition(QtGui.QTextCursor.End)
675 self.setTextCursor(cursor)
680 self.setTextCursor(cursor)
676 return True
681 return True
677 else:
682 else:
678 return False
683 return False
679
684
680
685
681 class HistoryConsoleWidget(ConsoleWidget):
686 class HistoryConsoleWidget(ConsoleWidget):
682 """ A ConsoleWidget that keeps a history of the commands that have been
687 """ A ConsoleWidget that keeps a history of the commands that have been
683 executed.
688 executed.
684 """
689 """
685
690
686 #---------------------------------------------------------------------------
691 #---------------------------------------------------------------------------
687 # 'QObject' interface
692 # 'QObject' interface
688 #---------------------------------------------------------------------------
693 #---------------------------------------------------------------------------
689
694
690 def __init__(self, parent=None):
695 def __init__(self, parent=None):
691 super(HistoryConsoleWidget, self).__init__(parent)
696 super(HistoryConsoleWidget, self).__init__(parent)
692
697
693 self._history = []
698 self._history = []
694 self._history_index = 0
699 self._history_index = 0
695
700
696 #---------------------------------------------------------------------------
701 #---------------------------------------------------------------------------
697 # 'ConsoleWidget' public interface
702 # 'ConsoleWidget' public interface
698 #---------------------------------------------------------------------------
703 #---------------------------------------------------------------------------
699
704
700 def execute(self, interactive=False):
705 def execute(self, interactive=False):
701 """ Reimplemented to the store history.
706 """ Reimplemented to the store history.
702 """
707 """
703 stripped = self.input_buffer.rstrip()
708 stripped = self.input_buffer.rstrip()
704 executed = super(HistoryConsoleWidget, self).execute(interactive)
709 executed = super(HistoryConsoleWidget, self).execute(interactive)
705 if executed:
710 if executed:
706 self._history.append(stripped)
711 self._history.append(stripped)
707 self._history_index = len(self._history)
712 self._history_index = len(self._history)
708 return executed
713 return executed
709
714
710 #---------------------------------------------------------------------------
715 #---------------------------------------------------------------------------
711 # 'ConsoleWidget' abstract interface
716 # 'ConsoleWidget' abstract interface
712 #---------------------------------------------------------------------------
717 #---------------------------------------------------------------------------
713
718
714 def _up_pressed(self):
719 def _up_pressed(self):
715 """ Called when the up key is pressed. Returns whether to continue
720 """ Called when the up key is pressed. Returns whether to continue
716 processing the event.
721 processing the event.
717 """
722 """
718 prompt_cursor = self._get_prompt_cursor()
723 prompt_cursor = self._get_prompt_cursor()
719 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
724 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
720 self.history_previous()
725 self.history_previous()
721
726
722 # Go to the first line of prompt for seemless history scrolling.
727 # Go to the first line of prompt for seemless history scrolling.
723 cursor = self._get_prompt_cursor()
728 cursor = self._get_prompt_cursor()
724 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
729 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
725 self.setTextCursor(cursor)
730 self.setTextCursor(cursor)
726
731
727 return False
732 return False
728 return True
733 return True
729
734
730 def _down_pressed(self):
735 def _down_pressed(self):
731 """ Called when the down key is pressed. Returns whether to continue
736 """ Called when the down key is pressed. Returns whether to continue
732 processing the event.
737 processing the event.
733 """
738 """
734 end_cursor = self._get_end_cursor()
739 end_cursor = self._get_end_cursor()
735 if self.textCursor().blockNumber() == end_cursor.blockNumber():
740 if self.textCursor().blockNumber() == end_cursor.blockNumber():
736 self.history_next()
741 self.history_next()
737 return False
742 return False
738 return True
743 return True
739
744
740 #---------------------------------------------------------------------------
745 #---------------------------------------------------------------------------
741 # 'HistoryConsoleWidget' interface
746 # 'HistoryConsoleWidget' interface
742 #---------------------------------------------------------------------------
747 #---------------------------------------------------------------------------
743
748
744 def history_previous(self):
749 def history_previous(self):
745 """ If possible, set the input buffer to the previous item in the
750 """ If possible, set the input buffer to the previous item in the
746 history.
751 history.
747 """
752 """
748 if self._history_index > 0:
753 if self._history_index > 0:
749 self._history_index -= 1
754 self._history_index -= 1
750 self.input_buffer = self._history[self._history_index]
755 self.input_buffer = self._history[self._history_index]
751
756
752 def history_next(self):
757 def history_next(self):
753 """ Set the input buffer to the next item in the history, or a blank
758 """ Set the input buffer to the next item in the history, or a blank
754 line if there is no subsequent item.
759 line if there is no subsequent item.
755 """
760 """
756 if self._history_index < len(self._history):
761 if self._history_index < len(self._history):
757 self._history_index += 1
762 self._history_index += 1
758 if self._history_index < len(self._history):
763 if self._history_index < len(self._history):
759 self.input_buffer = self._history[self._history_index]
764 self.input_buffer = self._history[self._history_index]
760 else:
765 else:
761 self.input_buffer = ''
766 self.input_buffer = ''
@@ -1,306 +1,326 b''
1 # Standard library imports
2 import signal
3
1 # System library imports
4 # System library imports
2 from pygments.lexers import PythonLexer
5 from pygments.lexers import PythonLexer
3 from PyQt4 import QtCore, QtGui
6 from PyQt4 import QtCore, QtGui
4 import zmq
7 import zmq
5
8
6 # Local imports
9 # Local imports
7 from IPython.core.inputsplitter import InputSplitter
10 from IPython.core.inputsplitter import InputSplitter
8 from call_tip_widget import CallTipWidget
11 from call_tip_widget import CallTipWidget
9 from completion_lexer import CompletionLexer
12 from completion_lexer import CompletionLexer
10 from console_widget import HistoryConsoleWidget
13 from console_widget import HistoryConsoleWidget
11 from pygments_highlighter import PygmentsHighlighter
14 from pygments_highlighter import PygmentsHighlighter
12
15
13
16
14 class FrontendHighlighter(PygmentsHighlighter):
17 class FrontendHighlighter(PygmentsHighlighter):
15 """ A Python PygmentsHighlighter that can be turned on and off and which
18 """ A Python PygmentsHighlighter that can be turned on and off and which
16 knows about continuation prompts.
19 knows about continuation prompts.
17 """
20 """
18
21
19 def __init__(self, frontend):
22 def __init__(self, frontend):
20 PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
23 PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
21 self._current_offset = 0
24 self._current_offset = 0
22 self._frontend = frontend
25 self._frontend = frontend
23 self.highlighting_on = False
26 self.highlighting_on = False
24
27
25 def highlightBlock(self, qstring):
28 def highlightBlock(self, qstring):
26 """ Highlight a block of text. Reimplemented to highlight selectively.
29 """ Highlight a block of text. Reimplemented to highlight selectively.
27 """
30 """
28 if self.highlighting_on:
31 if self.highlighting_on:
29 for prompt in (self._frontend._continuation_prompt,
32 for prompt in (self._frontend._continuation_prompt,
30 self._frontend._prompt):
33 self._frontend._prompt):
31 if qstring.startsWith(prompt):
34 if qstring.startsWith(prompt):
32 qstring.remove(0, len(prompt))
35 qstring.remove(0, len(prompt))
33 self._current_offset = len(prompt)
36 self._current_offset = len(prompt)
34 break
37 break
35 PygmentsHighlighter.highlightBlock(self, qstring)
38 PygmentsHighlighter.highlightBlock(self, qstring)
36
39
37 def setFormat(self, start, count, format):
40 def setFormat(self, start, count, format):
38 """ Reimplemented to avoid highlighting continuation prompts.
41 """ Reimplemented to avoid highlighting continuation prompts.
39 """
42 """
40 start += self._current_offset
43 start += self._current_offset
41 PygmentsHighlighter.setFormat(self, start, count, format)
44 PygmentsHighlighter.setFormat(self, start, count, format)
42
45
43
46
44 class FrontendWidget(HistoryConsoleWidget):
47 class FrontendWidget(HistoryConsoleWidget):
45 """ A Qt frontend for a generic Python kernel.
48 """ A Qt frontend for a generic Python kernel.
46 """
49 """
47
50
48 # Emitted when an 'execute_reply' is received from the kernel.
51 # Emitted when an 'execute_reply' is received from the kernel.
49 executed = QtCore.pyqtSignal(object)
52 executed = QtCore.pyqtSignal(object)
50
53
51 #---------------------------------------------------------------------------
54 #---------------------------------------------------------------------------
52 # 'QObject' interface
55 # 'QObject' interface
53 #---------------------------------------------------------------------------
56 #---------------------------------------------------------------------------
54
57
55 def __init__(self, parent=None):
58 def __init__(self, parent=None):
56 super(FrontendWidget, self).__init__(parent)
59 super(FrontendWidget, self).__init__(parent)
57
60
58 # ConsoleWidget protected variables.
61 # ConsoleWidget protected variables.
59 self._continuation_prompt = '... '
62 self._continuation_prompt = '... '
60 self._prompt = '>>> '
63 self._prompt = '>>> '
61
64
62 # FrontendWidget protected variables.
65 # FrontendWidget protected variables.
63 self._call_tip_widget = CallTipWidget(self)
66 self._call_tip_widget = CallTipWidget(self)
64 self._completion_lexer = CompletionLexer(PythonLexer())
67 self._completion_lexer = CompletionLexer(PythonLexer())
65 self._hidden = True
68 self._hidden = True
66 self._highlighter = FrontendHighlighter(self)
69 self._highlighter = FrontendHighlighter(self)
67 self._input_splitter = InputSplitter(input_mode='replace')
70 self._input_splitter = InputSplitter(input_mode='replace')
68 self._kernel_manager = None
71 self._kernel_manager = None
69
72
70 self.document().contentsChange.connect(self._document_contents_change)
73 self.document().contentsChange.connect(self._document_contents_change)
71
74
72 #---------------------------------------------------------------------------
75 #---------------------------------------------------------------------------
73 # 'QWidget' interface
76 # 'QWidget' interface
74 #---------------------------------------------------------------------------
77 #---------------------------------------------------------------------------
75
78
76 def focusOutEvent(self, event):
79 def focusOutEvent(self, event):
77 """ Reimplemented to hide calltips.
80 """ Reimplemented to hide calltips.
78 """
81 """
79 self._call_tip_widget.hide()
82 self._call_tip_widget.hide()
80 return super(FrontendWidget, self).focusOutEvent(event)
83 super(FrontendWidget, self).focusOutEvent(event)
81
84
82 def keyPressEvent(self, event):
85 def keyPressEvent(self, event):
83 """ Reimplemented to hide calltips.
86 """ Reimplemented to allow calltips to process events and to send
87 signals to the kernel.
84 """
88 """
85 if event.key() == QtCore.Qt.Key_Escape:
89 if self._executing and event.key() == QtCore.Qt.Key_C and \
86 self._call_tip_widget.hide()
90 self._control_down(event.modifiers()):
87 return super(FrontendWidget, self).keyPressEvent(event)
91 self._interrupt_kernel()
92 else:
93 self._call_tip_widget.keyPressEvent(event)
94 super(FrontendWidget, self).keyPressEvent(event)
88
95
89 #---------------------------------------------------------------------------
96 #---------------------------------------------------------------------------
90 # 'ConsoleWidget' abstract interface
97 # 'ConsoleWidget' abstract interface
91 #---------------------------------------------------------------------------
98 #---------------------------------------------------------------------------
92
99
93 def _execute(self, interactive):
100 def _execute(self, interactive):
94 """ Called to execute the input buffer. When triggered by an the enter
101 """ Called to execute the input buffer. When triggered by an the enter
95 key press, 'interactive' is True; otherwise, it is False. Returns
102 key press, 'interactive' is True; otherwise, it is False. Returns
96 whether the input buffer was completely processed and a new prompt
103 whether the input buffer was completely processed and a new prompt
97 created.
104 created.
98 """
105 """
99 return self.execute_source(self.input_buffer, interactive=interactive)
106 return self.execute_source(self.input_buffer, interactive=interactive)
100
107
101 def _prompt_started_hook(self):
108 def _prompt_started_hook(self):
102 """ Called immediately after a new prompt is displayed.
109 """ Called immediately after a new prompt is displayed.
103 """
110 """
104 self._highlighter.highlighting_on = True
111 self._highlighter.highlighting_on = True
105
112
106 def _prompt_finished_hook(self):
113 def _prompt_finished_hook(self):
107 """ Called immediately after a prompt is finished, i.e. when some input
114 """ Called immediately after a prompt is finished, i.e. when some input
108 will be processed and a new prompt displayed.
115 will be processed and a new prompt displayed.
109 """
116 """
110 self._highlighter.highlighting_on = False
117 self._highlighter.highlighting_on = False
111
118
112 def _tab_pressed(self):
119 def _tab_pressed(self):
113 """ Called when the tab key is pressed. Returns whether to continue
120 """ Called when the tab key is pressed. Returns whether to continue
114 processing the event.
121 processing the event.
115 """
122 """
116 self._keep_cursor_in_buffer()
123 self._keep_cursor_in_buffer()
117 cursor = self.textCursor()
124 cursor = self.textCursor()
118 if not self._complete():
125 if not self._complete():
119 cursor.insertText(' ')
126 cursor.insertText(' ')
120 return False
127 return False
121
128
122 #---------------------------------------------------------------------------
129 #---------------------------------------------------------------------------
123 # 'FrontendWidget' interface
130 # 'FrontendWidget' interface
124 #---------------------------------------------------------------------------
131 #---------------------------------------------------------------------------
125
132
126 def execute_source(self, source, hidden=False, interactive=False):
133 def execute_source(self, source, hidden=False, interactive=False):
127 """ Execute a string containing Python code. If 'hidden', no output is
134 """ Execute a string containing Python code. If 'hidden', no output is
128 shown. Returns whether the source executed (i.e., returns True only
135 shown. Returns whether the source executed (i.e., returns True only
129 if no more input is necessary).
136 if no more input is necessary).
130 """
137 """
131 self._input_splitter.push(source)
138 self._input_splitter.push(source)
132 executed = not self._input_splitter.push_accepts_more()
139 executed = not self._input_splitter.push_accepts_more()
133 if executed:
140 if executed:
134 self.kernel_manager.xreq_channel.execute(source)
141 self.kernel_manager.xreq_channel.execute(source)
135 self._hidden = hidden
142 self._hidden = hidden
136 else:
143 else:
137 self._show_continuation_prompt()
144 self._show_continuation_prompt()
138 self.appendPlainText(' ' * self._input_splitter.indent_spaces)
145 self.appendPlainText(' ' * self._input_splitter.indent_spaces)
139 return executed
146 return executed
140
147
141 def execute_file(self, path, hidden=False):
148 def execute_file(self, path, hidden=False):
142 """ Attempts to execute file with 'path'. If 'hidden', no output is
149 """ Attempts to execute file with 'path'. If 'hidden', no output is
143 shown.
150 shown.
144 """
151 """
145 self.execute_source('run %s' % path, hidden=hidden)
152 self.execute_source('run %s' % path, hidden=hidden)
146
153
147 def _get_kernel_manager(self):
154 def _get_kernel_manager(self):
148 """ Returns the current kernel manager.
155 """ Returns the current kernel manager.
149 """
156 """
150 return self._kernel_manager
157 return self._kernel_manager
151
158
152 def _set_kernel_manager(self, kernel_manager):
159 def _set_kernel_manager(self, kernel_manager):
153 """ Disconnect from the current kernel manager (if any) and set a new
160 """ Disconnect from the current kernel manager (if any) and set a new
154 kernel manager.
161 kernel manager.
155 """
162 """
156 # Disconnect the old kernel manager, if necessary.
163 # Disconnect the old kernel manager, if necessary.
157 if self._kernel_manager is not None:
164 if self._kernel_manager is not None:
158 self._kernel_manager.started_listening.disconnect(
165 self._kernel_manager.started_listening.disconnect(
159 self._started_listening)
166 self._started_listening)
160 self._kernel_manager.stopped_listening.disconnect(
167 self._kernel_manager.stopped_listening.disconnect(
161 self._stopped_listening)
168 self._stopped_listening)
162
169
163 # Disconnect the old kernel manager's channels.
170 # Disconnect the old kernel manager's channels.
164 sub = self._kernel_manager.sub_channel
171 sub = self._kernel_manager.sub_channel
165 xreq = self._kernel_manager.xreq_channel
172 xreq = self._kernel_manager.xreq_channel
166 sub.message_received.disconnect(self._handle_sub)
173 sub.message_received.disconnect(self._handle_sub)
167 xreq.execute_reply.disconnect(self._handle_execute_reply)
174 xreq.execute_reply.disconnect(self._handle_execute_reply)
168 xreq.complete_reply.disconnect(self._handle_complete_reply)
175 xreq.complete_reply.disconnect(self._handle_complete_reply)
169 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
176 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
170
177
178 # Handle the case where the old kernel manager is still listening.
179 if self._kernel_manager.is_listening:
180 self._stopped_listening()
181
171 # Set the new kernel manager.
182 # Set the new kernel manager.
172 self._kernel_manager = kernel_manager
183 self._kernel_manager = kernel_manager
173 if kernel_manager is None:
184 if kernel_manager is None:
174 return
185 return
175
186
176 # Connect the new kernel manager.
187 # Connect the new kernel manager.
177 kernel_manager.started_listening.connect(self._started_listening)
188 kernel_manager.started_listening.connect(self._started_listening)
178 kernel_manager.stopped_listening.connect(self._stopped_listening)
189 kernel_manager.stopped_listening.connect(self._stopped_listening)
179
190
180 # Connect the new kernel manager's channels.
191 # Connect the new kernel manager's channels.
181 sub = kernel_manager.sub_channel
192 sub = kernel_manager.sub_channel
182 xreq = kernel_manager.xreq_channel
193 xreq = kernel_manager.xreq_channel
183 sub.message_received.connect(self._handle_sub)
194 sub.message_received.connect(self._handle_sub)
184 xreq.execute_reply.connect(self._handle_execute_reply)
195 xreq.execute_reply.connect(self._handle_execute_reply)
185 xreq.complete_reply.connect(self._handle_complete_reply)
196 xreq.complete_reply.connect(self._handle_complete_reply)
186 xreq.object_info_reply.connect(self._handle_object_info_reply)
197 xreq.object_info_reply.connect(self._handle_object_info_reply)
187
198
188 # Handle the case where the kernel manager started listening before
199 # Handle the case where the kernel manager started listening before
189 # we connected.
200 # we connected.
190 if kernel_manager.is_listening:
201 if kernel_manager.is_listening:
191 self._started_listening()
202 self._started_listening()
192
203
193 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
204 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
194
205
195 #---------------------------------------------------------------------------
206 #---------------------------------------------------------------------------
196 # 'FrontendWidget' protected interface
207 # 'FrontendWidget' protected interface
197 #---------------------------------------------------------------------------
208 #---------------------------------------------------------------------------
198
209
199 def _call_tip(self):
210 def _call_tip(self):
200 """ Shows a call tip, if appropriate, at the current cursor location.
211 """ Shows a call tip, if appropriate, at the current cursor location.
201 """
212 """
202 # Decide if it makes sense to show a call tip
213 # Decide if it makes sense to show a call tip
203 cursor = self.textCursor()
214 cursor = self.textCursor()
204 cursor.movePosition(QtGui.QTextCursor.Left)
215 cursor.movePosition(QtGui.QTextCursor.Left)
205 document = self.document()
216 document = self.document()
206 if document.characterAt(cursor.position()).toAscii() != '(':
217 if document.characterAt(cursor.position()).toAscii() != '(':
207 return False
218 return False
208 context = self._get_context(cursor)
219 context = self._get_context(cursor)
209 if not context:
220 if not context:
210 return False
221 return False
211
222
212 # Send the metadata request to the kernel
223 # Send the metadata request to the kernel
213 name = '.'.join(context)
224 name = '.'.join(context)
214 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
225 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
215 self._calltip_pos = self.textCursor().position()
226 self._calltip_pos = self.textCursor().position()
216 return True
227 return True
217
228
218 def _complete(self):
229 def _complete(self):
219 """ Performs completion at the current cursor location.
230 """ Performs completion at the current cursor location.
220 """
231 """
221 # Decide if it makes sense to do completion
232 # Decide if it makes sense to do completion
222 context = self._get_context()
233 context = self._get_context()
223 if not context:
234 if not context:
224 return False
235 return False
225
236
226 # Send the completion request to the kernel
237 # Send the completion request to the kernel
227 text = '.'.join(context)
238 text = '.'.join(context)
228 self._complete_id = self.kernel_manager.xreq_channel.complete(
239 self._complete_id = self.kernel_manager.xreq_channel.complete(
229 text, self.input_buffer_cursor_line, self.input_buffer)
240 text, self.input_buffer_cursor_line, self.input_buffer)
230 self._complete_pos = self.textCursor().position()
241 self._complete_pos = self.textCursor().position()
231 return True
242 return True
232
243
233 def _get_context(self, cursor=None):
244 def _get_context(self, cursor=None):
234 """ Gets the context at the current cursor location.
245 """ Gets the context at the current cursor location.
235 """
246 """
236 if cursor is None:
247 if cursor is None:
237 cursor = self.textCursor()
248 cursor = self.textCursor()
238 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
249 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
239 QtGui.QTextCursor.KeepAnchor)
250 QtGui.QTextCursor.KeepAnchor)
240 text = unicode(cursor.selectedText())
251 text = unicode(cursor.selectedText())
241 return self._completion_lexer.get_context(text)
252 return self._completion_lexer.get_context(text)
242
253
254 def _interrupt_kernel(self):
255 """ Attempts to the interrupt the kernel.
256 """
257 if self.kernel_manager.has_kernel:
258 self.kernel_manager.signal_kernel(signal.SIGINT)
259 else:
260 self.appendPlainText('Kernel process is either remote or '
261 'unspecified. Cannot interrupt.\n')
262
243 #------ Signal handlers ----------------------------------------------------
263 #------ Signal handlers ----------------------------------------------------
244
264
245 def _document_contents_change(self, position, removed, added):
265 def _document_contents_change(self, position, removed, added):
246 """ Called whenever the document's content changes. Display a calltip
266 """ Called whenever the document's content changes. Display a calltip
247 if appropriate.
267 if appropriate.
248 """
268 """
249 # Calculate where the cursor should be *after* the change:
269 # Calculate where the cursor should be *after* the change:
250 position += added
270 position += added
251
271
252 document = self.document()
272 document = self.document()
253 if position == self.textCursor().position():
273 if position == self.textCursor().position():
254 self._call_tip()
274 self._call_tip()
255
275
256 def _handle_sub(self, omsg):
276 def _handle_sub(self, omsg):
257 if not self._hidden:
277 if not self._hidden:
258 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
278 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
259 if handler is not None:
279 if handler is not None:
260 handler(omsg)
280 handler(omsg)
261
281
262 def _handle_pyout(self, omsg):
282 def _handle_pyout(self, omsg):
263 session = omsg['parent_header']['session']
283 session = omsg['parent_header']['session']
264 if session == self.kernel_manager.session.session:
284 if session == self.kernel_manager.session.session:
265 self.appendPlainText(omsg['content']['data'] + '\n')
285 self.appendPlainText(omsg['content']['data'] + '\n')
266
286
267 def _handle_stream(self, omsg):
287 def _handle_stream(self, omsg):
268 self.appendPlainText(omsg['content']['data'])
288 self.appendPlainText(omsg['content']['data'])
269
289
270 def _handle_execute_reply(self, rep):
290 def _handle_execute_reply(self, rep):
271 # Make sure that all output from the SUB channel has been processed
291 # Make sure that all output from the SUB channel has been processed
272 # before writing a new prompt.
292 # before writing a new prompt.
273 self.kernel_manager.sub_channel.flush()
293 self.kernel_manager.sub_channel.flush()
274
294
275 content = rep['content']
295 content = rep['content']
276 status = content['status']
296 status = content['status']
277 if status == 'error':
297 if status == 'error':
278 self.appendPlainText(content['traceback'][-1])
298 self.appendPlainText(content['traceback'][-1])
279 elif status == 'aborted':
299 elif status == 'aborted':
280 text = "ERROR: ABORTED\n"
300 text = "ERROR: ABORTED\n"
281 self.appendPlainText(text)
301 self.appendPlainText(text)
282 self._hidden = True
302 self._hidden = True
283 self._show_prompt()
303 self._show_prompt()
284 self.executed.emit(rep)
304 self.executed.emit(rep)
285
305
286 def _handle_complete_reply(self, rep):
306 def _handle_complete_reply(self, rep):
287 cursor = self.textCursor()
307 cursor = self.textCursor()
288 if rep['parent_header']['msg_id'] == self._complete_id and \
308 if rep['parent_header']['msg_id'] == self._complete_id and \
289 cursor.position() == self._complete_pos:
309 cursor.position() == self._complete_pos:
290 text = '.'.join(self._get_context())
310 text = '.'.join(self._get_context())
291 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
311 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
292 self._complete_with_items(cursor, rep['content']['matches'])
312 self._complete_with_items(cursor, rep['content']['matches'])
293
313
294 def _handle_object_info_reply(self, rep):
314 def _handle_object_info_reply(self, rep):
295 cursor = self.textCursor()
315 cursor = self.textCursor()
296 if rep['parent_header']['msg_id'] == self._calltip_id and \
316 if rep['parent_header']['msg_id'] == self._calltip_id and \
297 cursor.position() == self._calltip_pos:
317 cursor.position() == self._calltip_pos:
298 doc = rep['content']['docstring']
318 doc = rep['content']['docstring']
299 if doc:
319 if doc:
300 self._call_tip_widget.show_docstring(doc)
320 self._call_tip_widget.show_docstring(doc)
301
321
302 def _started_listening(self):
322 def _started_listening(self):
303 self.clear()
323 self.clear()
304
324
305 def _stopped_listening(self):
325 def _stopped_listening(self):
306 pass
326 pass
General Comments 0
You need to be logged in to leave comments. Login now