Pārlūkot izejas kodu

New MultiValueInputWidget implementation

It now longer does the previous in-item editing approach. Instead,
it features a persistent editor. It should no longer crash.

Closes datalad/datalad-gooey#114

This approach could be extended to support multiple input widgets for
different types that joint feed into a list of items. This would be a
possible solution to datalad/datalad-gooey#90
Michael Hanke 1 gadu atpakaļ
vecāks
revīzija
42a43577ef
1 mainītis faili ar 82 papildinājumiem un 119 dzēšanām
  1. 82 119
      datalad_gooey/param_multival_widget.py

+ 82 - 119
datalad_gooey/param_multival_widget.py

@@ -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()