"""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()