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