##// END OF EJS Templates
BUG: Improve placement of CallTipWidget...
Puneeth Chaganti -
Show More
@@ -1,230 +1,262 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(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, place it above
160 # unless it would be off the screen. In that case, decide the best
161 # the current line.
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()
169
170 vertical = 'bottom'
171 horizontal = 'Right'
168 if point.y() + tip_height > screen_rect.height():
172 if point.y() + tip_height > screen_rect.height():
169 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
175 # half of screen.
176 if point_.y() - tip_height < padding:
177 # If point is in upper half of screen, show tip below it.
178 # otherwise above it.
179 if 2*point.y() < screen_rect.height():
180 vertical = 'bottom'
181 else:
182 vertical = 'top'
183 else:
184 vertical = 'top'
185 if point.x() + tip_width > screen_rect.width():
186 point_ = text_edit.mapToGlobal(cursor_rect.topRight())
187 # If tip is still off-screen, check if point is in the right or
188 # left half of the screen.
189 if point_.x() - tip_width < padding:
190 if 2*point.x() < screen_rect.width():
191 horizontal = 'Right'
192 else:
193 horizontal = 'Left'
194 else:
195 horizontal = 'Left'
196 pos = getattr(cursor_rect, '%s%s' %(vertical, horizontal))
197 point = text_edit.mapToGlobal(pos())
198 if vertical == 'top':
170 point.setY(point.y() - tip_height - padding)
199 point.setY(point.y() - tip_height - padding)
200 if horizontal == 'Left':
201 point.setX(point.x() - tip_width - padding)
202
171 self.move(point)
203 self.move(point)
172 self.show()
204 self.show()
173 return True
205 return True
174
206
175 #--------------------------------------------------------------------------
207 #--------------------------------------------------------------------------
176 # Protected interface
208 # Protected interface
177 #--------------------------------------------------------------------------
209 #--------------------------------------------------------------------------
178
210
179 def _find_parenthesis(self, position, forward=True):
211 def _find_parenthesis(self, position, forward=True):
180 """ If 'forward' is True (resp. False), proceed forwards
212 """ If 'forward' is True (resp. False), proceed forwards
181 (resp. backwards) through the line that contains 'position' until an
213 (resp. backwards) through the line that contains 'position' until an
182 unmatched closing (resp. opening) parenthesis is found. Returns a
214 unmatched closing (resp. opening) parenthesis is found. Returns a
183 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
184 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.
185 """
217 """
186 commas = depth = 0
218 commas = depth = 0
187 document = self._text_edit.document()
219 document = self._text_edit.document()
188 char = document.characterAt(position)
220 char = document.characterAt(position)
189 # 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
190 # encountered.
222 # encountered.
191 while category(char) != 'Cc' and position > 0:
223 while category(char) != 'Cc' and position > 0:
192 if char == ',' and depth == 0:
224 if char == ',' and depth == 0:
193 commas += 1
225 commas += 1
194 elif char == ')':
226 elif char == ')':
195 if forward and depth == 0:
227 if forward and depth == 0:
196 break
228 break
197 depth += 1
229 depth += 1
198 elif char == '(':
230 elif char == '(':
199 if not forward and depth == 0:
231 if not forward and depth == 0:
200 break
232 break
201 depth -= 1
233 depth -= 1
202 position += 1 if forward else -1
234 position += 1 if forward else -1
203 char = document.characterAt(position)
235 char = document.characterAt(position)
204 else:
236 else:
205 position = -1
237 position = -1
206 return position, commas
238 return position, commas
207
239
208 def _leave_event_hide(self):
240 def _leave_event_hide(self):
209 """ 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
210 not over the tooltip).
242 not over the tooltip).
211 """
243 """
212 if (not self._hide_timer.isActive() and
244 if (not self._hide_timer.isActive() and
213 # 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
214 # 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
215 # around when the tooltip is created.
247 # around when the tooltip is created.
216 QtGui.qApp.topLevelAt(QtGui.QCursor.pos()) != self):
248 QtGui.qApp.topLevelAt(QtGui.QCursor.pos()) != self):
217 self._hide_timer.start(300, self)
249 self._hide_timer.start(300, self)
218
250
219 #------ Signal handlers ----------------------------------------------------
251 #------ Signal handlers ----------------------------------------------------
220
252
221 def _cursor_position_changed(self):
253 def _cursor_position_changed(self):
222 """ Updates the tip based on user cursor movement.
254 """ Updates the tip based on user cursor movement.
223 """
255 """
224 cursor = self._text_edit.textCursor()
256 cursor = self._text_edit.textCursor()
225 if cursor.position() <= self._start_position:
257 if cursor.position() <= self._start_position:
226 self.hide()
258 self.hide()
227 else:
259 else:
228 position, commas = self._find_parenthesis(self._start_position + 1)
260 position, commas = self._find_parenthesis(self._start_position + 1)
229 if position != -1:
261 if position != -1:
230 self.hide()
262 self.hide()
General Comments 0
You need to be logged in to leave comments. Login now