##// END OF EJS Templates
BUG: Improve placement of CallTipWidget...
Puneeth Chaganti -
Show More
@@ -1,230 +1,262 b''
1 1 # Standard library imports
2 2 import re
3 3 from textwrap import dedent
4 4 from unicodedata import category
5 5
6 6 # System library imports
7 7 from IPython.external.qt import QtCore, QtGui
8 8
9 9
10 10 class CallTipWidget(QtGui.QLabel):
11 11 """ Shows call tips by parsing the current text of Q[Plain]TextEdit.
12 12 """
13 13
14 14 #--------------------------------------------------------------------------
15 15 # 'QObject' interface
16 16 #--------------------------------------------------------------------------
17 17
18 18 def __init__(self, text_edit):
19 19 """ Create a call tip manager that is attached to the specified Qt
20 20 text edit widget.
21 21 """
22 22 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
23 23 super(CallTipWidget, self).__init__(None, QtCore.Qt.ToolTip)
24 24
25 25 self._hide_timer = QtCore.QBasicTimer()
26 26 self._text_edit = text_edit
27 27
28 28 self.setFont(text_edit.document().defaultFont())
29 29 self.setForegroundRole(QtGui.QPalette.ToolTipText)
30 30 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
31 31 self.setPalette(QtGui.QToolTip.palette())
32 32
33 33 self.setAlignment(QtCore.Qt.AlignLeft)
34 34 self.setIndent(1)
35 35 self.setFrameStyle(QtGui.QFrame.NoFrame)
36 36 self.setMargin(1 + self.style().pixelMetric(
37 37 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
38 38 self.setWindowOpacity(self.style().styleHint(
39 39 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self, None) / 255.0)
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_call_info(self, call_line=None, doc=None, maxlines=20):
127 127 """ Attempts to show the specified call line and docstring at the
128 128 current cursor location. The docstring is possibly truncated for
129 129 length.
130 130 """
131 131 if doc:
132 132 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
133 133 if match:
134 134 doc = doc[:match.end()] + '\n[Documentation continues...]'
135 135 else:
136 136 doc = ''
137 137
138 138 if call_line:
139 139 doc = '\n\n'.join([call_line, doc])
140 140 return self.show_tip(doc)
141 141
142 142 def show_tip(self, tip):
143 143 """ Attempts to show the specified tip at the current cursor location.
144 144 """
145 145 # Attempt to find the cursor position at which to show the call tip.
146 146 text_edit = self._text_edit
147 147 document = text_edit.document()
148 148 cursor = text_edit.textCursor()
149 149 search_pos = cursor.position() - 1
150 150 self._start_position, _ = self._find_parenthesis(search_pos,
151 151 forward=False)
152 152 if self._start_position == -1:
153 153 return False
154 154
155 155 # Set the text and resize the widget accordingly.
156 156 self.setText(tip)
157 157 self.resize(self.sizeHint())
158 158
159 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
161 # the current line.
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.
162 162 padding = 3 # Distance in pixels between cursor bounds and tip box.
163 163 cursor_rect = text_edit.cursorRect(cursor)
164 164 screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit)
165 165 point = text_edit.mapToGlobal(cursor_rect.bottomRight())
166 166 point.setY(point.y() + padding)
167 167 tip_height = self.size().height()
168 tip_width = self.size().width()
169
170 vertical = 'bottom'
171 horizontal = 'Right'
168 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 199 point.setY(point.y() - tip_height - padding)
200 if horizontal == 'Left':
201 point.setX(point.x() - tip_width - padding)
202
171 203 self.move(point)
172 204 self.show()
173 205 return True
174 206
175 207 #--------------------------------------------------------------------------
176 208 # Protected interface
177 209 #--------------------------------------------------------------------------
178 210
179 211 def _find_parenthesis(self, position, forward=True):
180 212 """ If 'forward' is True (resp. False), proceed forwards
181 213 (resp. backwards) through the line that contains 'position' until an
182 214 unmatched closing (resp. opening) parenthesis is found. Returns a
183 215 tuple containing the position of this parenthesis (or -1 if it is
184 216 not found) and the number commas (at depth 0) found along the way.
185 217 """
186 218 commas = depth = 0
187 219 document = self._text_edit.document()
188 220 char = document.characterAt(position)
189 221 # Search until a match is found or a non-printable character is
190 222 # encountered.
191 223 while category(char) != 'Cc' and position > 0:
192 224 if char == ',' and depth == 0:
193 225 commas += 1
194 226 elif char == ')':
195 227 if forward and depth == 0:
196 228 break
197 229 depth += 1
198 230 elif char == '(':
199 231 if not forward and depth == 0:
200 232 break
201 233 depth -= 1
202 234 position += 1 if forward else -1
203 235 char = document.characterAt(position)
204 236 else:
205 237 position = -1
206 238 return position, commas
207 239
208 240 def _leave_event_hide(self):
209 241 """ Hides the tooltip after some time has passed (assuming the cursor is
210 242 not over the tooltip).
211 243 """
212 244 if (not self._hide_timer.isActive() and
213 245 # If Enter events always came after Leave events, we wouldn't need
214 246 # this check. But on Mac OS, it sometimes happens the other way
215 247 # around when the tooltip is created.
216 248 QtGui.qApp.topLevelAt(QtGui.QCursor.pos()) != self):
217 249 self._hide_timer.start(300, self)
218 250
219 251 #------ Signal handlers ----------------------------------------------------
220 252
221 253 def _cursor_position_changed(self):
222 254 """ Updates the tip based on user cursor movement.
223 255 """
224 256 cursor = self._text_edit.textCursor()
225 257 if cursor.position() <= self._start_position:
226 258 self.hide()
227 259 else:
228 260 position, commas = self._find_parenthesis(self._start_position + 1)
229 261 if position != -1:
230 262 self.hide()
General Comments 0
You need to be logged in to leave comments. Login now