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