call_tip_widget.py
171 lines
| 6.5 KiB
| text/x-python
|
PythonLexer
epatters
|
r2666 | # Standard library imports | ||
import re | ||||
from textwrap import dedent | ||||
epatters
|
r2602 | # System library imports | ||
from PyQt4 import QtCore, QtGui | ||||
class CallTipWidget(QtGui.QLabel): | ||||
""" Shows call tips by parsing the current text of Q[Plain]TextEdit. | ||||
""" | ||||
#-------------------------------------------------------------------------- | ||||
# 'QWidget' interface | ||||
#-------------------------------------------------------------------------- | ||||
def __init__(self, parent): | ||||
""" Create a call tip manager that is attached to the specified Qt | ||||
text edit widget. | ||||
""" | ||||
assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) | ||||
QtGui.QLabel.__init__(self, parent, QtCore.Qt.ToolTip) | ||||
self.setFont(parent.document().defaultFont()) | ||||
self.setForegroundRole(QtGui.QPalette.ToolTipText) | ||||
self.setBackgroundRole(QtGui.QPalette.ToolTipBase) | ||||
self.setPalette(QtGui.QToolTip.palette()) | ||||
self.setAlignment(QtCore.Qt.AlignLeft) | ||||
self.setIndent(1) | ||||
self.setFrameStyle(QtGui.QFrame.NoFrame) | ||||
self.setMargin(1 + self.style().pixelMetric( | ||||
QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self)) | ||||
self.setWindowOpacity(self.style().styleHint( | ||||
QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0) | ||||
def hideEvent(self, event): | ||||
""" Reimplemented to disconnect the cursor movement handler. | ||||
""" | ||||
epatters
|
r2673 | QtGui.QLabel.hideEvent(self, event) | ||
epatters
|
r2602 | self.parent().cursorPositionChanged.disconnect(self._update_tip) | ||
def paintEvent(self, event): | ||||
""" Reimplemented to paint the background panel. | ||||
""" | ||||
painter = QtGui.QStylePainter(self) | ||||
option = QtGui.QStyleOptionFrame() | ||||
option.init(self) | ||||
painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option) | ||||
painter.end() | ||||
QtGui.QLabel.paintEvent(self, event) | ||||
def showEvent(self, event): | ||||
""" Reimplemented to connect the cursor movement handler. | ||||
""" | ||||
epatters
|
r2673 | QtGui.QLabel.showEvent(self, event) | ||
epatters
|
r2602 | self.parent().cursorPositionChanged.connect(self._update_tip) | ||
#-------------------------------------------------------------------------- | ||||
# 'CallTipWidget' interface | ||||
#-------------------------------------------------------------------------- | ||||
epatters
|
r2666 | def show_docstring(self, doc, maxlines=20): | ||
""" Attempts to show the specified docstring at the current cursor | ||||
location. The docstring is dedented and possibly truncated for | ||||
length. | ||||
""" | ||||
doc = dedent(doc.rstrip()).lstrip() | ||||
match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc) | ||||
if match: | ||||
doc = doc[:match.end()] + '\n[Documentation continues...]' | ||||
return self.show_tip(doc) | ||||
epatters
|
r2602 | def show_tip(self, tip): | ||
""" Attempts to show the specified tip at the current cursor location. | ||||
""" | ||||
text_edit = self.parent() | ||||
document = text_edit.document() | ||||
cursor = text_edit.textCursor() | ||||
search_pos = cursor.position() - 1 | ||||
self._start_position, _ = self._find_parenthesis(search_pos, | ||||
forward=False) | ||||
if self._start_position == -1: | ||||
return False | ||||
point = text_edit.cursorRect(cursor).bottomRight() | ||||
point = text_edit.mapToGlobal(point) | ||||
self.move(point) | ||||
self.setText(tip) | ||||
if self.isVisible(): | ||||
self.resize(self.sizeHint()) | ||||
else: | ||||
self.show() | ||||
return True | ||||
#-------------------------------------------------------------------------- | ||||
# Protected interface | ||||
#-------------------------------------------------------------------------- | ||||
def _find_parenthesis(self, position, forward=True): | ||||
""" If 'forward' is True (resp. False), proceed forwards | ||||
(resp. backwards) through the line that contains 'position' until an | ||||
unmatched closing (resp. opening) parenthesis is found. Returns a | ||||
tuple containing the position of this parenthesis (or -1 if it is | ||||
not found) and the number commas (at depth 0) found along the way. | ||||
""" | ||||
commas = depth = 0 | ||||
document = self.parent().document() | ||||
qchar = document.characterAt(position) | ||||
while (position > 0 and qchar.isPrint() and | ||||
# Need to check explicitly for line/paragraph separators: | ||||
qchar.unicode() not in (0x2028, 0x2029)): | ||||
char = qchar.toAscii() | ||||
if char == ',' and depth == 0: | ||||
commas += 1 | ||||
elif char == ')': | ||||
if forward and depth == 0: | ||||
break | ||||
depth += 1 | ||||
elif char == '(': | ||||
if not forward and depth == 0: | ||||
break | ||||
depth -= 1 | ||||
position += 1 if forward else -1 | ||||
qchar = document.characterAt(position) | ||||
else: | ||||
position = -1 | ||||
return position, commas | ||||
def _highlight_tip(self, tip, current_argument): | ||||
""" Highlight the current argument (arguments start at 0), ending at the | ||||
next comma or unmatched closing parenthesis. | ||||
FIXME: This is an unreliable way to do things and it isn't being | ||||
used right now. Instead, we should use inspect.getargspec | ||||
metadata for this purpose. | ||||
""" | ||||
start = tip.find('(') | ||||
if start != -1: | ||||
for i in xrange(current_argument): | ||||
start = tip.find(',', start) | ||||
if start != -1: | ||||
end = start + 1 | ||||
while end < len(tip): | ||||
char = tip[end] | ||||
depth = 0 | ||||
if (char == ',' and depth == 0): | ||||
break | ||||
elif char == '(': | ||||
depth += 1 | ||||
elif char == ')': | ||||
if depth == 0: | ||||
break | ||||
depth -= 1 | ||||
end += 1 | ||||
tip = tip[:start+1] + '<font color="blue">' + \ | ||||
tip[start+1:end] + '</font>' + tip[end:] | ||||
tip = tip.replace('\n', '<br/>') | ||||
return tip | ||||
def _update_tip(self): | ||||
""" Updates the tip based on user cursor movement. | ||||
""" | ||||
cursor = self.parent().textCursor() | ||||
if cursor.position() <= self._start_position: | ||||
self.hide() | ||||
else: | ||||
position, commas = self._find_parenthesis(self._start_position + 1) | ||||
if position != -1: | ||||
self.hide() | ||||