models_views.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import os.path
  2. from pathlib import Path
  3. from qtpy import QtCore, QtWidgets
  4. from qtpy.QtCore import Qt
  5. class FilenameModel(QtCore.QStringListModel):
  6. """
  7. Model used by QCompleter for file name completions.
  8. Constructor options:
  9. `filter_` (None, 'dirs') - include all entries or folders only
  10. `fs_engine` ('qt', 'pathlib') - enumerate files using `QDir` or `pathlib`
  11. `icon_provider` (func, 'internal', None) - a function which gets path
  12. and returns QIcon
  13. """
  14. def __init__(self, filter_=None, fs_engine='qt', icon_provider='internal'):
  15. super().__init__()
  16. self.current_path = None
  17. self.fs_engine = fs_engine
  18. self.filter = filter_
  19. if icon_provider == 'internal':
  20. self.icons = QtWidgets.QFileIconProvider()
  21. self.icon_provider = self.get_icon
  22. else:
  23. self.icon_provider = icon_provider
  24. def data(self, index, role):
  25. "Get names/icons of files"
  26. default = super().data(index, role)
  27. if role == Qt.DecorationRole and self.icon_provider:
  28. # self.setData(index, dat, role)
  29. return self.icon_provider(super().data(index, Qt.DisplayRole))
  30. if role == Qt.DisplayRole:
  31. return Path(default).name
  32. return default
  33. def get_icon(self, path):
  34. "Internal icon provider"
  35. return self.icons.icon(QtCore.QFileInfo(path))
  36. def get_file_list(self, path):
  37. "List entries in `path` directory"
  38. lst = None
  39. if self.fs_engine == 'pathlib':
  40. lst = self.sort_paths([i for i in path.iterdir()
  41. if self.filter != 'dirs' or i.is_dir()])
  42. elif self.fs_engine == 'qt':
  43. qdir = QtCore.QDir(str(path))
  44. qdir.setFilter(qdir.NoDotAndDotDot | qdir.Hidden |
  45. (qdir.Dirs if self.filter == 'dirs' else qdir.AllEntries))
  46. names = qdir.entryList(sort=QtCore.QDir.DirsFirst |
  47. QtCore.QDir.LocaleAware)
  48. lst = [str(path / i) for i in names]
  49. return lst
  50. @staticmethod
  51. def sort_paths(paths):
  52. "Windows-Explorer-like filename sorting (for 'pathlib' engine)"
  53. dirs, files = [], []
  54. for i in paths:
  55. if i.is_dir():
  56. dirs.append(str(i))
  57. else:
  58. files.append(str(i))
  59. return sorted(dirs, key=str.lower) + sorted(files, key=str.lower)
  60. def setPathPrefix(self, prefix):
  61. path = Path(prefix)
  62. if not prefix.endswith(os.path.sep):
  63. path = path.parent
  64. if path == self.current_path:
  65. return # already listed
  66. if not path.exists():
  67. return # wrong path
  68. self.setStringList(self.get_file_list(path))
  69. self.current_path = path
  70. class MenuListView(QtWidgets.QMenu):
  71. """
  72. QMenu with QListView.
  73. Supports `activated`, `clicked`, `setModel`.
  74. """
  75. max_visible_items = 16
  76. def __init__(self, parent=None):
  77. super().__init__(parent)
  78. self.listview = lv = QtWidgets.QListView()
  79. lv.setFrameShape(lv.NoFrame)
  80. lv.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  81. pal = lv.palette()
  82. pal.setColor(pal.Base, self.palette().color(pal.Window))
  83. lv.setPalette(pal)
  84. act_wgt = QtWidgets.QWidgetAction(self)
  85. act_wgt.setDefaultWidget(lv)
  86. self.addAction(act_wgt)
  87. self.activated = lv.activated
  88. self.clicked = lv.clicked
  89. self.setModel = lv.setModel
  90. lv.sizeHint = self.size_hint
  91. lv.minimumSizeHint = self.size_hint
  92. lv.mousePressEvent = self.mouse_press_event
  93. lv.mouseMoveEvent = self.update_current_index
  94. lv.setMouseTracking(True) # receive mouse move events
  95. lv.leaveEvent = self.clear_selection
  96. lv.mouseReleaseEvent = self.mouse_release_event
  97. lv.keyPressEvent = self.key_press_event
  98. lv.setFocusPolicy(Qt.NoFocus) # no focus rect
  99. lv.setFocus()
  100. self.last_index = QtCore.QModelIndex() # selected index
  101. self.flag_mouse_l_pressed = False
  102. def key_press_event(self, event):
  103. key = event.key()
  104. if key in (Qt.Key_Return, Qt.Key_Enter):
  105. if self.last_index.isValid():
  106. self.activated.emit(self.last_index)
  107. self.close()
  108. elif key == Qt.Key_Escape:
  109. self.close()
  110. elif key in (Qt.Key_Down, Qt.Key_Up):
  111. model = self.listview.model()
  112. row_from, row_to = 0, model.rowCount()-1
  113. if key == Qt.Key_Down:
  114. row_from, row_to = row_to, row_from
  115. if self.last_index.row() in (-1, row_from): # no index=-1
  116. index = model.index(row_to, 0)
  117. else:
  118. shift = 1 if key == Qt.Key_Down else -1
  119. index = model.index(self.last_index.row()+shift, 0)
  120. self.listview.setCurrentIndex(index)
  121. self.last_index = index
  122. def update_current_index(self, event):
  123. self.last_index = self.listview.indexAt(event.pos())
  124. self.listview.setCurrentIndex(self.last_index)
  125. def clear_selection(self, event=None):
  126. self.listview.clearSelection()
  127. # selectionModel().clear() leaves selected item in Fusion theme
  128. self.listview.setCurrentIndex(QtCore.QModelIndex())
  129. self.last_index = QtCore.QModelIndex()
  130. def mouse_press_event(self, event):
  131. if event.button() == Qt.LeftButton:
  132. self.flag_mouse_l_pressed = True
  133. self.update_current_index(event)
  134. def mouse_release_event(self, event):
  135. """
  136. When item is clicked w/ left mouse button close menu, emit `clicked`.
  137. Check if there was left button press event inside this widget.
  138. """
  139. if event.button() == Qt.LeftButton and self.flag_mouse_l_pressed:
  140. self.flag_mouse_l_pressed = False
  141. if self.last_index.isValid():
  142. self.clicked.emit(self.last_index)
  143. self.close()
  144. def size_hint(self):
  145. lv = self.listview
  146. width = lv.sizeHintForColumn(0)
  147. width += lv.verticalScrollBar().sizeHint().width()
  148. if isinstance(self.parent(), QtWidgets.QToolButton):
  149. width = max(width, self.parent().width())
  150. visible_rows = min(self.max_visible_items, lv.model().rowCount())
  151. return QtCore.QSize(width, visible_rows * lv.sizeHintForRow(0))