call_tip_widget.py
220 lines
| 8.2 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 | #-------------------------------------------------------------------------- | ||
epatters
|
r2982 | def __init__(self, text_edit): | ||
epatters
|
r2602 | """ Create a call tip manager that is attached to the specified Qt | ||
text edit widget. | ||||
""" | ||||
epatters
|
r2982 | assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) | ||
super(CallTipWidget, self).__init__(None, QtCore.Qt.ToolTip) | ||||
epatters
|
r2602 | |||
epatters
|
r2964 | self._hide_timer = QtCore.QBasicTimer() | ||
epatters
|
r2982 | self._text_edit = text_edit | ||
epatters
|
r2964 | |||
epatters
|
r2982 | self.setFont(text_edit.document().defaultFont()) | ||
epatters
|
r2602 | 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): | ||
epatters
|
r2982 | """ Reimplemented to hide on certain key presses and on text edit focus | ||
epatters
|
r2744 | changes. | ||
""" | ||||
epatters
|
r2982 | if obj == self._text_edit: | ||
epatters
|
r2744 | etype = event.type() | ||
epatters
|
r2962 | |||
if etype == QtCore.QEvent.KeyPress: | ||||
key = event.key() | ||||
if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): | ||||
self.hide() | ||||
elif key == QtCore.Qt.Key_Escape: | ||||
self.hide() | ||||
return True | ||||
epatters
|
r2744 | elif etype == QtCore.QEvent.FocusOut: | ||
self.hide() | ||||
epatters
|
r2964 | elif etype == QtCore.QEvent.Enter: | ||
self._hide_timer.stop() | ||||
elif etype == QtCore.QEvent.Leave: | ||||
self._hide_later() | ||||
epatters
|
r2982 | return super(CallTipWidget, self).eventFilter(obj, event) | ||
epatters
|
r2744 | |||
epatters
|
r2964 | def timerEvent(self, event): | ||
""" Reimplemented to hide the widget when the hide timer fires. | ||||
""" | ||||
if event.timerId() == self._hide_timer.timerId(): | ||||
self._hide_timer.stop() | ||||
self.hide() | ||||
epatters
|
r2687 | #-------------------------------------------------------------------------- | ||
# 'QWidget' interface | ||||
#-------------------------------------------------------------------------- | ||||
epatters
|
r2964 | def enterEvent(self, event): | ||
""" Reimplemented to cancel the hide timer. | ||||
""" | ||||
epatters
|
r2982 | super(CallTipWidget, self).enterEvent(event) | ||
epatters
|
r2964 | self._hide_timer.stop() | ||
epatters
|
r2602 | def hideEvent(self, event): | ||
epatters
|
r2744 | """ Reimplemented to disconnect signal handlers and event filter. | ||
epatters
|
r2602 | """ | ||
epatters
|
r2982 | super(CallTipWidget, self).hideEvent(event) | ||
self._text_edit.cursorPositionChanged.disconnect( | ||||
self._cursor_position_changed) | ||||
self._text_edit.removeEventFilter(self) | ||||
epatters
|
r2687 | |||
epatters
|
r2964 | def leaveEvent(self, event): | ||
""" Reimplemented to start the hide timer. | ||||
""" | ||||
epatters
|
r2982 | super(CallTipWidget, self).leaveEvent(event) | ||
epatters
|
r2964 | self._hide_later() | ||
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() | ||||
epatters
|
r2982 | super(CallTipWidget, self).paintEvent(event) | ||
epatters
|
r2602 | |||
epatters
|
r3031 | def setFont(self, font): | ||
""" Reimplemented to allow use of this method as a slot. | ||||
""" | ||||
super(CallTipWidget, self).setFont(font) | ||||
epatters
|
r2602 | def showEvent(self, event): | ||
epatters
|
r2744 | """ Reimplemented to connect signal handlers and event filter. | ||
epatters
|
r2602 | """ | ||
epatters
|
r2982 | super(CallTipWidget, self).showEvent(event) | ||
self._text_edit.cursorPositionChanged.connect( | ||||
self._cursor_position_changed) | ||||
self._text_edit.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
|
r2982 | text_edit = self._text_edit | ||
epatters
|
r2602 | 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. | ||||
epatters
|
r2963 | padding = 3 # Distance in pixels between cursor bounds and tip box. | ||
epatters
|
r2881 | cursor_rect = text_edit.cursorRect(cursor) | ||
screen_rect = QtGui.qApp.desktop().screenGeometry(text_edit) | ||||
point = text_edit.mapToGlobal(cursor_rect.bottomRight()) | ||||
epatters
|
r2963 | point.setY(point.y() + padding) | ||
epatters
|
r2881 | tip_height = self.size().height() | ||
if point.y() + tip_height > screen_rect.height(): | ||||
point = text_edit.mapToGlobal(cursor_rect.topRight()) | ||||
epatters
|
r2963 | point.setY(point.y() - tip_height - padding) | ||
epatters
|
r2881 | 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 | ||||
epatters
|
r2982 | document = self._text_edit.document() | ||
epatters
|
r2602 | 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
|
r2964 | def _hide_later(self): | ||
""" Hides the tooltip after some time has passed. | ||||
""" | ||||
if not self._hide_timer.isActive(): | ||||
self._hide_timer.start(300, self) | ||||
epatters
|
r2744 | #------ Signal handlers ---------------------------------------------------- | ||
def _cursor_position_changed(self): | ||||
epatters
|
r2602 | """ Updates the tip based on user cursor movement. | ||
""" | ||||
epatters
|
r2982 | cursor = self._text_edit.textCursor() | ||
epatters
|
r2602 | if cursor.position() <= self._start_position: | ||
self.hide() | ||||
else: | ||||
position, commas = self._find_parenthesis(self._start_position + 1) | ||||
if position != -1: | ||||
self.hide() | ||||