214 lines
8.2 KiB
Python
214 lines
8.2 KiB
Python
"""A dropdown completer widget for the qtconsole."""
|
|
|
|
import os
|
|
import sys
|
|
|
|
from qtpy import QtCore, QtGui, QtWidgets
|
|
|
|
|
|
class CompletionWidget(QtWidgets.QListWidget):
|
|
""" A widget for GUI tab completion.
|
|
"""
|
|
|
|
#--------------------------------------------------------------------------
|
|
# 'QObject' interface
|
|
#--------------------------------------------------------------------------
|
|
|
|
def __init__(self, console_widget):
|
|
""" Create a completion widget that is attached to the specified Qt
|
|
text edit widget.
|
|
"""
|
|
text_edit = console_widget._control
|
|
assert isinstance(text_edit, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit))
|
|
super(CompletionWidget, self).__init__(parent=console_widget)
|
|
|
|
self._text_edit = text_edit
|
|
self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
|
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
|
self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
|
|
|
# We need Popup style to ensure correct mouse interaction
|
|
# (dialog would dissappear on mouse click with ToolTip style)
|
|
self.setWindowFlags(QtCore.Qt.Popup)
|
|
|
|
self.setAttribute(QtCore.Qt.WA_StaticContents)
|
|
original_policy = text_edit.focusPolicy()
|
|
|
|
self.setFocusPolicy(QtCore.Qt.NoFocus)
|
|
text_edit.setFocusPolicy(original_policy)
|
|
|
|
# Ensure that the text edit keeps focus when widget is displayed.
|
|
self.setFocusProxy(self._text_edit)
|
|
|
|
self.setFrameShadow(QtWidgets.QFrame.Plain)
|
|
self.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
|
|
|
self.itemActivated.connect(self._complete_current)
|
|
|
|
def eventFilter(self, obj, event):
|
|
""" Reimplemented to handle mouse input and to auto-hide when the
|
|
text edit loses focus.
|
|
"""
|
|
if obj is self:
|
|
if event.type() == QtCore.QEvent.MouseButtonPress:
|
|
pos = self.mapToGlobal(event.pos())
|
|
target = QtWidgets.QApplication.widgetAt(pos)
|
|
if (target and self.isAncestorOf(target) or target is self):
|
|
return False
|
|
else:
|
|
self.cancel_completion()
|
|
|
|
return super(CompletionWidget, self).eventFilter(obj, event)
|
|
|
|
def keyPressEvent(self, event):
|
|
key = event.key()
|
|
if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
|
|
QtCore.Qt.Key_Tab):
|
|
self._complete_current()
|
|
elif key == QtCore.Qt.Key_Escape:
|
|
self.hide()
|
|
elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
|
|
QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
|
|
QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
|
|
return super(CompletionWidget, self).keyPressEvent(event)
|
|
else:
|
|
QtWidgets.QApplication.sendEvent(self._text_edit, event)
|
|
|
|
#--------------------------------------------------------------------------
|
|
# 'QWidget' interface
|
|
#--------------------------------------------------------------------------
|
|
|
|
def hideEvent(self, event):
|
|
""" Reimplemented to disconnect signal handlers and event filter.
|
|
"""
|
|
super(CompletionWidget, self).hideEvent(event)
|
|
try:
|
|
self._text_edit.cursorPositionChanged.disconnect(self._update_current)
|
|
except TypeError:
|
|
pass
|
|
self.removeEventFilter(self)
|
|
|
|
def showEvent(self, event):
|
|
""" Reimplemented to connect signal handlers and event filter.
|
|
"""
|
|
super(CompletionWidget, self).showEvent(event)
|
|
self._text_edit.cursorPositionChanged.connect(self._update_current)
|
|
self.installEventFilter(self)
|
|
|
|
#--------------------------------------------------------------------------
|
|
# 'CompletionWidget' interface
|
|
#--------------------------------------------------------------------------
|
|
|
|
def show_items(self, cursor, items, prefix_length=0):
|
|
""" Shows the completion widget with 'items' at the position specified
|
|
by 'cursor'.
|
|
"""
|
|
text_edit = self._text_edit
|
|
point = self._get_top_left_position(cursor)
|
|
self.clear()
|
|
path_items = []
|
|
for item in items:
|
|
# Check if the item could refer to a file or dir. The replacing
|
|
# of '"' is needed for items on Windows
|
|
if (os.path.isfile(os.path.abspath(item.replace("\"", ""))) or
|
|
os.path.isdir(os.path.abspath(item.replace("\"", "")))):
|
|
path_items.append(item.replace("\"", ""))
|
|
else:
|
|
list_item = QtWidgets.QListWidgetItem()
|
|
list_item.setData(QtCore.Qt.UserRole, item)
|
|
# Need to split to only show last element of a dot completion
|
|
list_item.setText(item.split(".")[-1])
|
|
self.addItem(list_item)
|
|
|
|
common_prefix = os.path.dirname(os.path.commonprefix(path_items))
|
|
for path_item in path_items:
|
|
list_item = QtWidgets.QListWidgetItem()
|
|
list_item.setData(QtCore.Qt.UserRole, path_item)
|
|
if common_prefix:
|
|
text = path_item.split(common_prefix)[-1]
|
|
else:
|
|
text = path_item
|
|
list_item.setText(text)
|
|
self.addItem(list_item)
|
|
|
|
height = self.sizeHint().height()
|
|
screen_rect = QtWidgets.QApplication.desktop().availableGeometry(self)
|
|
if (screen_rect.size().height() + screen_rect.y() -
|
|
point.y() - height < 0):
|
|
point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
|
|
point.setY(point.y() - height)
|
|
w = (self.sizeHintForColumn(0) +
|
|
self.verticalScrollBar().sizeHint().width() +
|
|
2 * self.frameWidth())
|
|
self.setGeometry(point.x(), point.y(), w, height)
|
|
|
|
# Move cursor to start of the prefix to replace it
|
|
# when a item is selected
|
|
cursor.movePosition(QtGui.QTextCursor.Left, n=prefix_length)
|
|
self._start_position = cursor.position()
|
|
self.setCurrentRow(0)
|
|
self.raise_()
|
|
self.show()
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Protected interface
|
|
#--------------------------------------------------------------------------
|
|
|
|
def _get_top_left_position(self, cursor):
|
|
""" Get top left position for this widget.
|
|
"""
|
|
point = self._text_edit.cursorRect(cursor).center()
|
|
point_size = self._text_edit.font().pointSize()
|
|
|
|
if sys.platform == 'darwin':
|
|
delta = int((point_size * 1.20) ** 0.98)
|
|
elif os.name == 'nt':
|
|
delta = int((point_size * 1.20) ** 1.05)
|
|
else:
|
|
delta = int((point_size * 1.20) ** 0.98)
|
|
|
|
y = delta - (point_size / 2)
|
|
point.setY(point.y() + y)
|
|
point = self._text_edit.mapToGlobal(point)
|
|
return point
|
|
|
|
def _complete_current(self):
|
|
""" Perform the completion with the currently selected item.
|
|
"""
|
|
text = self.currentItem().data(QtCore.Qt.UserRole)
|
|
self._current_text_cursor().insertText(text)
|
|
self.hide()
|
|
|
|
def _current_text_cursor(self):
|
|
""" Returns a cursor with text between the start position and the
|
|
current position selected.
|
|
"""
|
|
cursor = self._text_edit.textCursor()
|
|
if cursor.position() >= self._start_position:
|
|
cursor.setPosition(self._start_position,
|
|
QtGui.QTextCursor.KeepAnchor)
|
|
return cursor
|
|
|
|
def _update_current(self):
|
|
""" Updates the current item based on the current text and the
|
|
position of the widget.
|
|
"""
|
|
# Update widget position
|
|
cursor = self._text_edit.textCursor()
|
|
point = self._get_top_left_position(cursor)
|
|
self.move(point)
|
|
|
|
# Update current item
|
|
prefix = self._current_text_cursor().selection().toPlainText()
|
|
if prefix:
|
|
items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
|
|
QtCore.Qt.MatchCaseSensitive))
|
|
if items:
|
|
self.setCurrentItem(items[0])
|
|
else:
|
|
self.hide()
|
|
else:
|
|
self.hide()
|
|
|
|
def cancel_completion(self):
|
|
self.hide()
|