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