##// END OF EJS Templates
Fixed the segfaults on application exit. The BracketMatcher, CallTipWidget, and CompletionWidget were using the text control as their parents. This should not be a problem, but for some reason it resulted in problems during shutdown. I suspect that PyQt is bugged and was deleting the C++ objects a second time in the garbage collection phase after they had already been deleted automatically by the C++ layer of Qt.
epatters -
Show More
@@ -1,101 +1,101 b''
1 """ Provides bracket matching for Q[Plain]TextEdit widgets.
1 """ Provides bracket matching for Q[Plain]TextEdit widgets.
2 """
2 """
3
3
4 # System library imports
4 # System library imports
5 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
6
6
7
7
8 class BracketMatcher(QtCore.QObject):
8 class BracketMatcher(QtCore.QObject):
9 """ Matches square brackets, braces, and parentheses based on cursor
9 """ Matches square brackets, braces, and parentheses based on cursor
10 position.
10 position.
11 """
11 """
12
12
13 # Protected class variables.
13 # Protected class variables.
14 _opening_map = { '(':')', '{':'}', '[':']' }
14 _opening_map = { '(':')', '{':'}', '[':']' }
15 _closing_map = { ')':'(', '}':'{', ']':'[' }
15 _closing_map = { ')':'(', '}':'{', ']':'[' }
16
16
17 #--------------------------------------------------------------------------
17 #--------------------------------------------------------------------------
18 # 'QObject' interface
18 # 'QObject' interface
19 #--------------------------------------------------------------------------
19 #--------------------------------------------------------------------------
20
20
21 def __init__(self, parent):
21 def __init__(self, text_edit):
22 """ Create a call tip manager that is attached to the specified Qt
22 """ Create a call tip manager that is attached to the specified Qt
23 text edit widget.
23 text edit widget.
24 """
24 """
25 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
25 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
26 QtCore.QObject.__init__(self, parent)
26 super(BracketMatcher, self).__init__()
27
27
28 # The format to apply to matching brackets.
28 # The format to apply to matching brackets.
29 self.format = QtGui.QTextCharFormat()
29 self.format = QtGui.QTextCharFormat()
30 self.format.setBackground(QtGui.QColor('silver'))
30 self.format.setBackground(QtGui.QColor('silver'))
31
31
32 parent.cursorPositionChanged.connect(self._cursor_position_changed)
32 self._text_edit = text_edit
33 text_edit.cursorPositionChanged.connect(self._cursor_position_changed)
33
34
34 #--------------------------------------------------------------------------
35 #--------------------------------------------------------------------------
35 # Protected interface
36 # Protected interface
36 #--------------------------------------------------------------------------
37 #--------------------------------------------------------------------------
37
38
38 def _find_match(self, position):
39 def _find_match(self, position):
39 """ Given a valid position in the text document, try to find the
40 """ Given a valid position in the text document, try to find the
40 position of the matching bracket. Returns -1 if unsuccessful.
41 position of the matching bracket. Returns -1 if unsuccessful.
41 """
42 """
42 # Decide what character to search for and what direction to search in.
43 # Decide what character to search for and what direction to search in.
43 document = self.parent().document()
44 document = self._text_edit.document()
44 qchar = document.characterAt(position)
45 qchar = document.characterAt(position)
45 start_char = qchar.toAscii()
46 start_char = qchar.toAscii()
46 search_char = self._opening_map.get(start_char)
47 search_char = self._opening_map.get(start_char)
47 if search_char:
48 if search_char:
48 increment = 1
49 increment = 1
49 else:
50 else:
50 search_char = self._closing_map.get(start_char)
51 search_char = self._closing_map.get(start_char)
51 if search_char:
52 if search_char:
52 increment = -1
53 increment = -1
53 else:
54 else:
54 return -1
55 return -1
55
56
56 # Search for the character.
57 # Search for the character.
57 depth = 0
58 depth = 0
58 while position >= 0 and position < document.characterCount():
59 while position >= 0 and position < document.characterCount():
59 char = qchar.toAscii()
60 char = qchar.toAscii()
60 if char == start_char:
61 if char == start_char:
61 depth += 1
62 depth += 1
62 elif char == search_char:
63 elif char == search_char:
63 depth -= 1
64 depth -= 1
64 if depth == 0:
65 if depth == 0:
65 break
66 break
66 position += increment
67 position += increment
67 qchar = document.characterAt(position)
68 qchar = document.characterAt(position)
68 else:
69 else:
69 position = -1
70 position = -1
70 return position
71 return position
71
72
72 def _selection_for_character(self, position):
73 def _selection_for_character(self, position):
73 """ Convenience method for selecting a character.
74 """ Convenience method for selecting a character.
74 """
75 """
75 selection = QtGui.QTextEdit.ExtraSelection()
76 selection = QtGui.QTextEdit.ExtraSelection()
76 cursor = self.parent().textCursor()
77 cursor = self._text_edit.textCursor()
77 cursor.setPosition(position)
78 cursor.setPosition(position)
78 cursor.movePosition(QtGui.QTextCursor.NextCharacter,
79 cursor.movePosition(QtGui.QTextCursor.NextCharacter,
79 QtGui.QTextCursor.KeepAnchor)
80 QtGui.QTextCursor.KeepAnchor)
80 selection.cursor = cursor
81 selection.cursor = cursor
81 selection.format = self.format
82 selection.format = self.format
82 return selection
83 return selection
83
84
84 #------ Signal handlers ----------------------------------------------------
85 #------ Signal handlers ----------------------------------------------------
85
86
86 def _cursor_position_changed(self):
87 def _cursor_position_changed(self):
87 """ Updates the document formatting based on the new cursor position.
88 """ Updates the document formatting based on the new cursor position.
88 """
89 """
89 # Clear out the old formatting.
90 # Clear out the old formatting.
90 text_edit = self.parent()
91 self._text_edit.setExtraSelections([])
91 text_edit.setExtraSelections([])
92
92
93 # Attempt to match a bracket for the new cursor position.
93 # Attempt to match a bracket for the new cursor position.
94 cursor = text_edit.textCursor()
94 cursor = self._text_edit.textCursor()
95 if not cursor.hasSelection():
95 if not cursor.hasSelection():
96 position = cursor.position() - 1
96 position = cursor.position() - 1
97 match_position = self._find_match(position)
97 match_position = self._find_match(position)
98 if match_position != -1:
98 if match_position != -1:
99 extra_selections = [ self._selection_for_character(pos)
99 extra_selections = [ self._selection_for_character(pos)
100 for pos in (position, match_position) ]
100 for pos in (position, match_position) ]
101 text_edit.setExtraSelections(extra_selections)
101 self._text_edit.setExtraSelections(extra_selections)
@@ -1,214 +1,215 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, text_edit):
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(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
22 QtGui.QLabel.__init__(self, parent, QtCore.Qt.ToolTip)
22 super(CallTipWidget, self).__init__(None, QtCore.Qt.ToolTip)
23
23
24 self._hide_timer = QtCore.QBasicTimer()
24 self._hide_timer = QtCore.QBasicTimer()
25 self._text_edit = text_edit
25
26
26 self.setFont(parent.document().defaultFont())
27 self.setFont(text_edit.document().defaultFont())
27 self.setForegroundRole(QtGui.QPalette.ToolTipText)
28 self.setForegroundRole(QtGui.QPalette.ToolTipText)
28 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
29 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
29 self.setPalette(QtGui.QToolTip.palette())
30 self.setPalette(QtGui.QToolTip.palette())
30
31
31 self.setAlignment(QtCore.Qt.AlignLeft)
32 self.setAlignment(QtCore.Qt.AlignLeft)
32 self.setIndent(1)
33 self.setIndent(1)
33 self.setFrameStyle(QtGui.QFrame.NoFrame)
34 self.setFrameStyle(QtGui.QFrame.NoFrame)
34 self.setMargin(1 + self.style().pixelMetric(
35 self.setMargin(1 + self.style().pixelMetric(
35 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
36 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
36 self.setWindowOpacity(self.style().styleHint(
37 self.setWindowOpacity(self.style().styleHint(
37 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
38 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
38
39
39 def eventFilter(self, obj, event):
40 def eventFilter(self, obj, event):
40 """ Reimplemented to hide on certain key presses and on parent focus
41 """ Reimplemented to hide on certain key presses and on text edit focus
41 changes.
42 changes.
42 """
43 """
43 if obj == self.parent():
44 if obj == self._text_edit:
44 etype = event.type()
45 etype = event.type()
45
46
46 if etype == QtCore.QEvent.KeyPress:
47 if etype == QtCore.QEvent.KeyPress:
47 key = event.key()
48 key = event.key()
48 if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
49 if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
49 self.hide()
50 self.hide()
50 elif key == QtCore.Qt.Key_Escape:
51 elif key == QtCore.Qt.Key_Escape:
51 self.hide()
52 self.hide()
52 return True
53 return True
53
54
54 elif etype == QtCore.QEvent.FocusOut:
55 elif etype == QtCore.QEvent.FocusOut:
55 self.hide()
56 self.hide()
56
57
57 elif etype == QtCore.QEvent.Enter:
58 elif etype == QtCore.QEvent.Enter:
58 self._hide_timer.stop()
59 self._hide_timer.stop()
59
60
60 elif etype == QtCore.QEvent.Leave:
61 elif etype == QtCore.QEvent.Leave:
61 self._hide_later()
62 self._hide_later()
62
63
63 return QtGui.QLabel.eventFilter(self, obj, event)
64 return super(CallTipWidget, self).eventFilter(obj, event)
64
65
65 def timerEvent(self, event):
66 def timerEvent(self, event):
66 """ Reimplemented to hide the widget when the hide timer fires.
67 """ Reimplemented to hide the widget when the hide timer fires.
67 """
68 """
68 if event.timerId() == self._hide_timer.timerId():
69 if event.timerId() == self._hide_timer.timerId():
69 self._hide_timer.stop()
70 self._hide_timer.stop()
70 self.hide()
71 self.hide()
71
72
72 #--------------------------------------------------------------------------
73 #--------------------------------------------------------------------------
73 # 'QWidget' interface
74 # 'QWidget' interface
74 #--------------------------------------------------------------------------
75 #--------------------------------------------------------------------------
75
76
76 def enterEvent(self, event):
77 def enterEvent(self, event):
77 """ Reimplemented to cancel the hide timer.
78 """ Reimplemented to cancel the hide timer.
78 """
79 """
79 QtGui.QLabel.enterEvent(self, event)
80 super(CallTipWidget, self).enterEvent(event)
80 self._hide_timer.stop()
81 self._hide_timer.stop()
81
82
82 def hideEvent(self, event):
83 def hideEvent(self, event):
83 """ Reimplemented to disconnect signal handlers and event filter.
84 """ Reimplemented to disconnect signal handlers and event filter.
84 """
85 """
85 QtGui.QLabel.hideEvent(self, event)
86 super(CallTipWidget, self).hideEvent(event)
86 parent = self.parent()
87 self._text_edit.cursorPositionChanged.disconnect(
87 parent.cursorPositionChanged.disconnect(self._cursor_position_changed)
88 self._cursor_position_changed)
88 parent.removeEventFilter(self)
89 self._text_edit.removeEventFilter(self)
89
90
90 def leaveEvent(self, event):
91 def leaveEvent(self, event):
91 """ Reimplemented to start the hide timer.
92 """ Reimplemented to start the hide timer.
92 """
93 """
93 QtGui.QLabel.leaveEvent(self, event)
94 super(CallTipWidget, self).leaveEvent(event)
94 self._hide_later()
95 self._hide_later()
95
96
96 def paintEvent(self, event):
97 def paintEvent(self, event):
97 """ Reimplemented to paint the background panel.
98 """ Reimplemented to paint the background panel.
98 """
99 """
99 painter = QtGui.QStylePainter(self)
100 painter = QtGui.QStylePainter(self)
100 option = QtGui.QStyleOptionFrame()
101 option = QtGui.QStyleOptionFrame()
101 option.init(self)
102 option.init(self)
102 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
103 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
103 painter.end()
104 painter.end()
104
105
105 QtGui.QLabel.paintEvent(self, event)
106 super(CallTipWidget, self).paintEvent(event)
106
107
107 def showEvent(self, event):
108 def showEvent(self, event):
108 """ Reimplemented to connect signal handlers and event filter.
109 """ Reimplemented to connect signal handlers and event filter.
109 """
110 """
110 QtGui.QLabel.showEvent(self, event)
111 super(CallTipWidget, self).showEvent(event)
111 parent = self.parent()
112 self._text_edit.cursorPositionChanged.connect(
112 parent.cursorPositionChanged.connect(self._cursor_position_changed)
113 self._cursor_position_changed)
113 parent.installEventFilter(self)
114 self._text_edit.installEventFilter(self)
114
115
115 #--------------------------------------------------------------------------
116 #--------------------------------------------------------------------------
116 # 'CallTipWidget' interface
117 # 'CallTipWidget' interface
117 #--------------------------------------------------------------------------
118 #--------------------------------------------------------------------------
118
119
119 def show_docstring(self, doc, maxlines=20):
120 def show_docstring(self, doc, maxlines=20):
120 """ Attempts to show the specified docstring at the current cursor
121 """ Attempts to show the specified docstring at the current cursor
121 location. The docstring is dedented and possibly truncated for
122 location. The docstring is dedented and possibly truncated for
122 length.
123 length.
123 """
124 """
124 doc = dedent(doc.rstrip()).lstrip()
125 doc = dedent(doc.rstrip()).lstrip()
125 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
126 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
126 if match:
127 if match:
127 doc = doc[:match.end()] + '\n[Documentation continues...]'
128 doc = doc[:match.end()] + '\n[Documentation continues...]'
128 return self.show_tip(doc)
129 return self.show_tip(doc)
129
130
130 def show_tip(self, tip):
131 def show_tip(self, tip):
131 """ Attempts to show the specified tip at the current cursor location.
132 """ Attempts to show the specified tip at the current cursor location.
132 """
133 """
133 # Attempt to find the cursor position at which to show the call tip.
134 # Attempt to find the cursor position at which to show the call tip.
134 text_edit = self.parent()
135 text_edit = self._text_edit
135 document = text_edit.document()
136 document = text_edit.document()
136 cursor = text_edit.textCursor()
137 cursor = text_edit.textCursor()
137 search_pos = cursor.position() - 1
138 search_pos = cursor.position() - 1
138 self._start_position, _ = self._find_parenthesis(search_pos,
139 self._start_position, _ = self._find_parenthesis(search_pos,
139 forward=False)
140 forward=False)
140 if self._start_position == -1:
141 if self._start_position == -1:
141 return False
142 return False
142
143
143 # Set the text and resize the widget accordingly.
144 # Set the text and resize the widget accordingly.
144 self.setText(tip)
145 self.setText(tip)
145 self.resize(self.sizeHint())
146 self.resize(self.sizeHint())
146
147
147 # Locate and show the widget. Place the tip below the current line
148 # Locate and show the widget. Place the tip below the current line
148 # unless it would be off the screen. In that case, place it above
149 # unless it would be off the screen. In that case, place it above
149 # the current line.
150 # the current line.
150 padding = 3 # Distance in pixels between cursor bounds and tip box.
151 padding = 3 # Distance in pixels between cursor bounds and tip box.
151 cursor_rect = text_edit.cursorRect(cursor)
152 cursor_rect = text_edit.cursorRect(cursor)
152 screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit)
153 screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit)
153 point = text_edit.mapToGlobal(cursor_rect.bottomRight())
154 point = text_edit.mapToGlobal(cursor_rect.bottomRight())
154 point.setY(point.y() + padding)
155 point.setY(point.y() + padding)
155 tip_height = self.size().height()
156 tip_height = self.size().height()
156 if point.y() + tip_height > screen_rect.height():
157 if point.y() + tip_height > screen_rect.height():
157 point = text_edit.mapToGlobal(cursor_rect.topRight())
158 point = text_edit.mapToGlobal(cursor_rect.topRight())
158 point.setY(point.y() - tip_height - padding)
159 point.setY(point.y() - tip_height - padding)
159 self.move(point)
160 self.move(point)
160 self.show()
161 self.show()
161 return True
162 return True
162
163
163 #--------------------------------------------------------------------------
164 #--------------------------------------------------------------------------
164 # Protected interface
165 # Protected interface
165 #--------------------------------------------------------------------------
166 #--------------------------------------------------------------------------
166
167
167 def _find_parenthesis(self, position, forward=True):
168 def _find_parenthesis(self, position, forward=True):
168 """ If 'forward' is True (resp. False), proceed forwards
169 """ If 'forward' is True (resp. False), proceed forwards
169 (resp. backwards) through the line that contains 'position' until an
170 (resp. backwards) through the line that contains 'position' until an
170 unmatched closing (resp. opening) parenthesis is found. Returns a
171 unmatched closing (resp. opening) parenthesis is found. Returns a
171 tuple containing the position of this parenthesis (or -1 if it is
172 tuple containing the position of this parenthesis (or -1 if it is
172 not found) and the number commas (at depth 0) found along the way.
173 not found) and the number commas (at depth 0) found along the way.
173 """
174 """
174 commas = depth = 0
175 commas = depth = 0
175 document = self.parent().document()
176 document = self._text_edit.document()
176 qchar = document.characterAt(position)
177 qchar = document.characterAt(position)
177 while (position > 0 and qchar.isPrint() and
178 while (position > 0 and qchar.isPrint() and
178 # Need to check explicitly for line/paragraph separators:
179 # Need to check explicitly for line/paragraph separators:
179 qchar.unicode() not in (0x2028, 0x2029)):
180 qchar.unicode() not in (0x2028, 0x2029)):
180 char = qchar.toAscii()
181 char = qchar.toAscii()
181 if char == ',' and depth == 0:
182 if char == ',' and depth == 0:
182 commas += 1
183 commas += 1
183 elif char == ')':
184 elif char == ')':
184 if forward and depth == 0:
185 if forward and depth == 0:
185 break
186 break
186 depth += 1
187 depth += 1
187 elif char == '(':
188 elif char == '(':
188 if not forward and depth == 0:
189 if not forward and depth == 0:
189 break
190 break
190 depth -= 1
191 depth -= 1
191 position += 1 if forward else -1
192 position += 1 if forward else -1
192 qchar = document.characterAt(position)
193 qchar = document.characterAt(position)
193 else:
194 else:
194 position = -1
195 position = -1
195 return position, commas
196 return position, commas
196
197
197 def _hide_later(self):
198 def _hide_later(self):
198 """ Hides the tooltip after some time has passed.
199 """ Hides the tooltip after some time has passed.
199 """
200 """
200 if not self._hide_timer.isActive():
201 if not self._hide_timer.isActive():
201 self._hide_timer.start(300, self)
202 self._hide_timer.start(300, self)
202
203
203 #------ Signal handlers ----------------------------------------------------
204 #------ Signal handlers ----------------------------------------------------
204
205
205 def _cursor_position_changed(self):
206 def _cursor_position_changed(self):
206 """ Updates the tip based on user cursor movement.
207 """ Updates the tip based on user cursor movement.
207 """
208 """
208 cursor = self.parent().textCursor()
209 cursor = self._text_edit.textCursor()
209 if cursor.position() <= self._start_position:
210 if cursor.position() <= self._start_position:
210 self.hide()
211 self.hide()
211 else:
212 else:
212 position, commas = self._find_parenthesis(self._start_position + 1)
213 position, commas = self._find_parenthesis(self._start_position + 1)
213 if position != -1:
214 if position != -1:
214 self.hide()
215 self.hide()
@@ -1,136 +1,133 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 # 'QObject' interface
10 # 'QObject' interface
11 #--------------------------------------------------------------------------
11 #--------------------------------------------------------------------------
12
12
13 def __init__(self, parent):
13 def __init__(self, text_edit):
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(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 QtGui.QListWidget.__init__(self, parent)
18 super(CompletionWidget, self).__init__()
19
20 self._text_edit = text_edit
19
21
20 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
21 self.setAttribute(QtCore.Qt.WA_StaticContents)
22 self.setAttribute(QtCore.Qt.WA_StaticContents)
23 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
22
24
23 # Ensure that parent keeps focus when widget is displayed.
25 # Ensure that the text edit keeps focus when widget is displayed.
24 self.setFocusProxy(parent)
26 self.setFocusProxy(self._text_edit)
25
27
26 self.setFrameShadow(QtGui.QFrame.Plain)
28 self.setFrameShadow(QtGui.QFrame.Plain)
27 self.setFrameShape(QtGui.QFrame.StyledPanel)
29 self.setFrameShape(QtGui.QFrame.StyledPanel)
28
30
29 self.itemActivated.connect(self._complete_current)
31 self.itemActivated.connect(self._complete_current)
30
32
31 def eventFilter(self, obj, event):
33 def eventFilter(self, obj, event):
32 """ Reimplemented to handle keyboard input and to auto-hide when our
34 """ Reimplemented to handle keyboard input and to auto-hide when the
33 parent loses focus.
35 text edit loses focus.
34 """
36 """
35 if obj == self.parent():
37 if obj == self._text_edit:
36 etype = event.type()
38 etype = event.type()
37
39
38 if etype == QtCore.QEvent.KeyPress:
40 if etype == QtCore.QEvent.KeyPress:
39 key, text = event.key(), event.text()
41 key, text = event.key(), event.text()
40 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
42 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
41 QtCore.Qt.Key_Tab):
43 QtCore.Qt.Key_Tab):
42 self._complete_current()
44 self._complete_current()
43 return True
45 return True
44 elif key == QtCore.Qt.Key_Escape:
46 elif key == QtCore.Qt.Key_Escape:
45 self.hide()
47 self.hide()
46 return True
48 return True
47 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
49 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
48 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
50 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
49 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
51 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
50 QtGui.QListWidget.keyPressEvent(self, event)
52 self.keyPressEvent(event)
51 return True
53 return True
52
54
53 elif etype == QtCore.QEvent.FocusOut:
55 elif etype == QtCore.QEvent.FocusOut:
54 self.hide()
56 self.hide()
55
57
56 return QtGui.QListWidget.eventFilter(self, obj, event)
58 return super(CompletionWidget, self).eventFilter(obj, event)
57
59
58 #--------------------------------------------------------------------------
60 #--------------------------------------------------------------------------
59 # 'QWidget' interface
61 # 'QWidget' interface
60 #--------------------------------------------------------------------------
62 #--------------------------------------------------------------------------
61
63
62 def hideEvent(self, event):
64 def hideEvent(self, event):
63 """ Reimplemented to disconnect signal handlers and event filter.
65 """ Reimplemented to disconnect signal handlers and event filter.
64 """
66 """
65 QtGui.QListWidget.hideEvent(self, event)
67 super(CompletionWidget, self).hideEvent(event)
66 parent = self.parent()
68 self._text_edit.cursorPositionChanged.disconnect(self._update_current)
67 try:
69 self._text_edit.removeEventFilter(self)
68 parent.cursorPositionChanged.disconnect(self._update_current)
69 except TypeError:
70 pass
71 parent.removeEventFilter(self)
72
70
73 def showEvent(self, event):
71 def showEvent(self, event):
74 """ Reimplemented to connect signal handlers and event filter.
72 """ Reimplemented to connect signal handlers and event filter.
75 """
73 """
76 QtGui.QListWidget.showEvent(self, event)
74 super(CompletionWidget, self).showEvent(event)
77 parent = self.parent()
75 self._text_edit.cursorPositionChanged.connect(self._update_current)
78 parent.cursorPositionChanged.connect(self._update_current)
76 self._text_edit.installEventFilter(self)
79 parent.installEventFilter(self)
80
77
81 #--------------------------------------------------------------------------
78 #--------------------------------------------------------------------------
82 # 'CompletionWidget' interface
79 # 'CompletionWidget' interface
83 #--------------------------------------------------------------------------
80 #--------------------------------------------------------------------------
84
81
85 def show_items(self, cursor, items):
82 def show_items(self, cursor, items):
86 """ Shows the completion widget with 'items' at the position specified
83 """ Shows the completion widget with 'items' at the position specified
87 by 'cursor'.
84 by 'cursor'.
88 """
85 """
89 text_edit = self.parent()
86 text_edit = self._text_edit
90 point = text_edit.cursorRect(cursor).bottomRight()
87 point = text_edit.cursorRect(cursor).bottomRight()
91 point = text_edit.mapToGlobal(point)
88 point = text_edit.mapToGlobal(point)
92 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
89 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
93 if screen_rect.size().height() - point.y() - self.height() < 0:
90 if screen_rect.size().height() - point.y() - self.height() < 0:
94 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
91 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
95 point.setY(point.y() - self.height())
92 point.setY(point.y() - self.height())
96 self.move(point)
93 self.move(point)
97
94
98 self._start_position = cursor.position()
95 self._start_position = cursor.position()
99 self.clear()
96 self.clear()
100 self.addItems(items)
97 self.addItems(items)
101 self.setCurrentRow(0)
98 self.setCurrentRow(0)
102 self.show()
99 self.show()
103
100
104 #--------------------------------------------------------------------------
101 #--------------------------------------------------------------------------
105 # Protected interface
102 # Protected interface
106 #--------------------------------------------------------------------------
103 #--------------------------------------------------------------------------
107
104
108 def _complete_current(self):
105 def _complete_current(self):
109 """ Perform the completion with the currently selected item.
106 """ Perform the completion with the currently selected item.
110 """
107 """
111 self._current_text_cursor().insertText(self.currentItem().text())
108 self._current_text_cursor().insertText(self.currentItem().text())
112 self.hide()
109 self.hide()
113
110
114 def _current_text_cursor(self):
111 def _current_text_cursor(self):
115 """ Returns a cursor with text between the start position and the
112 """ Returns a cursor with text between the start position and the
116 current position selected.
113 current position selected.
117 """
114 """
118 cursor = self.parent().textCursor()
115 cursor = self._text_edit.textCursor()
119 if cursor.position() >= self._start_position:
116 if cursor.position() >= self._start_position:
120 cursor.setPosition(self._start_position,
117 cursor.setPosition(self._start_position,
121 QtGui.QTextCursor.KeepAnchor)
118 QtGui.QTextCursor.KeepAnchor)
122 return cursor
119 return cursor
123
120
124 def _update_current(self):
121 def _update_current(self):
125 """ Updates the current item based on the current text.
122 """ Updates the current item based on the current text.
126 """
123 """
127 prefix = self._current_text_cursor().selection().toPlainText()
124 prefix = self._current_text_cursor().selection().toPlainText()
128 if prefix:
125 if prefix:
129 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
126 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
130 QtCore.Qt.MatchCaseSensitive))
127 QtCore.Qt.MatchCaseSensitive))
131 if items:
128 if items:
132 self.setCurrentItem(items[0])
129 self.setCurrentItem(items[0])
133 else:
130 else:
134 self.hide()
131 self.hide()
135 else:
132 else:
136 self.hide()
133 self.hide()
@@ -1,143 +1,147 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2 """
2 """
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Imports
5 # Imports
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7
7
8 # Systemm library imports
8 # Systemm library imports
9 from PyQt4 import QtGui
9 from PyQt4 import QtGui
10
10
11 # Local imports
11 # Local imports
12 from IPython.external.argparse import ArgumentParser
12 from IPython.external.argparse import ArgumentParser
13 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
13 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
14 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
14 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
15 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
15 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
16 from IPython.frontend.qt.kernelmanager import QtKernelManager
16 from IPython.frontend.qt.kernelmanager import QtKernelManager
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Constants
19 # Constants
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 LOCALHOST = '127.0.0.1'
22 LOCALHOST = '127.0.0.1'
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Classes
25 # Classes
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27
27
28 class MainWindow(QtGui.QMainWindow):
28 class MainWindow(QtGui.QMainWindow):
29
29
30 #---------------------------------------------------------------------------
30 #---------------------------------------------------------------------------
31 # 'object' interface
31 # 'object' interface
32 #---------------------------------------------------------------------------
32 #---------------------------------------------------------------------------
33
33
34 def __init__(self, frontend):
34 def __init__(self, frontend):
35 """ Create a MainWindow for the specified FrontendWidget.
35 """ Create a MainWindow for the specified FrontendWidget.
36 """
36 """
37 super(MainWindow, self).__init__()
37 super(MainWindow, self).__init__()
38 self._frontend = frontend
38 self._frontend = frontend
39 self._frontend.exit_requested.connect(self.close)
39 self._frontend.exit_requested.connect(self.close)
40 self.setCentralWidget(frontend)
40 self.setCentralWidget(frontend)
41
41
42 #---------------------------------------------------------------------------
42 #---------------------------------------------------------------------------
43 # QWidget interface
43 # QWidget interface
44 #---------------------------------------------------------------------------
44 #---------------------------------------------------------------------------
45
45
46 def closeEvent(self, event):
46 def closeEvent(self, event):
47 """ Reimplemented to prompt the user and close the kernel cleanly.
47 """ Reimplemented to prompt the user and close the kernel cleanly.
48 """
48 """
49 reply = QtGui.QMessageBox.question(self, self.window().windowTitle(),
49 kernel_manager = self._frontend.kernel_manager
50 'Close console?', QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
50 if kernel_manager and kernel_manager.channels_running:
51 if reply == QtGui.QMessageBox.Yes:
51 title = self.window().windowTitle()
52 self._frontend.kernel_manager.shutdown_kernel()
52 reply = QtGui.QMessageBox.question(self, title,
53 event.accept()
53 'Close console?', QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
54 else:
54 if reply == QtGui.QMessageBox.Yes:
55 event.ignore()
55 kernel_manager.shutdown_kernel()
56 #kernel_manager.stop_channels()
57 event.accept()
58 else:
59 event.ignore()
56
60
57 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
58 # Main entry point
62 # Main entry point
59 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
60
64
61 def main():
65 def main():
62 """ Entry point for application.
66 """ Entry point for application.
63 """
67 """
64 # Parse command line arguments.
68 # Parse command line arguments.
65 parser = ArgumentParser()
69 parser = ArgumentParser()
66 kgroup = parser.add_argument_group('kernel options')
70 kgroup = parser.add_argument_group('kernel options')
67 kgroup.add_argument('-e', '--existing', action='store_true',
71 kgroup.add_argument('-e', '--existing', action='store_true',
68 help='connect to an existing kernel')
72 help='connect to an existing kernel')
69 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
73 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
70 help='set the kernel\'s IP address [default localhost]')
74 help='set the kernel\'s IP address [default localhost]')
71 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
75 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
72 help='set the XREQ channel port [default random]')
76 help='set the XREQ channel port [default random]')
73 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
77 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
74 help='set the SUB channel port [default random]')
78 help='set the SUB channel port [default random]')
75 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
79 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
76 help='set the REP channel port [default random]')
80 help='set the REP channel port [default random]')
77 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
81 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
78 help='set the heartbeat port [default: random]')
82 help='set the heartbeat port [default: random]')
79
83
80 egroup = kgroup.add_mutually_exclusive_group()
84 egroup = kgroup.add_mutually_exclusive_group()
81 egroup.add_argument('--pure', action='store_true', help = \
85 egroup.add_argument('--pure', action='store_true', help = \
82 'use a pure Python kernel instead of an IPython kernel')
86 'use a pure Python kernel instead of an IPython kernel')
83 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
87 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
84 const='auto', help = \
88 const='auto', help = \
85 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
89 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
86 given, the GUI backend is matplotlib's, otherwise use one of: \
90 given, the GUI backend is matplotlib's, otherwise use one of: \
87 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
91 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
88
92
89 wgroup = parser.add_argument_group('widget options')
93 wgroup = parser.add_argument_group('widget options')
90 wgroup.add_argument('--paging', type=str, default='inside',
94 wgroup.add_argument('--paging', type=str, default='inside',
91 choices = ['inside', 'hsplit', 'vsplit', 'none'],
95 choices = ['inside', 'hsplit', 'vsplit', 'none'],
92 help='set the paging style [default inside]')
96 help='set the paging style [default inside]')
93 wgroup.add_argument('--rich', action='store_true',
97 wgroup.add_argument('--rich', action='store_true',
94 help='enable rich text support')
98 help='enable rich text support')
95 wgroup.add_argument('--gui-completion', action='store_true',
99 wgroup.add_argument('--gui-completion', action='store_true',
96 help='use a GUI widget for tab completion')
100 help='use a GUI widget for tab completion')
97
101
98 args = parser.parse_args()
102 args = parser.parse_args()
99
103
100 # Don't let Qt or ZMQ swallow KeyboardInterupts.
104 # Don't let Qt or ZMQ swallow KeyboardInterupts.
101 import signal
105 import signal
102 signal.signal(signal.SIGINT, signal.SIG_DFL)
106 signal.signal(signal.SIGINT, signal.SIG_DFL)
103
107
104 # Create a KernelManager and start a kernel.
108 # Create a KernelManager and start a kernel.
105 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
109 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
106 sub_address=(args.ip, args.sub),
110 sub_address=(args.ip, args.sub),
107 rep_address=(args.ip, args.rep),
111 rep_address=(args.ip, args.rep),
108 hb_address=(args.ip, args.hb))
112 hb_address=(args.ip, args.hb))
109 if args.ip == LOCALHOST and not args.existing:
113 if args.ip == LOCALHOST and not args.existing:
110 if args.pure:
114 if args.pure:
111 kernel_manager.start_kernel(ipython=False)
115 kernel_manager.start_kernel(ipython=False)
112 elif args.pylab:
116 elif args.pylab:
113 if args.rich:
117 if args.rich:
114 kernel_manager.start_kernel(pylab='payload-svg')
118 kernel_manager.start_kernel(pylab='payload-svg')
115 else:
119 else:
116 kernel_manager.start_kernel(pylab=args.pylab)
120 kernel_manager.start_kernel(pylab=args.pylab)
117 else:
121 else:
118 kernel_manager.start_kernel()
122 kernel_manager.start_kernel()
119 kernel_manager.start_channels()
123 kernel_manager.start_channels()
120
124
121 # Create the widget.
125 # Create the widget.
122 app = QtGui.QApplication([])
126 app = QtGui.QApplication([])
123 if args.pure:
127 if args.pure:
124 kind = 'rich' if args.rich else 'plain'
128 kind = 'rich' if args.rich else 'plain'
125 widget = FrontendWidget(kind=kind, paging=args.paging)
129 widget = FrontendWidget(kind=kind, paging=args.paging)
126 elif args.rich:
130 elif args.rich:
127 widget = RichIPythonWidget(paging=args.paging)
131 widget = RichIPythonWidget(paging=args.paging)
128 else:
132 else:
129 widget = IPythonWidget(paging=args.paging)
133 widget = IPythonWidget(paging=args.paging)
130 widget.gui_completion = args.gui_completion
134 widget.gui_completion = args.gui_completion
131 widget.kernel_manager = kernel_manager
135 widget.kernel_manager = kernel_manager
132
136
133 # Create the main window.
137 # Create the main window.
134 window = MainWindow(widget)
138 window = MainWindow(widget)
135 window.setWindowTitle('Python' if args.pure else 'IPython')
139 window.setWindowTitle('Python' if args.pure else 'IPython')
136 window.show()
140 window.show()
137
141
138 # Start the application main loop.
142 # Start the application main loop.
139 app.exec_()
143 app.exec_()
140
144
141
145
142 if __name__ == '__main__':
146 if __name__ == '__main__':
143 main()
147 main()
General Comments 0
You need to be logged in to leave comments. Login now