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