##// END OF EJS Templates
Fixed bug where calltips would sometimes inappropriately disappear on OSX.
epatters -
Show More
@@ -1,225 +1,230 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, text_edit):
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(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
21 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
22 super(CallTipWidget, self).__init__(None, 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 self._text_edit = text_edit
26
26
27 self.setFont(text_edit.document().defaultFont())
27 self.setFont(text_edit.document().defaultFont())
28 self.setForegroundRole(QtGui.QPalette.ToolTipText)
28 self.setForegroundRole(QtGui.QPalette.ToolTipText)
29 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
29 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
30 self.setPalette(QtGui.QToolTip.palette())
30 self.setPalette(QtGui.QToolTip.palette())
31
31
32 self.setAlignment(QtCore.Qt.AlignLeft)
32 self.setAlignment(QtCore.Qt.AlignLeft)
33 self.setIndent(1)
33 self.setIndent(1)
34 self.setFrameStyle(QtGui.QFrame.NoFrame)
34 self.setFrameStyle(QtGui.QFrame.NoFrame)
35 self.setMargin(1 + self.style().pixelMetric(
35 self.setMargin(1 + self.style().pixelMetric(
36 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
36 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
37 self.setWindowOpacity(self.style().styleHint(
37 self.setWindowOpacity(self.style().styleHint(
38 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
38 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
39
39
40 def eventFilter(self, obj, event):
40 def eventFilter(self, obj, event):
41 """ Reimplemented to hide on certain key presses and on text edit focus
41 """ Reimplemented to hide on certain key presses and on text edit focus
42 changes.
42 changes.
43 """
43 """
44 if obj == self._text_edit:
44 if obj == self._text_edit:
45 etype = event.type()
45 etype = event.type()
46
46
47 if etype == QtCore.QEvent.KeyPress:
47 if etype == QtCore.QEvent.KeyPress:
48 key = event.key()
48 key = event.key()
49 if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
49 if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
50 self.hide()
50 self.hide()
51 elif key == QtCore.Qt.Key_Escape:
51 elif key == QtCore.Qt.Key_Escape:
52 self.hide()
52 self.hide()
53 return True
53 return True
54
54
55 elif etype == QtCore.QEvent.FocusOut:
55 elif etype == QtCore.QEvent.FocusOut:
56 self.hide()
56 self.hide()
57
57
58 elif etype == QtCore.QEvent.Enter:
58 elif etype == QtCore.QEvent.Enter:
59 self._hide_timer.stop()
59 self._hide_timer.stop()
60
60
61 elif etype == QtCore.QEvent.Leave:
61 elif etype == QtCore.QEvent.Leave:
62 self._hide_later()
62 self._leave_event_hide()
63
63
64 return super(CallTipWidget, self).eventFilter(obj, event)
64 return super(CallTipWidget, self).eventFilter(obj, event)
65
65
66 def timerEvent(self, event):
66 def timerEvent(self, event):
67 """ Reimplemented to hide the widget when the hide timer fires.
67 """ Reimplemented to hide the widget when the hide timer fires.
68 """
68 """
69 if event.timerId() == self._hide_timer.timerId():
69 if event.timerId() == self._hide_timer.timerId():
70 self._hide_timer.stop()
70 self._hide_timer.stop()
71 self.hide()
71 self.hide()
72
72
73 #--------------------------------------------------------------------------
73 #--------------------------------------------------------------------------
74 # 'QWidget' interface
74 # 'QWidget' interface
75 #--------------------------------------------------------------------------
75 #--------------------------------------------------------------------------
76
76
77 def enterEvent(self, event):
77 def enterEvent(self, event):
78 """ Reimplemented to cancel the hide timer.
78 """ Reimplemented to cancel the hide timer.
79 """
79 """
80 super(CallTipWidget, self).enterEvent(event)
80 super(CallTipWidget, self).enterEvent(event)
81 self._hide_timer.stop()
81 self._hide_timer.stop()
82
82
83 def hideEvent(self, event):
83 def hideEvent(self, event):
84 """ Reimplemented to disconnect signal handlers and event filter.
84 """ Reimplemented to disconnect signal handlers and event filter.
85 """
85 """
86 super(CallTipWidget, self).hideEvent(event)
86 super(CallTipWidget, self).hideEvent(event)
87 self._text_edit.cursorPositionChanged.disconnect(
87 self._text_edit.cursorPositionChanged.disconnect(
88 self._cursor_position_changed)
88 self._cursor_position_changed)
89 self._text_edit.removeEventFilter(self)
89 self._text_edit.removeEventFilter(self)
90
90
91 def leaveEvent(self, event):
91 def leaveEvent(self, event):
92 """ Reimplemented to start the hide timer.
92 """ Reimplemented to start the hide timer.
93 """
93 """
94 super(CallTipWidget, self).leaveEvent(event)
94 super(CallTipWidget, self).leaveEvent(event)
95 self._hide_later()
95 self._leave_event_hide()
96
96
97 def paintEvent(self, event):
97 def paintEvent(self, event):
98 """ Reimplemented to paint the background panel.
98 """ Reimplemented to paint the background panel.
99 """
99 """
100 painter = QtGui.QStylePainter(self)
100 painter = QtGui.QStylePainter(self)
101 option = QtGui.QStyleOptionFrame()
101 option = QtGui.QStyleOptionFrame()
102 option.init(self)
102 option.init(self)
103 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
103 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
104 painter.end()
104 painter.end()
105
105
106 super(CallTipWidget, self).paintEvent(event)
106 super(CallTipWidget, self).paintEvent(event)
107
107
108 def setFont(self, font):
108 def setFont(self, font):
109 """ Reimplemented to allow use of this method as a slot.
109 """ Reimplemented to allow use of this method as a slot.
110 """
110 """
111 super(CallTipWidget, self).setFont(font)
111 super(CallTipWidget, self).setFont(font)
112
112
113 def showEvent(self, event):
113 def showEvent(self, event):
114 """ Reimplemented to connect signal handlers and event filter.
114 """ Reimplemented to connect signal handlers and event filter.
115 """
115 """
116 super(CallTipWidget, self).showEvent(event)
116 super(CallTipWidget, self).showEvent(event)
117 self._text_edit.cursorPositionChanged.connect(
117 self._text_edit.cursorPositionChanged.connect(
118 self._cursor_position_changed)
118 self._cursor_position_changed)
119 self._text_edit.installEventFilter(self)
119 self._text_edit.installEventFilter(self)
120
120
121 #--------------------------------------------------------------------------
121 #--------------------------------------------------------------------------
122 # 'CallTipWidget' interface
122 # 'CallTipWidget' interface
123 #--------------------------------------------------------------------------
123 #--------------------------------------------------------------------------
124
124
125 def show_call_info(self, call_line=None, doc=None, maxlines=20):
125 def show_call_info(self, call_line=None, doc=None, maxlines=20):
126 """ Attempts to show the specified call line and docstring at the
126 """ Attempts to show the specified call line and docstring at the
127 current cursor location. The docstring is possibly truncated for
127 current cursor location. The docstring is possibly truncated for
128 length.
128 length.
129 """
129 """
130 if doc:
130 if doc:
131 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
131 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
132 if match:
132 if match:
133 doc = doc[:match.end()] + '\n[Documentation continues...]'
133 doc = doc[:match.end()] + '\n[Documentation continues...]'
134 else:
134 else:
135 doc = ''
135 doc = ''
136
136
137 if call_line:
137 if call_line:
138 doc = '\n\n'.join([call_line, doc])
138 doc = '\n\n'.join([call_line, doc])
139 return self.show_tip(doc)
139 return self.show_tip(doc)
140
140
141 def show_tip(self, tip):
141 def show_tip(self, tip):
142 """ Attempts to show the specified tip at the current cursor location.
142 """ Attempts to show the specified tip at the current cursor location.
143 """
143 """
144 # Attempt to find the cursor position at which to show the call tip.
144 # Attempt to find the cursor position at which to show the call tip.
145 text_edit = self._text_edit
145 text_edit = self._text_edit
146 document = text_edit.document()
146 document = text_edit.document()
147 cursor = text_edit.textCursor()
147 cursor = text_edit.textCursor()
148 search_pos = cursor.position() - 1
148 search_pos = cursor.position() - 1
149 self._start_position, _ = self._find_parenthesis(search_pos,
149 self._start_position, _ = self._find_parenthesis(search_pos,
150 forward=False)
150 forward=False)
151 if self._start_position == -1:
151 if self._start_position == -1:
152 return False
152 return False
153
153
154 # Set the text and resize the widget accordingly.
154 # Set the text and resize the widget accordingly.
155 self.setText(tip)
155 self.setText(tip)
156 self.resize(self.sizeHint())
156 self.resize(self.sizeHint())
157
157
158 # Locate and show the widget. Place the tip below the current line
158 # Locate and show the widget. Place the tip below the current line
159 # unless it would be off the screen. In that case, place it above
159 # unless it would be off the screen. In that case, place it above
160 # the current line.
160 # the current line.
161 padding = 3 # Distance in pixels between cursor bounds and tip box.
161 padding = 3 # Distance in pixels between cursor bounds and tip box.
162 cursor_rect = text_edit.cursorRect(cursor)
162 cursor_rect = text_edit.cursorRect(cursor)
163 screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit)
163 screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit)
164 point = text_edit.mapToGlobal(cursor_rect.bottomRight())
164 point = text_edit.mapToGlobal(cursor_rect.bottomRight())
165 point.setY(point.y() + padding)
165 point.setY(point.y() + padding)
166 tip_height = self.size().height()
166 tip_height = self.size().height()
167 if point.y() + tip_height > screen_rect.height():
167 if point.y() + tip_height > screen_rect.height():
168 point = text_edit.mapToGlobal(cursor_rect.topRight())
168 point = text_edit.mapToGlobal(cursor_rect.topRight())
169 point.setY(point.y() - tip_height - padding)
169 point.setY(point.y() - tip_height - padding)
170 self.move(point)
170 self.move(point)
171 self.show()
171 self.show()
172 return True
172 return True
173
173
174 #--------------------------------------------------------------------------
174 #--------------------------------------------------------------------------
175 # Protected interface
175 # Protected interface
176 #--------------------------------------------------------------------------
176 #--------------------------------------------------------------------------
177
177
178 def _find_parenthesis(self, position, forward=True):
178 def _find_parenthesis(self, position, forward=True):
179 """ If 'forward' is True (resp. False), proceed forwards
179 """ If 'forward' is True (resp. False), proceed forwards
180 (resp. backwards) through the line that contains 'position' until an
180 (resp. backwards) through the line that contains 'position' until an
181 unmatched closing (resp. opening) parenthesis is found. Returns a
181 unmatched closing (resp. opening) parenthesis is found. Returns a
182 tuple containing the position of this parenthesis (or -1 if it is
182 tuple containing the position of this parenthesis (or -1 if it is
183 not found) and the number commas (at depth 0) found along the way.
183 not found) and the number commas (at depth 0) found along the way.
184 """
184 """
185 commas = depth = 0
185 commas = depth = 0
186 document = self._text_edit.document()
186 document = self._text_edit.document()
187 qchar = document.characterAt(position)
187 qchar = document.characterAt(position)
188 while (position > 0 and qchar.isPrint() and
188 while (position > 0 and qchar.isPrint() and
189 # Need to check explicitly for line/paragraph separators:
189 # Need to check explicitly for line/paragraph separators:
190 qchar.unicode() not in (0x2028, 0x2029)):
190 qchar.unicode() not in (0x2028, 0x2029)):
191 char = qchar.toAscii()
191 char = qchar.toAscii()
192 if char == ',' and depth == 0:
192 if char == ',' and depth == 0:
193 commas += 1
193 commas += 1
194 elif char == ')':
194 elif char == ')':
195 if forward and depth == 0:
195 if forward and depth == 0:
196 break
196 break
197 depth += 1
197 depth += 1
198 elif char == '(':
198 elif char == '(':
199 if not forward and depth == 0:
199 if not forward and depth == 0:
200 break
200 break
201 depth -= 1
201 depth -= 1
202 position += 1 if forward else -1
202 position += 1 if forward else -1
203 qchar = document.characterAt(position)
203 qchar = document.characterAt(position)
204 else:
204 else:
205 position = -1
205 position = -1
206 return position, commas
206 return position, commas
207
207
208 def _hide_later(self):
208 def _leave_event_hide(self):
209 """ Hides the tooltip after some time has passed.
209 """ Hides the tooltip after some time has passed (assuming the cursor is
210 not over the tooltip).
210 """
211 """
211 if not self._hide_timer.isActive():
212 if (not self._hide_timer.isActive() and
213 # If Enter events always came after Leave events, we wouldn't need
214 # this check. But on Mac OS, it sometimes happens the other way
215 # around when the tooltip is created.
216 QtGui.qApp.topLevelAt(QtGui.QCursor.pos()) != self):
212 self._hide_timer.start(300, self)
217 self._hide_timer.start(300, self)
213
218
214 #------ Signal handlers ----------------------------------------------------
219 #------ Signal handlers ----------------------------------------------------
215
220
216 def _cursor_position_changed(self):
221 def _cursor_position_changed(self):
217 """ Updates the tip based on user cursor movement.
222 """ Updates the tip based on user cursor movement.
218 """
223 """
219 cursor = self._text_edit.textCursor()
224 cursor = self._text_edit.textCursor()
220 if cursor.position() <= self._start_position:
225 if cursor.position() <= self._start_position:
221 self.hide()
226 self.hide()
222 else:
227 else:
223 position, commas = self._find_parenthesis(self._start_position + 1)
228 position, commas = self._find_parenthesis(self._start_position + 1)
224 if position != -1:
229 if position != -1:
225 self.hide()
230 self.hide()
General Comments 0
You need to be logged in to leave comments. Login now