completion_widget.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. """A dropdown completer widget for the qtconsole."""
  2. import os
  3. import sys
  4. from qtpy import QtCore, QtGui, QtWidgets
  5. class CompletionWidget(QtWidgets.QListWidget):
  6. """ A widget for GUI tab completion.
  7. """
  8. #--------------------------------------------------------------------------
  9. # 'QObject' interface
  10. #--------------------------------------------------------------------------
  11. def __init__(self, console_widget):
  12. """ Create a completion widget that is attached to the specified Qt
  13. text edit widget.
  14. """
  15. text_edit = console_widget._control
  16. assert isinstance(text_edit, (QtWidgets.QTextEdit, QtWidgets.QPlainTextEdit))
  17. super(CompletionWidget, self).__init__(parent=console_widget)
  18. self._text_edit = text_edit
  19. self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
  20. self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
  21. self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
  22. # We need Popup style to ensure correct mouse interaction
  23. # (dialog would dissappear on mouse click with ToolTip style)
  24. self.setWindowFlags(QtCore.Qt.Popup)
  25. self.setAttribute(QtCore.Qt.WA_StaticContents)
  26. original_policy = text_edit.focusPolicy()
  27. self.setFocusPolicy(QtCore.Qt.NoFocus)
  28. text_edit.setFocusPolicy(original_policy)
  29. # Ensure that the text edit keeps focus when widget is displayed.
  30. self.setFocusProxy(self._text_edit)
  31. self.setFrameShadow(QtWidgets.QFrame.Plain)
  32. self.setFrameShape(QtWidgets.QFrame.StyledPanel)
  33. self.itemActivated.connect(self._complete_current)
  34. def eventFilter(self, obj, event):
  35. """ Reimplemented to handle mouse input and to auto-hide when the
  36. text edit loses focus.
  37. """
  38. if obj is self:
  39. if event.type() == QtCore.QEvent.MouseButtonPress:
  40. pos = self.mapToGlobal(event.pos())
  41. target = QtWidgets.QApplication.widgetAt(pos)
  42. if (target and self.isAncestorOf(target) or target is self):
  43. return False
  44. else:
  45. self.cancel_completion()
  46. return super(CompletionWidget, self).eventFilter(obj, event)
  47. def keyPressEvent(self, event):
  48. key = event.key()
  49. if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
  50. QtCore.Qt.Key_Tab):
  51. self._complete_current()
  52. elif key == QtCore.Qt.Key_Escape:
  53. self.hide()
  54. elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
  55. QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
  56. QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
  57. return super(CompletionWidget, self).keyPressEvent(event)
  58. else:
  59. QtWidgets.QApplication.sendEvent(self._text_edit, event)
  60. #--------------------------------------------------------------------------
  61. # 'QWidget' interface
  62. #--------------------------------------------------------------------------
  63. def hideEvent(self, event):
  64. """ Reimplemented to disconnect signal handlers and event filter.
  65. """
  66. super(CompletionWidget, self).hideEvent(event)
  67. try:
  68. self._text_edit.cursorPositionChanged.disconnect(self._update_current)
  69. except TypeError:
  70. pass
  71. self.removeEventFilter(self)
  72. def showEvent(self, event):
  73. """ Reimplemented to connect signal handlers and event filter.
  74. """
  75. super(CompletionWidget, self).showEvent(event)
  76. self._text_edit.cursorPositionChanged.connect(self._update_current)
  77. self.installEventFilter(self)
  78. #--------------------------------------------------------------------------
  79. # 'CompletionWidget' interface
  80. #--------------------------------------------------------------------------
  81. def show_items(self, cursor, items, prefix_length=0):
  82. """ Shows the completion widget with 'items' at the position specified
  83. by 'cursor'.
  84. """
  85. text_edit = self._text_edit
  86. point = self._get_top_left_position(cursor)
  87. self.clear()
  88. path_items = []
  89. for item in items:
  90. # Check if the item could refer to a file or dir. The replacing
  91. # of '"' is needed for items on Windows
  92. if (os.path.isfile(os.path.abspath(item.replace("\"", ""))) or
  93. os.path.isdir(os.path.abspath(item.replace("\"", "")))):
  94. path_items.append(item.replace("\"", ""))
  95. else:
  96. list_item = QtWidgets.QListWidgetItem()
  97. list_item.setData(QtCore.Qt.UserRole, item)
  98. # Need to split to only show last element of a dot completion
  99. list_item.setText(item.split(".")[-1])
  100. self.addItem(list_item)
  101. common_prefix = os.path.dirname(os.path.commonprefix(path_items))
  102. for path_item in path_items:
  103. list_item = QtWidgets.QListWidgetItem()
  104. list_item.setData(QtCore.Qt.UserRole, path_item)
  105. if common_prefix:
  106. text = path_item.split(common_prefix)[-1]
  107. else:
  108. text = path_item
  109. list_item.setText(text)
  110. self.addItem(list_item)
  111. height = self.sizeHint().height()
  112. screen_rect = QtWidgets.QApplication.desktop().availableGeometry(self)
  113. if (screen_rect.size().height() + screen_rect.y() -
  114. point.y() - height < 0):
  115. point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
  116. point.setY(point.y() - height)
  117. w = (self.sizeHintForColumn(0) +
  118. self.verticalScrollBar().sizeHint().width() +
  119. 2 * self.frameWidth())
  120. self.setGeometry(point.x(), point.y(), w, height)
  121. # Move cursor to start of the prefix to replace it
  122. # when a item is selected
  123. cursor.movePosition(QtGui.QTextCursor.Left, n=prefix_length)
  124. self._start_position = cursor.position()
  125. self.setCurrentRow(0)
  126. self.raise_()
  127. self.show()
  128. #--------------------------------------------------------------------------
  129. # Protected interface
  130. #--------------------------------------------------------------------------
  131. def _get_top_left_position(self, cursor):
  132. """ Get top left position for this widget.
  133. """
  134. point = self._text_edit.cursorRect(cursor).center()
  135. point_size = self._text_edit.font().pointSize()
  136. if sys.platform == 'darwin':
  137. delta = int((point_size * 1.20) ** 0.98)
  138. elif os.name == 'nt':
  139. delta = int((point_size * 1.20) ** 1.05)
  140. else:
  141. delta = int((point_size * 1.20) ** 0.98)
  142. y = delta - (point_size / 2)
  143. point.setY(point.y() + y)
  144. point = self._text_edit.mapToGlobal(point)
  145. return point
  146. def _complete_current(self):
  147. """ Perform the completion with the currently selected item.
  148. """
  149. text = self.currentItem().data(QtCore.Qt.UserRole)
  150. self._current_text_cursor().insertText(text)
  151. self.hide()
  152. def _current_text_cursor(self):
  153. """ Returns a cursor with text between the start position and the
  154. current position selected.
  155. """
  156. cursor = self._text_edit.textCursor()
  157. if cursor.position() >= self._start_position:
  158. cursor.setPosition(self._start_position,
  159. QtGui.QTextCursor.KeepAnchor)
  160. return cursor
  161. def _update_current(self):
  162. """ Updates the current item based on the current text and the
  163. position of the widget.
  164. """
  165. # Update widget position
  166. cursor = self._text_edit.textCursor()
  167. point = self._get_top_left_position(cursor)
  168. self.move(point)
  169. # Update current item
  170. prefix = self._current_text_cursor().selection().toPlainText()
  171. if prefix:
  172. items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
  173. QtCore.Qt.MatchCaseSensitive))
  174. if items:
  175. self.setCurrentItem(items[0])
  176. else:
  177. self.hide()
  178. else:
  179. self.hide()
  180. def cancel_completion(self):
  181. self.hide()