##// END OF EJS Templates
The CallTipWidget is now smarter about automatically hiding itself.
epatters -
Show More
@@ -1,181 +1,214
1 1 # Standard library imports
2 2 import re
3 3 from textwrap import dedent
4 4
5 5 # System library imports
6 6 from PyQt4 import QtCore, QtGui
7 7
8 8
9 9 class CallTipWidget(QtGui.QLabel):
10 10 """ Shows call tips by parsing the current text of Q[Plain]TextEdit.
11 11 """
12 12
13 13 #--------------------------------------------------------------------------
14 14 # 'QObject' interface
15 15 #--------------------------------------------------------------------------
16 16
17 17 def __init__(self, parent):
18 18 """ Create a call tip manager that is attached to the specified Qt
19 19 text edit widget.
20 20 """
21 21 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
22 22 QtGui.QLabel.__init__(self, parent, QtCore.Qt.ToolTip)
23 23
24 self._hide_timer = QtCore.QBasicTimer()
25
24 26 self.setFont(parent.document().defaultFont())
25 27 self.setForegroundRole(QtGui.QPalette.ToolTipText)
26 28 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
27 29 self.setPalette(QtGui.QToolTip.palette())
28 30
29 31 self.setAlignment(QtCore.Qt.AlignLeft)
30 32 self.setIndent(1)
31 33 self.setFrameStyle(QtGui.QFrame.NoFrame)
32 34 self.setMargin(1 + self.style().pixelMetric(
33 35 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
34 36 self.setWindowOpacity(self.style().styleHint(
35 37 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
36 38
37 39 def eventFilter(self, obj, event):
38 40 """ Reimplemented to hide on certain key presses and on parent focus
39 41 changes.
40 42 """
41 43 if obj == self.parent():
42 44 etype = event.type()
43 45
44 46 if etype == QtCore.QEvent.KeyPress:
45 47 key = event.key()
46 48 if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
47 49 self.hide()
48 50 elif key == QtCore.Qt.Key_Escape:
49 51 self.hide()
50 52 return True
51 53
52 54 elif etype == QtCore.QEvent.FocusOut:
53 55 self.hide()
54 56
57 elif etype == QtCore.QEvent.Enter:
58 self._hide_timer.stop()
59
60 elif etype == QtCore.QEvent.Leave:
61 self._hide_later()
62
55 63 return QtGui.QLabel.eventFilter(self, obj, event)
56 64
65 def timerEvent(self, event):
66 """ Reimplemented to hide the widget when the hide timer fires.
67 """
68 if event.timerId() == self._hide_timer.timerId():
69 self._hide_timer.stop()
70 self.hide()
71
57 72 #--------------------------------------------------------------------------
58 73 # 'QWidget' interface
59 74 #--------------------------------------------------------------------------
60 75
76 def enterEvent(self, event):
77 """ Reimplemented to cancel the hide timer.
78 """
79 QtGui.QLabel.enterEvent(self, event)
80 self._hide_timer.stop()
81
61 82 def hideEvent(self, event):
62 83 """ Reimplemented to disconnect signal handlers and event filter.
63 84 """
64 85 QtGui.QLabel.hideEvent(self, event)
65 86 parent = self.parent()
66 87 parent.cursorPositionChanged.disconnect(self._cursor_position_changed)
67 88 parent.removeEventFilter(self)
68 89
90 def leaveEvent(self, event):
91 """ Reimplemented to start the hide timer.
92 """
93 QtGui.QLabel.leaveEvent(self, event)
94 self._hide_later()
95
69 96 def paintEvent(self, event):
70 97 """ Reimplemented to paint the background panel.
71 98 """
72 99 painter = QtGui.QStylePainter(self)
73 100 option = QtGui.QStyleOptionFrame()
74 101 option.init(self)
75 102 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
76 103 painter.end()
77 104
78 105 QtGui.QLabel.paintEvent(self, event)
79 106
80 107 def showEvent(self, event):
81 108 """ Reimplemented to connect signal handlers and event filter.
82 109 """
83 110 QtGui.QLabel.showEvent(self, event)
84 111 parent = self.parent()
85 112 parent.cursorPositionChanged.connect(self._cursor_position_changed)
86 113 parent.installEventFilter(self)
87 114
88 115 #--------------------------------------------------------------------------
89 116 # 'CallTipWidget' interface
90 117 #--------------------------------------------------------------------------
91 118
92 119 def show_docstring(self, doc, maxlines=20):
93 120 """ Attempts to show the specified docstring at the current cursor
94 121 location. The docstring is dedented and possibly truncated for
95 122 length.
96 123 """
97 124 doc = dedent(doc.rstrip()).lstrip()
98 125 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
99 126 if match:
100 127 doc = doc[:match.end()] + '\n[Documentation continues...]'
101 128 return self.show_tip(doc)
102 129
103 130 def show_tip(self, tip):
104 131 """ Attempts to show the specified tip at the current cursor location.
105 132 """
106 133 # Attempt to find the cursor position at which to show the call tip.
107 134 text_edit = self.parent()
108 135 document = text_edit.document()
109 136 cursor = text_edit.textCursor()
110 137 search_pos = cursor.position() - 1
111 138 self._start_position, _ = self._find_parenthesis(search_pos,
112 139 forward=False)
113 140 if self._start_position == -1:
114 141 return False
115 142
116 143 # Set the text and resize the widget accordingly.
117 144 self.setText(tip)
118 145 self.resize(self.sizeHint())
119 146
120 147 # Locate and show the widget. Place the tip below the current line
121 148 # unless it would be off the screen. In that case, place it above
122 149 # the current line.
123 150 padding = 3 # Distance in pixels between cursor bounds and tip box.
124 151 cursor_rect = text_edit.cursorRect(cursor)
125 152 screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit)
126 153 point = text_edit.mapToGlobal(cursor_rect.bottomRight())
127 154 point.setY(point.y() + padding)
128 155 tip_height = self.size().height()
129 156 if point.y() + tip_height > screen_rect.height():
130 157 point = text_edit.mapToGlobal(cursor_rect.topRight())
131 158 point.setY(point.y() - tip_height - padding)
132 159 self.move(point)
133 160 self.show()
134 161 return True
135 162
136 163 #--------------------------------------------------------------------------
137 164 # Protected interface
138 165 #--------------------------------------------------------------------------
139 166
140 167 def _find_parenthesis(self, position, forward=True):
141 168 """ If 'forward' is True (resp. False), proceed forwards
142 169 (resp. backwards) through the line that contains 'position' until an
143 170 unmatched closing (resp. opening) parenthesis is found. Returns a
144 171 tuple containing the position of this parenthesis (or -1 if it is
145 172 not found) and the number commas (at depth 0) found along the way.
146 173 """
147 174 commas = depth = 0
148 175 document = self.parent().document()
149 176 qchar = document.characterAt(position)
150 177 while (position > 0 and qchar.isPrint() and
151 178 # Need to check explicitly for line/paragraph separators:
152 179 qchar.unicode() not in (0x2028, 0x2029)):
153 180 char = qchar.toAscii()
154 181 if char == ',' and depth == 0:
155 182 commas += 1
156 183 elif char == ')':
157 184 if forward and depth == 0:
158 185 break
159 186 depth += 1
160 187 elif char == '(':
161 188 if not forward and depth == 0:
162 189 break
163 190 depth -= 1
164 191 position += 1 if forward else -1
165 192 qchar = document.characterAt(position)
166 193 else:
167 194 position = -1
168 195 return position, commas
169 196
197 def _hide_later(self):
198 """ Hides the tooltip after some time has passed.
199 """
200 if not self._hide_timer.isActive():
201 self._hide_timer.start(300, self)
202
170 203 #------ Signal handlers ----------------------------------------------------
171 204
172 205 def _cursor_position_changed(self):
173 206 """ Updates the tip based on user cursor movement.
174 207 """
175 208 cursor = self.parent().textCursor()
176 209 if cursor.position() <= self._start_position:
177 210 self.hide()
178 211 else:
179 212 position, commas = self._find_parenthesis(self._start_position + 1)
180 213 if position != -1:
181 214 self.hide()
General Comments 0
You need to be logged in to leave comments. Login now