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