param_multival_widget.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. from PySide6.QtCore import Qt
  2. from PySide6.QtWidgets import (
  3. QWidget,
  4. QListWidget,
  5. QListWidgetItem,
  6. QVBoxLayout,
  7. QHBoxLayout,
  8. QPushButton,
  9. )
  10. from datalad.utils import ensure_list
  11. from .param_widgets import (
  12. GooeyParamWidgetMixin,
  13. )
  14. from .utils import _NoValue
  15. class MultiValueInputWidget(QWidget, GooeyParamWidgetMixin):
  16. NativeObjectRole = Qt.UserRole + 233
  17. def __init__(self, editor_factory, *args, **kwargs):
  18. super().__init__(*args, **kwargs)
  19. # main layout
  20. layout = QVBoxLayout()
  21. # tight layout
  22. layout.setContentsMargins(0, 0, 0, 0)
  23. self.setLayout(layout)
  24. # define initial widget state
  25. # with no item present, we can hide everything other than
  26. # the add button to save on space
  27. # key component is a persistent editor
  28. editor = editor_factory(parent=self)
  29. self._editor = editor
  30. layout.addWidget(editor)
  31. # underneath the buttons
  32. pb_layout = QHBoxLayout()
  33. layout.addLayout(pb_layout, 0)
  34. for name, label, callback in (
  35. ('_add_pb', 'Add', self._add_item),
  36. ('_update_pb', 'Update', self._update_item),
  37. ('_del_pb', 'Remove', self._del_item),
  38. ):
  39. pb = QPushButton(label)
  40. pb.clicked.connect(callback)
  41. pb_layout.addWidget(pb)
  42. setattr(self, name, pb)
  43. if name != '_add_pb':
  44. pb.setDisabled(True)
  45. # the main list for inputting multiple values
  46. lw = QListWidget()
  47. lw.setAlternatingRowColors(True)
  48. lw.itemChanged.connect(self._load_item_in_editor)
  49. lw.itemSelectionChanged.connect(self._reconfigure_for_selection)
  50. layout.addWidget(lw)
  51. lw.hide()
  52. self._lw = lw
  53. def _reconfigure_for_selection(self):
  54. items = self._lw.selectedItems()
  55. n_items = len(items)
  56. assert n_items < 2
  57. if not n_items:
  58. # nothing selected
  59. self._update_pb.setDisabled(True)
  60. self._del_pb.setDisabled(True)
  61. else:
  62. # we verify that there is only one item
  63. self._load_item_in_editor(items[0])
  64. self._update_pb.setEnabled(True)
  65. def _load_item_in_editor(self, item):
  66. self._editor.init_gooey_from_params({
  67. self._editor._gooey_param_name:
  68. item.data(
  69. MultiValueInputWidget.NativeObjectRole)
  70. })
  71. # * to avoid Qt passing unexpected stuff from signals
  72. def _add_item(self, *, data=_NoValue) -> QListWidgetItem:
  73. newitem = QListWidgetItem(
  74. # must give custom type
  75. type=QListWidgetItem.UserType + 234,
  76. )
  77. self._update_item(item=newitem, data=data)
  78. # put in list
  79. self._lw.addItem(newitem)
  80. self._lw.setCurrentItem(newitem)
  81. self._del_pb.setDisabled(False)
  82. self._del_pb.show()
  83. self._lw.show()
  84. return newitem
  85. def _update_item(self, *, item=None, data=_NoValue):
  86. if item is None:
  87. item = self._lw.selectedItems()
  88. assert len(item)
  89. item = item[0]
  90. if data is _NoValue:
  91. print(self._editor.get_gooey_param_spec())
  92. # TODO avoid the need to use the name
  93. data = self._editor.get_gooey_param_spec().get(
  94. self._gooey_param_name, _NoValue)
  95. # give it a special value if nothing is set
  96. # this helps to populate the edit widget with existing
  97. # values, or not
  98. item.setData(
  99. MultiValueInputWidget.NativeObjectRole,
  100. data)
  101. item.setData(Qt.DisplayRole, _get_item_display(data))
  102. def _del_item(self):
  103. for i in self._lw.selectedItems():
  104. self._lw.takeItem(self._lw.row(i))
  105. if not self._lw.count():
  106. self._del_pb.setDisabled(True)
  107. self._lw.hide()
  108. self._set_gooey_param_value(_NoValue)
  109. def _set_gooey_param_value_in_widget(self, value):
  110. # tabula rasa first, otherwise this would all be
  111. # incremental
  112. self._lw.clear()
  113. # we want to support multi-value setting
  114. for val in ensure_list(value):
  115. self._add_item(data=val)
  116. def _handle_input(self):
  117. val = []
  118. if self._lw.count():
  119. for row in range(self._lw.count()):
  120. item = self._lw.item(row)
  121. val.append(item.data(
  122. MultiValueInputWidget.NativeObjectRole))
  123. val = [v for v in val if v is not _NoValue]
  124. if not val:
  125. # do not report an empty list, when no valid items exist.
  126. # setting a value, even by API would have added one
  127. val = _NoValue
  128. self._set_gooey_param_value(val)
  129. def set_gooey_param_spec(self, name: str, default=_NoValue):
  130. super().set_gooey_param_spec(name, default)
  131. self._editor.set_gooey_param_spec(name, default)
  132. def set_gooey_param_docs(self, docs: str) -> None:
  133. self._editor_param_docs = docs
  134. # the "+" button is always visible. Use it to make the docs accessible
  135. self._add_pb.setToolTip(docs)
  136. def init_gooey_from_params(self, spec):
  137. # first the normal handling
  138. super().init_gooey_from_params(spec)
  139. # for the editor widget, we just keep the union of all reported
  140. # changes, i.e. the latest info for all parameters that ever changed.
  141. # this is then passed to the editor widget, after its creation
  142. self._editor.init_gooey_from_params(spec)
  143. def get_gooey_param_spec(self):
  144. self._handle_input()
  145. # we must override, because we need to handle the cases of list vs
  146. # plain item in default settings.
  147. # TODO This likely needs more work and awareness of `nargs`, see
  148. # https://github.com/datalad/datalad-gooey/issues/212#issuecomment-1256950251
  149. # https://github.com/datalad/datalad-gooey/issues/212#issuecomment-1257170208
  150. val = self._gooey_param_value
  151. default = self._gooey_param_default
  152. if val == default:
  153. val = _NoValue
  154. elif val == [default]:
  155. val = _NoValue
  156. return {self._gooey_param_name: val}
  157. def _get_item_display(value) -> str:
  158. if value is _NoValue:
  159. return '--not set--'
  160. else:
  161. return str(value)