|
@@ -1,122 +1,84 @@
|
|
|
-from PySide6.QtCore import (
|
|
|
- QAbstractItemModel,
|
|
|
- QModelIndex,
|
|
|
- Qt,
|
|
|
-)
|
|
|
+from PySide6.QtCore import Qt
|
|
|
from PySide6.QtWidgets import (
|
|
|
QWidget,
|
|
|
QListWidget,
|
|
|
QListWidgetItem,
|
|
|
QVBoxLayout,
|
|
|
QHBoxLayout,
|
|
|
- QStyledItemDelegate,
|
|
|
- QStyleOptionViewItem,
|
|
|
QPushButton,
|
|
|
)
|
|
|
|
|
|
from datalad.utils import ensure_list
|
|
|
from .param_widgets import (
|
|
|
GooeyParamWidgetMixin,
|
|
|
- load_parameter_widget,
|
|
|
)
|
|
|
from .utils import _NoValue
|
|
|
|
|
|
|
|
|
-class MyItemDelegate(QStyledItemDelegate):
|
|
|
- def __init__(self, mviw, *args, **kwargs):
|
|
|
- super().__init__(*args, **kwargs)
|
|
|
- self._mviw = mviw
|
|
|
-
|
|
|
- # called on edit request
|
|
|
- def createEditor(self,
|
|
|
- parent: QWidget,
|
|
|
- option: QStyleOptionViewItem,
|
|
|
- index: QModelIndex) -> QWidget:
|
|
|
- mviw = self._mviw
|
|
|
- wid = load_parameter_widget(
|
|
|
- parent,
|
|
|
- mviw._editor_factory,
|
|
|
- name=mviw._gooey_param_name,
|
|
|
- docs=mviw._editor_param_docs,
|
|
|
- default=getattr(mviw, 'editor_param_default', _NoValue),
|
|
|
- #validator=mviw._editor_validator,
|
|
|
- )
|
|
|
- # we draw on top of the selected item, and having the highlighting
|
|
|
- # "shine" through is not nice
|
|
|
- wid.setAutoFillBackground(True)
|
|
|
- return wid
|
|
|
-
|
|
|
- # called after createEditor
|
|
|
- def updateEditorGeometry(self,
|
|
|
- editor: QWidget,
|
|
|
- option: QStyleOptionViewItem,
|
|
|
- index: QModelIndex) -> None:
|
|
|
- # force the editor widget into the item rectangle
|
|
|
- editor.setGeometry(option.rect)
|
|
|
-
|
|
|
- # called after updateEditorGeometry
|
|
|
- def setEditorData(self, editor: QWidget, index: QModelIndex):
|
|
|
- # this requires the inverse of the already existing
|
|
|
- # _get_datalad_param_spec() "retriever" methods
|
|
|
- edit_init = dict(
|
|
|
- self._mviw._editor_init,
|
|
|
- # TODO use custom role for actual dtype
|
|
|
- **{editor._gooey_param_name: index.data(
|
|
|
- MultiValueInputWidget.NativeObjectRole)}
|
|
|
- )
|
|
|
- editor.init_gooey_from_params(edit_init)
|
|
|
-
|
|
|
- # called after editing is done
|
|
|
- def setModelData(self,
|
|
|
- editor: QWidget,
|
|
|
- model: QAbstractItemModel,
|
|
|
- index: QModelIndex):
|
|
|
- val = editor.get_gooey_param_spec()[editor._gooey_param_name]
|
|
|
- model.setData(index, val, MultiValueInputWidget.NativeObjectRole)
|
|
|
- model.setData(index, _get_item_display(val), Qt.DisplayRole)
|
|
|
-
|
|
|
-
|
|
|
class MultiValueInputWidget(QWidget, GooeyParamWidgetMixin):
|
|
|
NativeObjectRole = Qt.UserRole + 233
|
|
|
|
|
|
def __init__(self, editor_factory, *args, **kwargs):
|
|
|
super().__init__(*args, **kwargs)
|
|
|
- self._editor_factory = editor_factory
|
|
|
- # maintained via init_gooey_from_params()
|
|
|
- self._editor_init = dict()
|
|
|
|
|
|
+ # main layout
|
|
|
layout = QVBoxLayout()
|
|
|
# tight layout
|
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
self.setLayout(layout)
|
|
|
- # the main list for inputting multiple values
|
|
|
- self._lw = QListWidget()
|
|
|
- self._lw.setAlternatingRowColors(True)
|
|
|
- # we assing the editor factory
|
|
|
- self._lw.setItemDelegate(MyItemDelegate(self))
|
|
|
- self._lw.setToolTip('Double-click to edit items')
|
|
|
- layout.addWidget(self._lw)
|
|
|
-
|
|
|
- # now the buttons
|
|
|
- additem_button = QPushButton('+')
|
|
|
- additem_button.clicked.connect(self._add_item)
|
|
|
- removeitem_button = QPushButton('-')
|
|
|
- removeitem_button.clicked.connect(self._remove_item)
|
|
|
- button_layout = QHBoxLayout()
|
|
|
- button_layout.addWidget(additem_button)
|
|
|
- button_layout.addWidget(removeitem_button)
|
|
|
- layout.addLayout(button_layout, 1)
|
|
|
-
|
|
|
- self._additem_button = additem_button
|
|
|
- self._removeitem_button = removeitem_button
|
|
|
|
|
|
# define initial widget state
|
|
|
- # empty by default, nothing to remove
|
|
|
- removeitem_button.setDisabled(True)
|
|
|
# with no item present, we can hide everything other than
|
|
|
# the add button to save on space
|
|
|
- removeitem_button.hide()
|
|
|
- self._lw.hide()
|
|
|
+
|
|
|
+ # key component is a persistent editor
|
|
|
+ editor = editor_factory(parent=self)
|
|
|
+ self._editor = editor
|
|
|
+ layout.addWidget(editor)
|
|
|
+
|
|
|
+ # underneath the buttons
|
|
|
+ pb_layout = QHBoxLayout()
|
|
|
+ layout.addLayout(pb_layout, 0)
|
|
|
+ for name, label, callback in (
|
|
|
+ ('_add_pb', 'Add', self._add_item),
|
|
|
+ ('_update_pb', 'Update', self._update_item),
|
|
|
+ ('_del_pb', 'Remove', self._del_item),
|
|
|
+ ):
|
|
|
+ pb = QPushButton(label)
|
|
|
+ pb.clicked.connect(callback)
|
|
|
+ pb_layout.addWidget(pb)
|
|
|
+ setattr(self, name, pb)
|
|
|
+ if name != '_add_pb':
|
|
|
+ pb.setDisabled(True)
|
|
|
+
|
|
|
+ # the main list for inputting multiple values
|
|
|
+ lw = QListWidget()
|
|
|
+ lw.setAlternatingRowColors(True)
|
|
|
+ lw.itemChanged.connect(self._load_item_in_editor)
|
|
|
+ lw.itemSelectionChanged.connect(self._reconfigure_for_selection)
|
|
|
+ layout.addWidget(lw)
|
|
|
+ lw.hide()
|
|
|
+ self._lw = lw
|
|
|
+
|
|
|
+ def _reconfigure_for_selection(self):
|
|
|
+ items = self._lw.selectedItems()
|
|
|
+ n_items = len(items)
|
|
|
+ assert n_items < 2
|
|
|
+ if not n_items:
|
|
|
+ # nothing selected
|
|
|
+ self._update_pb.setDisabled(True)
|
|
|
+ self._del_pb.setDisabled(True)
|
|
|
+ else:
|
|
|
+ # we verify that there is only one item
|
|
|
+ self._load_item_in_editor(items[0])
|
|
|
+ self._update_pb.setEnabled(True)
|
|
|
+
|
|
|
+ def _load_item_in_editor(self, item):
|
|
|
+ self._editor.init_gooey_from_params({
|
|
|
+ self._editor._gooey_param_name:
|
|
|
+ item.data(
|
|
|
+ MultiValueInputWidget.NativeObjectRole)
|
|
|
+ })
|
|
|
|
|
|
# * to avoid Qt passing unexpected stuff from signals
|
|
|
def _add_item(self, *, data=_NoValue) -> QListWidgetItem:
|
|
@@ -124,32 +86,38 @@ class MultiValueInputWidget(QWidget, GooeyParamWidgetMixin):
|
|
|
# must give custom type
|
|
|
type=QListWidgetItem.UserType + 234,
|
|
|
)
|
|
|
- newitem.setFlags(newitem.flags() | Qt.ItemIsEditable)
|
|
|
- # give it a special value if nothing is set
|
|
|
- # this helps to populate the edit widget with existing
|
|
|
- # values, or not
|
|
|
- newitem.setData(
|
|
|
- MultiValueInputWidget.NativeObjectRole,
|
|
|
- data)
|
|
|
- newitem.setData(Qt.DisplayRole, _get_item_display(data))
|
|
|
-
|
|
|
+ self._update_item(item=newitem, data=data)
|
|
|
# put in list
|
|
|
self._lw.addItem(newitem)
|
|
|
self._lw.setCurrentItem(newitem)
|
|
|
- # edit mode, right away TODO unless value specified
|
|
|
- self._lw.editItem(newitem)
|
|
|
- self._removeitem_button.setDisabled(False)
|
|
|
- self._removeitem_button.show()
|
|
|
+ self._del_pb.setDisabled(False)
|
|
|
+ self._del_pb.show()
|
|
|
self._lw.show()
|
|
|
-
|
|
|
return newitem
|
|
|
|
|
|
- def _remove_item(self):
|
|
|
+ def _update_item(self, *, item=None, data=_NoValue):
|
|
|
+ if item is None:
|
|
|
+ item = self._lw.selectedItems()
|
|
|
+ assert len(item)
|
|
|
+ item = item[0]
|
|
|
+ if data is _NoValue:
|
|
|
+ print(self._editor.get_gooey_param_spec())
|
|
|
+ # TODO avoid the need to use the name
|
|
|
+ data = self._editor.get_gooey_param_spec().get(
|
|
|
+ self._gooey_param_name, _NoValue)
|
|
|
+ # give it a special value if nothing is set
|
|
|
+ # this helps to populate the edit widget with existing
|
|
|
+ # values, or not
|
|
|
+ item.setData(
|
|
|
+ MultiValueInputWidget.NativeObjectRole,
|
|
|
+ data)
|
|
|
+ item.setData(Qt.DisplayRole, _get_item_display(data))
|
|
|
+
|
|
|
+ def _del_item(self):
|
|
|
for i in self._lw.selectedItems():
|
|
|
self._lw.takeItem(self._lw.row(i))
|
|
|
if not self._lw.count():
|
|
|
- self._removeitem_button.setDisabled(True)
|
|
|
- self._removeitem_button.hide()
|
|
|
+ self._del_pb.setDisabled(True)
|
|
|
self._lw.hide()
|
|
|
self._set_gooey_param_value(_NoValue)
|
|
|
|
|
@@ -166,17 +134,8 @@ class MultiValueInputWidget(QWidget, GooeyParamWidgetMixin):
|
|
|
if self._lw.count():
|
|
|
for row in range(self._lw.count()):
|
|
|
item = self._lw.item(row)
|
|
|
- wid = self._lw.itemWidget(item)
|
|
|
- if wid:
|
|
|
- # an editor is still open, retrieve from editor directly.
|
|
|
- # just append, _NoValue handling is below
|
|
|
- val.append(wid._gooey_param_value)
|
|
|
- else:
|
|
|
- # TODO consider using a different role, here and in
|
|
|
- # setModelData()
|
|
|
- # TODO check re segfault
|
|
|
- val.append(item.data(
|
|
|
- MultiValueInputWidget.NativeObjectRole))
|
|
|
+ val.append(item.data(
|
|
|
+ MultiValueInputWidget.NativeObjectRole))
|
|
|
val = [v for v in val if v is not _NoValue]
|
|
|
if not val:
|
|
|
# do not report an empty list, when no valid items exist.
|
|
@@ -184,10 +143,14 @@ class MultiValueInputWidget(QWidget, GooeyParamWidgetMixin):
|
|
|
val = _NoValue
|
|
|
self._set_gooey_param_value(val)
|
|
|
|
|
|
+ def set_gooey_param_spec(self, name: str, default=_NoValue):
|
|
|
+ super().set_gooey_param_spec(name, default)
|
|
|
+ self._editor.set_gooey_param_spec(name, default)
|
|
|
+
|
|
|
def set_gooey_param_docs(self, docs: str) -> None:
|
|
|
self._editor_param_docs = docs
|
|
|
# the "+" button is always visible. Use it to make the docs accessible
|
|
|
- self._additem_button.setToolTip(docs)
|
|
|
+ self._add_pb.setToolTip(docs)
|
|
|
|
|
|
def init_gooey_from_params(self, spec):
|
|
|
# first the normal handling
|
|
@@ -195,7 +158,7 @@ class MultiValueInputWidget(QWidget, GooeyParamWidgetMixin):
|
|
|
# for the editor widget, we just keep the union of all reported
|
|
|
# changes, i.e. the latest info for all parameters that ever changed.
|
|
|
# this is then passed to the editor widget, after its creation
|
|
|
- self._editor_init.update(spec)
|
|
|
+ self._editor.init_gooey_from_params(spec)
|
|
|
|
|
|
def get_gooey_param_spec(self):
|
|
|
self._handle_input()
|