call_tip_widget.py
174 lines
| 6.8 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. | ||||
""" | ||||
#-------------------------------------------------------------------------- | ||||
epatters
|
r2687 | # 'QObject' interface | ||
epatters
|
r2602 | #-------------------------------------------------------------------------- | ||
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) | ||||
epatters
|
r2744 | def eventFilter(self, obj, event): | ||
""" Reimplemented to hide on certain key presses and on parent focus | ||||
changes. | ||||
""" | ||||
if obj == self.parent(): | ||||
etype = event.type() | ||||
if (etype == QtCore.QEvent.KeyPress and | ||||
event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return, | ||||
QtCore.Qt.Key_Escape)): | ||||
self.hide() | ||||
elif etype == QtCore.QEvent.FocusOut: | ||||
self.hide() | ||||
return QtGui.QLabel.eventFilter(self, obj, event) | ||||
epatters
|
r2687 | #-------------------------------------------------------------------------- | ||
# 'QWidget' interface | ||||
#-------------------------------------------------------------------------- | ||||
epatters
|
r2602 | def hideEvent(self, event): | ||
epatters
|
r2744 | """ Reimplemented to disconnect signal handlers and event filter. | ||
epatters
|
r2602 | """ | ||
epatters
|
r2673 | QtGui.QLabel.hideEvent(self, event) | ||
epatters
|
r2744 | parent = self.parent() | ||
parent.cursorPositionChanged.disconnect(self._cursor_position_changed) | ||||
parent.removeEventFilter(self) | ||||
epatters
|
r2687 | |||
epatters
|
r2602 | 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): | ||||
epatters
|
r2744 | """ Reimplemented to connect signal handlers and event filter. | ||
epatters
|
r2602 | """ | ||
epatters
|
r2673 | QtGui.QLabel.showEvent(self, event) | ||
epatters
|
r2744 | parent = self.parent() | ||
parent.cursorPositionChanged.connect(self._cursor_position_changed) | ||||
parent.installEventFilter(self) | ||||
epatters
|
r2602 | |||
#-------------------------------------------------------------------------- | ||||
# '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. | ||||
""" | ||||
epatters
|
r2881 | # Attempt to find the cursor position at which to show the call tip. | ||
epatters
|
r2602 | 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 | ||||
epatters
|
r2881 | |||
# Set the text and resize the widget accordingly. | ||||
epatters
|
r2602 | self.setText(tip) | ||
epatters
|
r2758 | self.resize(self.sizeHint()) | ||
epatters
|
r2881 | |||
# Locate and show the widget. Place the tip below the current line | ||||
# unless it would be off the screen. In that case, place it above | ||||
# the current line. | ||||
cursor_rect = text_edit.cursorRect(cursor) | ||||
screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit) | ||||
point = text_edit.mapToGlobal(cursor_rect.bottomRight()) | ||||
tip_height = self.size().height() | ||||
if point.y() + tip_height > screen_rect.height(): | ||||
point = text_edit.mapToGlobal(cursor_rect.topRight()) | ||||
point.setY(point.y() - tip_height) | ||||
self.move(point) | ||||
epatters
|
r2758 | self.show() | ||
epatters
|
r2602 | 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 | ||||
epatters
|
r2744 | #------ Signal handlers ---------------------------------------------------- | ||
def _cursor_position_changed(self): | ||||
epatters
|
r2602 | """ 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() | ||||