##// END OF EJS Templates
Fix QtConsole tooltip formatting remove string mangling, and turn on QT wordWrap
Kalibri -
Show More
@@ -1,270 +1,260 b''
1 1 # Standard library imports
2 2 import re
3 import textwrap
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)
39 self.setWordWrap(True)
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_inspect_data(self, content, maxlines=20):
127 127 """Show inspection data as a tooltip"""
128 128 data = content.get('data', {})
129 129 text = data.get('text/plain', '')
130 130 match = re.match("(?:[^\n]*\n){%i}" % maxlines, text)
131 131 if match:
132 132 text = text[:match.end()] + '\n[Documentation continues...]'
133 133
134 134 return self.show_tip(self._format_tooltip(text))
135 135
136 136 def show_tip(self, tip):
137 137 """ Attempts to show the specified tip at the current cursor location.
138 138 """
139 139 # Attempt to find the cursor position at which to show the call tip.
140 140 text_edit = self._text_edit
141 141 document = text_edit.document()
142 142 cursor = text_edit.textCursor()
143 143 search_pos = cursor.position() - 1
144 144 self._start_position, _ = self._find_parenthesis(search_pos,
145 145 forward=False)
146 146 if self._start_position == -1:
147 147 return False
148 148
149 149 # Set the text and resize the widget accordingly.
150 150 self.setText(tip)
151 151 self.resize(self.sizeHint())
152 152
153 153 # Locate and show the widget. Place the tip below the current line
154 154 # unless it would be off the screen. In that case, decide the best
155 155 # location based trying to minimize the area that goes off-screen.
156 156 padding = 3 # Distance in pixels between cursor bounds and tip box.
157 157 cursor_rect = text_edit.cursorRect(cursor)
158 158 screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit)
159 159 point = text_edit.mapToGlobal(cursor_rect.bottomRight())
160 160 point.setY(point.y() + padding)
161 161 tip_height = self.size().height()
162 162 tip_width = self.size().width()
163 163
164 164 vertical = 'bottom'
165 165 horizontal = 'Right'
166 166 if point.y() + tip_height > screen_rect.height():
167 167 point_ = text_edit.mapToGlobal(cursor_rect.topRight())
168 168 # If tip is still off screen, check if point is in top or bottom
169 169 # half of screen.
170 170 if point_.y() - tip_height < padding:
171 171 # If point is in upper half of screen, show tip below it.
172 172 # otherwise above it.
173 173 if 2*point.y() < screen_rect.height():
174 174 vertical = 'bottom'
175 175 else:
176 176 vertical = 'top'
177 177 else:
178 178 vertical = 'top'
179 179 if point.x() + tip_width > screen_rect.width():
180 180 point_ = text_edit.mapToGlobal(cursor_rect.topRight())
181 181 # If tip is still off-screen, check if point is in the right or
182 182 # left half of the screen.
183 183 if point_.x() - tip_width < padding:
184 184 if 2*point.x() < screen_rect.width():
185 185 horizontal = 'Right'
186 186 else:
187 187 horizontal = 'Left'
188 188 else:
189 189 horizontal = 'Left'
190 190 pos = getattr(cursor_rect, '%s%s' %(vertical, horizontal))
191 191 point = text_edit.mapToGlobal(pos())
192 192 if vertical == 'top':
193 193 point.setY(point.y() - tip_height - padding)
194 194 if horizontal == 'Left':
195 195 point.setX(point.x() - tip_width - padding)
196 196
197 197 self.move(point)
198 198 self.show()
199 199 return True
200 200
201 201 #--------------------------------------------------------------------------
202 202 # Protected interface
203 203 #--------------------------------------------------------------------------
204 204
205 205 def _find_parenthesis(self, position, forward=True):
206 206 """ If 'forward' is True (resp. False), proceed forwards
207 207 (resp. backwards) through the line that contains 'position' until an
208 208 unmatched closing (resp. opening) parenthesis is found. Returns a
209 209 tuple containing the position of this parenthesis (or -1 if it is
210 210 not found) and the number commas (at depth 0) found along the way.
211 211 """
212 212 commas = depth = 0
213 213 document = self._text_edit.document()
214 214 char = document.characterAt(position)
215 215 # Search until a match is found or a non-printable character is
216 216 # encountered.
217 217 while category(char) != 'Cc' and position > 0:
218 218 if char == ',' and depth == 0:
219 219 commas += 1
220 220 elif char == ')':
221 221 if forward and depth == 0:
222 222 break
223 223 depth += 1
224 224 elif char == '(':
225 225 if not forward and depth == 0:
226 226 break
227 227 depth -= 1
228 228 position += 1 if forward else -1
229 229 char = document.characterAt(position)
230 230 else:
231 231 position = -1
232 232 return position, commas
233 233
234 234 def _leave_event_hide(self):
235 235 """ Hides the tooltip after some time has passed (assuming the cursor is
236 236 not over the tooltip).
237 237 """
238 238 if (not self._hide_timer.isActive() and
239 239 # If Enter events always came after Leave events, we wouldn't need
240 240 # this check. But on Mac OS, it sometimes happens the other way
241 241 # around when the tooltip is created.
242 242 QtGui.qApp.topLevelAt(QtGui.QCursor.pos()) != self):
243 243 self._hide_timer.start(300, self)
244 244
245 245 def _format_tooltip(self, doc):
246 246 doc = re.sub(r'\033\[(\d|;)+?m', '', doc)
247
248 # make sure a long argument list does not make
249 # the first row overflow the width of the actual tip body
250 rows = doc.split("\n")
251 # An object which is not a callable has '<no docstring>' as doc
252 if len(rows) == 1:
253 return doc
254 max_text_width = max(80, max([len(x) for x in rows[1:]]))
255 rows= textwrap.wrap(rows[0],max_text_width) + rows[1:]
256 doc = "\n".join(rows)
257 247 return doc
258 248
259 249 #------ Signal handlers ----------------------------------------------------
260 250
261 251 def _cursor_position_changed(self):
262 252 """ Updates the tip based on user cursor movement.
263 253 """
264 254 cursor = self._text_edit.textCursor()
265 255 if cursor.position() <= self._start_position:
266 256 self.hide()
267 257 else:
268 258 position, commas = self._find_parenthesis(self._start_position + 1)
269 259 if position != -1:
270 260 self.hide()
General Comments 0
You need to be logged in to leave comments. Login now