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