param_multival_widget.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. from PySide6.QtCore import (
  2. QAbstractItemModel,
  3. QModelIndex,
  4. Qt,
  5. )
  6. from PySide6.QtWidgets import (
  7. QWidget,
  8. QListWidget,
  9. QListWidgetItem,
  10. QVBoxLayout,
  11. QHBoxLayout,
  12. QStyledItemDelegate,
  13. QStyleOptionViewItem,
  14. QPushButton,
  15. )
  16. from datalad.utils import ensure_list
  17. from .param_widgets import (
  18. GooeyParamWidgetMixin,
  19. load_parameter_widget,
  20. _NoValue,
  21. )
  22. class MyItemDelegate(QStyledItemDelegate):
  23. def __init__(self, mviw, *args, **kwargs):
  24. super().__init__(*args, **kwargs)
  25. self._mviw = mviw
  26. # called on edit request
  27. def createEditor(self,
  28. parent: QWidget,
  29. option: QStyleOptionViewItem,
  30. index: QModelIndex) -> QWidget:
  31. mviw = self._mviw
  32. wid = load_parameter_widget(
  33. parent,
  34. mviw._editor_factory,
  35. name=mviw._gooey_param_name,
  36. docs=mviw._editor_param_docs,
  37. value=getattr(mviw, 'editor_param_value', _NoValue),
  38. default=getattr(mviw, 'editor_param_default', _NoValue),
  39. #validator=mviw._editor_validator,
  40. #allargs=mviw._editor_allargs,
  41. )
  42. # we draw on top of the selected item, and having the highlighting
  43. # "shine" through is not nice
  44. wid.setAutoFillBackground(True)
  45. return wid
  46. # called after createEditor
  47. def updateEditorGeometry(self,
  48. editor: QWidget,
  49. option: QStyleOptionViewItem,
  50. index: QModelIndex) -> None:
  51. # force the editor widget into the item rectangle
  52. editor.setGeometry(option.rect)
  53. # called after updateEditorGeometry
  54. def setEditorData(self, editor: QWidget, index: QModelIndex):
  55. # this requires the inverse of the already existing
  56. # _get_datalad_param_spec() "retriever" methods
  57. editor.set_gooey_param_value(index.data())
  58. # called after editing is done
  59. def setModelData(self,
  60. editor:
  61. QWidget,
  62. model: QAbstractItemModel,
  63. index: QModelIndex):
  64. # this could call the already existing _get_datalad_param_spec()
  65. # "retriever" methods
  66. got_value = False
  67. try:
  68. val = editor.get_gooey_param_value()
  69. got_value = True
  70. except ValueError:
  71. # input widget saw no actual input
  72. pass
  73. if got_value:
  74. # TODO other role than Qt.EditRole?
  75. model.setData(index, val)
  76. class MultiValueInputWidget(QWidget, GooeyParamWidgetMixin):
  77. def __init__(self, editor_factory, *args, **kwargs):
  78. super().__init__(*args, **kwargs)
  79. self._editor_factory = editor_factory
  80. layout = QVBoxLayout()
  81. # tight layout
  82. layout.setContentsMargins(0, 0, 0, 0)
  83. self.setLayout(layout)
  84. # the main list for inputting multiple values
  85. self._lw = QListWidget()
  86. self._lw.setAlternatingRowColors(True)
  87. # we assing the editor factory
  88. self._lw.setItemDelegate(MyItemDelegate(self))
  89. layout.addWidget(self._lw)
  90. # now the buttons
  91. additem_button = QPushButton('+')
  92. additem_button.clicked.connect(self._add_item)
  93. removeitem_button = QPushButton('-')
  94. removeitem_button.clicked.connect(self._remove_item)
  95. button_layout = QHBoxLayout()
  96. button_layout.addWidget(additem_button)
  97. button_layout.addWidget(removeitem_button)
  98. layout.addLayout(button_layout, 1)
  99. self._removeitem_button = removeitem_button
  100. # define initial widget state
  101. # empty by default, nothing to remove
  102. removeitem_button.setDisabled(True)
  103. # with no item present, we can hide everything other than
  104. # the add button to save on space
  105. removeitem_button.hide()
  106. self._lw.hide()
  107. def _add_item(self) -> QListWidgetItem:
  108. newitem = QListWidgetItem(
  109. # must give custom type
  110. type=QListWidgetItem.UserType + 234,
  111. )
  112. # TODO if a value is given, we do not want it to be editable
  113. newitem.setFlags(newitem.flags() | Qt.ItemIsEditable)
  114. # put in list
  115. self._lw.addItem(newitem)
  116. self._lw.setCurrentItem(newitem)
  117. # edit mode, right away TODO unless value specified
  118. self._lw.editItem(newitem)
  119. self._removeitem_button.setDisabled(False)
  120. self._removeitem_button.show()
  121. self._lw.show()
  122. return newitem
  123. def _remove_item(self):
  124. for i in self._lw.selectedItems():
  125. self._lw.takeItem(self._lw.row(i))
  126. if not self._lw.count():
  127. self._removeitem_button.setDisabled(True)
  128. self._removeitem_button.hide()
  129. self._lw.hide()
  130. def set_gooey_param_value(self, value):
  131. # we want to support multi-value setting
  132. for val in ensure_list(value):
  133. item = self._add_item()
  134. # TODO another place where we need to think about the underlying
  135. # role specification
  136. item.setData(Qt.EditRole, val)
  137. def set_gooey_param_default(self, value):
  138. self._editor_param_default = value
  139. def get_gooey_param_value(self):
  140. if not self._lw.count():
  141. # do not report an empty list, when no items have been added.
  142. # setting a value, even by API would have added one
  143. raise ValueError("No items added")
  144. return [
  145. # TODO consider using a different role, here and in setModelData()
  146. self._lw.item(row).data(Qt.EditRole)
  147. for row in range(self._lw.count())
  148. ]
  149. def set_gooey_param_docs(self, docs: str) -> None:
  150. self._editor_param_docs = docs