param_multival_widget.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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. )
  21. from .utils import _NoValue
  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. default=getattr(mviw, 'editor_param_default', _NoValue),
  38. #validator=mviw._editor_validator,
  39. )
  40. value = getattr(mviw, 'editor_param_value', _NoValue)
  41. if value != _NoValue:
  42. wid.set_gooey_param_value(value)
  43. wid.init_gooey_from_other_param(mviw._editor_init)
  44. # we draw on top of the selected item, and having the highlighting
  45. # "shine" through is not nice
  46. wid.setAutoFillBackground(True)
  47. return wid
  48. # called after createEditor
  49. def updateEditorGeometry(self,
  50. editor: QWidget,
  51. option: QStyleOptionViewItem,
  52. index: QModelIndex) -> None:
  53. # force the editor widget into the item rectangle
  54. editor.setGeometry(option.rect)
  55. # called after updateEditorGeometry
  56. def setEditorData(self, editor: QWidget, index: QModelIndex):
  57. # this requires the inverse of the already existing
  58. # _get_datalad_param_spec() "retriever" methods
  59. data = index.data()
  60. if data is not _NoValue:
  61. editor.set_gooey_param_value(data)
  62. # called after editing is done
  63. def setModelData(self,
  64. editor:
  65. QWidget,
  66. model: QAbstractItemModel,
  67. index: QModelIndex):
  68. # this could call the already existing _get_datalad_param_spec()
  69. # "retriever" methods
  70. got_value = False
  71. try:
  72. val = editor.get_gooey_param_value()
  73. got_value = True
  74. except ValueError:
  75. # input widget saw no actual input
  76. pass
  77. if got_value:
  78. # TODO other role than Qt.EditRole?
  79. model.setData(index, val)
  80. class MultiValueInputWidget(QWidget, GooeyParamWidgetMixin):
  81. def __init__(self, editor_factory, *args, **kwargs):
  82. super().__init__(*args, **kwargs)
  83. self._editor_factory = editor_factory
  84. # maintained via init_gooey_from_other_param()
  85. self._editor_init = dict()
  86. layout = QVBoxLayout()
  87. # tight layout
  88. layout.setContentsMargins(0, 0, 0, 0)
  89. self.setLayout(layout)
  90. # the main list for inputting multiple values
  91. self._lw = QListWidget()
  92. self._lw.setAlternatingRowColors(True)
  93. # we assing the editor factory
  94. self._lw.setItemDelegate(MyItemDelegate(self))
  95. self._lw.setToolTip('Double-click to edit items')
  96. layout.addWidget(self._lw)
  97. # now the buttons
  98. additem_button = QPushButton('+')
  99. additem_button.clicked.connect(self._add_item)
  100. removeitem_button = QPushButton('-')
  101. removeitem_button.clicked.connect(self._remove_item)
  102. button_layout = QHBoxLayout()
  103. button_layout.addWidget(additem_button)
  104. button_layout.addWidget(removeitem_button)
  105. layout.addLayout(button_layout, 1)
  106. self._additem_button = additem_button
  107. self._removeitem_button = removeitem_button
  108. # define initial widget state
  109. # empty by default, nothing to remove
  110. removeitem_button.setDisabled(True)
  111. # with no item present, we can hide everything other than
  112. # the add button to save on space
  113. removeitem_button.hide()
  114. self._lw.hide()
  115. def _add_item(self) -> QListWidgetItem:
  116. newitem = QListWidgetItem(
  117. # must give custom type
  118. type=QListWidgetItem.UserType + 234,
  119. )
  120. # TODO if a value is given, we do not want it to be editable
  121. newitem.setFlags(newitem.flags() | Qt.ItemIsEditable)
  122. # give it a special value if nothing is set
  123. # this helps to populate the edit widget with existing
  124. # values, or not
  125. newitem.setData(Qt.EditRole, _NoValue)
  126. # put in list
  127. self._lw.addItem(newitem)
  128. self._lw.setCurrentItem(newitem)
  129. # edit mode, right away TODO unless value specified
  130. self._lw.editItem(newitem)
  131. self._removeitem_button.setDisabled(False)
  132. self._removeitem_button.show()
  133. self._lw.show()
  134. return newitem
  135. def _remove_item(self):
  136. for i in self._lw.selectedItems():
  137. self._lw.takeItem(self._lw.row(i))
  138. if not self._lw.count():
  139. self._removeitem_button.setDisabled(True)
  140. self._removeitem_button.hide()
  141. self._lw.hide()
  142. def _set_gooey_param_value(self, value):
  143. # we want to support multi-value setting
  144. for val in ensure_list(value):
  145. item = self._add_item()
  146. # TODO another place where we need to think about the underlying
  147. # role specification
  148. item.setData(Qt.EditRole, val)
  149. def set_gooey_param_default(self, value):
  150. self._editor_param_default = value
  151. def get_gooey_param_value(self):
  152. if not self._lw.count():
  153. # do not report an empty list, when no items have been added.
  154. # setting a value, even by API would have added one
  155. raise ValueError("No items added")
  156. return [
  157. # TODO consider using a different role, here and in setModelData()
  158. self._lw.item(row).data(Qt.EditRole)
  159. for row in range(self._lw.count())
  160. ]
  161. def set_gooey_param_docs(self, docs: str) -> None:
  162. self._editor_param_docs = docs
  163. # the "+" button is always visible. Use it to make the docs accessible
  164. self._additem_button.setToolTip(docs)
  165. def init_gooey_from_other_param(self, spec):
  166. # we just keep the union of all reported changes, i.e.
  167. # the latest info for all parameters that ever changed.
  168. # this is then passed to the editor widget, after its
  169. # creation
  170. self._editor_init.update(spec)