##// END OF EJS Templates
Merge pull request #2830 from punchagan/fix-format-tooltip...
Bradley M. Froehle -
r9225:21c6d8fe merge
parent child Browse files
Show More
@@ -1,273 +1,275
1 1 # Standard library imports
2 2 import re
3 from textwrap import dedent
4 3 from unicodedata import category
5 4
6 5 # System library imports
7 6 from IPython.external.qt import QtCore, QtGui
8 7
9 8
10 9 class CallTipWidget(QtGui.QLabel):
11 10 """ Shows call tips by parsing the current text of Q[Plain]TextEdit.
12 11 """
13 12
14 13 #--------------------------------------------------------------------------
15 14 # 'QObject' interface
16 15 #--------------------------------------------------------------------------
17 16
18 17 def __init__(self, text_edit):
19 18 """ Create a call tip manager that is attached to the specified Qt
20 19 text edit widget.
21 20 """
22 21 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
23 22 super(CallTipWidget, self).__init__(None, QtCore.Qt.ToolTip)
24 23
25 24 self._hide_timer = QtCore.QBasicTimer()
26 25 self._text_edit = text_edit
27 26
28 27 self.setFont(text_edit.document().defaultFont())
29 28 self.setForegroundRole(QtGui.QPalette.ToolTipText)
30 29 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
31 30 self.setPalette(QtGui.QToolTip.palette())
32 31
33 32 self.setAlignment(QtCore.Qt.AlignLeft)
34 33 self.setIndent(1)
35 34 self.setFrameStyle(QtGui.QFrame.NoFrame)
36 35 self.setMargin(1 + self.style().pixelMetric(
37 36 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
38 37 self.setWindowOpacity(self.style().styleHint(
39 38 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self, None) / 255.0)
40 39
41 40 def eventFilter(self, obj, event):
42 41 """ Reimplemented to hide on certain key presses and on text edit focus
43 42 changes.
44 43 """
45 44 if obj == self._text_edit:
46 45 etype = event.type()
47 46
48 47 if etype == QtCore.QEvent.KeyPress:
49 48 key = event.key()
50 49 if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
51 50 self.hide()
52 51 elif key == QtCore.Qt.Key_Escape:
53 52 self.hide()
54 53 return True
55 54
56 55 elif etype == QtCore.QEvent.FocusOut:
57 56 self.hide()
58 57
59 58 elif etype == QtCore.QEvent.Enter:
60 59 self._hide_timer.stop()
61 60
62 61 elif etype == QtCore.QEvent.Leave:
63 62 self._leave_event_hide()
64 63
65 64 return super(CallTipWidget, self).eventFilter(obj, event)
66 65
67 66 def timerEvent(self, event):
68 67 """ Reimplemented to hide the widget when the hide timer fires.
69 68 """
70 69 if event.timerId() == self._hide_timer.timerId():
71 70 self._hide_timer.stop()
72 71 self.hide()
73 72
74 73 #--------------------------------------------------------------------------
75 74 # 'QWidget' interface
76 75 #--------------------------------------------------------------------------
77 76
78 77 def enterEvent(self, event):
79 78 """ Reimplemented to cancel the hide timer.
80 79 """
81 80 super(CallTipWidget, self).enterEvent(event)
82 81 self._hide_timer.stop()
83 82
84 83 def hideEvent(self, event):
85 84 """ Reimplemented to disconnect signal handlers and event filter.
86 85 """
87 86 super(CallTipWidget, self).hideEvent(event)
88 87 self._text_edit.cursorPositionChanged.disconnect(
89 88 self._cursor_position_changed)
90 89 self._text_edit.removeEventFilter(self)
91 90
92 91 def leaveEvent(self, event):
93 92 """ Reimplemented to start the hide timer.
94 93 """
95 94 super(CallTipWidget, self).leaveEvent(event)
96 95 self._leave_event_hide()
97 96
98 97 def paintEvent(self, event):
99 98 """ Reimplemented to paint the background panel.
100 99 """
101 100 painter = QtGui.QStylePainter(self)
102 101 option = QtGui.QStyleOptionFrame()
103 102 option.initFrom(self)
104 103 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
105 104 painter.end()
106 105
107 106 super(CallTipWidget, self).paintEvent(event)
108 107
109 108 def setFont(self, font):
110 109 """ Reimplemented to allow use of this method as a slot.
111 110 """
112 111 super(CallTipWidget, self).setFont(font)
113 112
114 113 def showEvent(self, event):
115 114 """ Reimplemented to connect signal handlers and event filter.
116 115 """
117 116 super(CallTipWidget, self).showEvent(event)
118 117 self._text_edit.cursorPositionChanged.connect(
119 118 self._cursor_position_changed)
120 119 self._text_edit.installEventFilter(self)
121 120
122 121 #--------------------------------------------------------------------------
123 122 # 'CallTipWidget' interface
124 123 #--------------------------------------------------------------------------
125 124
126 125 def show_call_info(self, call_line=None, doc=None, maxlines=20):
127 126 """ Attempts to show the specified call line and docstring at the
128 127 current cursor location. The docstring is possibly truncated for
129 128 length.
130 129 """
131 130 if doc:
132 131 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
133 132 if match:
134 133 doc = doc[:match.end()] + '\n[Documentation continues...]'
135 134 else:
136 135 doc = ''
137 136
138 137 if call_line:
139 138 doc = '\n\n'.join([call_line, doc])
140 139 return self.show_tip(self._format_tooltip(doc))
141 140
142 141 def show_tip(self, tip):
143 142 """ Attempts to show the specified tip at the current cursor location.
144 143 """
145 144 # Attempt to find the cursor position at which to show the call tip.
146 145 text_edit = self._text_edit
147 146 document = text_edit.document()
148 147 cursor = text_edit.textCursor()
149 148 search_pos = cursor.position() - 1
150 149 self._start_position, _ = self._find_parenthesis(search_pos,
151 150 forward=False)
152 151 if self._start_position == -1:
153 152 return False
154 153
155 154 # Set the text and resize the widget accordingly.
156 155 self.setText(tip)
157 156 self.resize(self.sizeHint())
158 157
159 158 # Locate and show the widget. Place the tip below the current line
160 159 # unless it would be off the screen. In that case, decide the best
161 160 # location based trying to minimize the area that goes off-screen.
162 161 padding = 3 # Distance in pixels between cursor bounds and tip box.
163 162 cursor_rect = text_edit.cursorRect(cursor)
164 163 screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit)
165 164 point = text_edit.mapToGlobal(cursor_rect.bottomRight())
166 165 point.setY(point.y() + padding)
167 166 tip_height = self.size().height()
168 167 tip_width = self.size().width()
169 168
170 169 vertical = 'bottom'
171 170 horizontal = 'Right'
172 171 if point.y() + tip_height > screen_rect.height():
173 172 point_ = text_edit.mapToGlobal(cursor_rect.topRight())
174 173 # If tip is still off screen, check if point is in top or bottom
175 174 # half of screen.
176 175 if point_.y() - tip_height < padding:
177 176 # If point is in upper half of screen, show tip below it.
178 177 # otherwise above it.
179 178 if 2*point.y() < screen_rect.height():
180 179 vertical = 'bottom'
181 180 else:
182 181 vertical = 'top'
183 182 else:
184 183 vertical = 'top'
185 184 if point.x() + tip_width > screen_rect.width():
186 185 point_ = text_edit.mapToGlobal(cursor_rect.topRight())
187 186 # If tip is still off-screen, check if point is in the right or
188 187 # left half of the screen.
189 188 if point_.x() - tip_width < padding:
190 189 if 2*point.x() < screen_rect.width():
191 190 horizontal = 'Right'
192 191 else:
193 192 horizontal = 'Left'
194 193 else:
195 194 horizontal = 'Left'
196 195 pos = getattr(cursor_rect, '%s%s' %(vertical, horizontal))
197 196 point = text_edit.mapToGlobal(pos())
198 197 if vertical == 'top':
199 198 point.setY(point.y() - tip_height - padding)
200 199 if horizontal == 'Left':
201 200 point.setX(point.x() - tip_width - padding)
202 201
203 202 self.move(point)
204 203 self.show()
205 204 return True
206 205
207 206 #--------------------------------------------------------------------------
208 207 # Protected interface
209 208 #--------------------------------------------------------------------------
210 209
211 210 def _find_parenthesis(self, position, forward=True):
212 211 """ If 'forward' is True (resp. False), proceed forwards
213 212 (resp. backwards) through the line that contains 'position' until an
214 213 unmatched closing (resp. opening) parenthesis is found. Returns a
215 214 tuple containing the position of this parenthesis (or -1 if it is
216 215 not found) and the number commas (at depth 0) found along the way.
217 216 """
218 217 commas = depth = 0
219 218 document = self._text_edit.document()
220 219 char = document.characterAt(position)
221 220 # Search until a match is found or a non-printable character is
222 221 # encountered.
223 222 while category(char) != 'Cc' and position > 0:
224 223 if char == ',' and depth == 0:
225 224 commas += 1
226 225 elif char == ')':
227 226 if forward and depth == 0:
228 227 break
229 228 depth += 1
230 229 elif char == '(':
231 230 if not forward and depth == 0:
232 231 break
233 232 depth -= 1
234 233 position += 1 if forward else -1
235 234 char = document.characterAt(position)
236 235 else:
237 236 position = -1
238 237 return position, commas
239 238
240 239 def _leave_event_hide(self):
241 240 """ Hides the tooltip after some time has passed (assuming the cursor is
242 241 not over the tooltip).
243 242 """
244 243 if (not self._hide_timer.isActive() and
245 244 # If Enter events always came after Leave events, we wouldn't need
246 245 # this check. But on Mac OS, it sometimes happens the other way
247 246 # around when the tooltip is created.
248 247 QtGui.qApp.topLevelAt(QtGui.QCursor.pos()) != self):
249 248 self._hide_timer.start(300, self)
250 249
251 250 def _format_tooltip(self,doc):
252 251 import textwrap
253 252
254 253 # make sure a long argument list does not make
255 254 # the first row overflow the width of the actual tip body
256 255 rows = doc.split("\n")
256 # An object which is not a callable has '<no docstring>' as doc
257 if len(rows) == 1:
258 return doc
257 259 max_text_width = max(80, max([len(x) for x in rows[1:]]))
258 260 rows= textwrap.wrap(rows[0],max_text_width) + rows[1:]
259 261 doc = "\n".join(rows)
260 262 return doc
261 263
262 264 #------ Signal handlers ----------------------------------------------------
263 265
264 266 def _cursor_position_changed(self):
265 267 """ Updates the tip based on user cursor movement.
266 268 """
267 269 cursor = self._text_edit.textCursor()
268 270 if cursor.position() <= self._start_position:
269 271 self.hide()
270 272 else:
271 273 position, commas = self._find_parenthesis(self._start_position + 1)
272 274 if position != -1:
273 275 self.hide()
General Comments 0
You need to be logged in to leave comments. Login now