0

In QML, I want to specify a list of options (strings) that the user can choose to insert into my backend. But I don't want the list to contain strings that are already in the backend. And if the backend is updated, then the list of options should also update.

First I wrote a subclass QAbstractListModel which exposes a list of QString interface for my backend. It follows the guidelines from the docs, and exposes two custom functions push_back and indexOf.

class MyModel: public QAbstractListModel {
    Q_OBJECT
public:
    MyModel() { ... } // connect to the backend

    QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override {
        Q_UNUSED(role);
        if (!hasIndex(index.row(), index.column())) {
            return {};
        }
        return ... // retreive backend object at index.row() and convert to QString
    }


    Q_INVOKABLE void push_back(const QString& item) {
        beginInsertRows(QModelIndex(), int(_data->size()), int(_data->size()));

        ... // convert QString to backend data type and insert into backend

        endInsertRows();
    }

    Q_INVOKABLE int indexOf(const QString& item) {
        return ... // convert QString to backend type, and search backend for this object. return -1 if not found
    }


private:
    // pointer to backend object
};

I was thinking something like this in my QML. First a view of the strings that are already in the backend. This part works fine.

ListView {
  anchors.fill: parent
  spacing: 5
  model: backend // an instance MyModel interface, passed in from main.cpp

  delegate: Rectangle {
    height: 25
    width: 200
    Text { text: model.display}
  }
}

And then the list of options that can be added to the backend, which are not already part of the backend. This part doesn't work.

ListView {
  anchors.fill: parent
  spacing: 5
  model: ["option1", "option2", "option3"]

  delegate: Rectangle {
    visible: backend.indexOf(model.modelData) >= 0
    height: 25
    width: 200
    MouseArea {
        id: mouseArea1
        anchors.fill: parent
        onClicked: {
            backend.push_back(model.modelData)
        }
    }
    Text { text: model.modelData }
  }
}

The problem is, when strings are added to the backend, this list does not refresh. I don't think Qt understands that backend.indexOf needs to be recomputed whenever its modified.

Mark
  • 5,286
  • 5
  • 42
  • 73
  • Why don't you remove the element from the qml model that was added to the c++ model? – eyllanesc Nov 18 '20 at 23:23
  • The backend can be updated by other means as well. For example, loading a config file or pressing "reset to defaults" button. Then those functions would also have to reach into this list somehow and synchronize it. – Mark Nov 18 '20 at 23:31
  • mmm, I think your post does not cover everything you pointed out in the previous comment. For example let's say that through qml you select the option "option1" so it will be added to the backend and it will not be visible (or removed) but then from C++ (or other means) "option1" is removed then in that case, should it be done visible "option1" in the ListView? – eyllanesc Nov 18 '20 at 23:35
  • If the backend changes by alternate means, the `endInsertRows()` or `endRemoveRows()` would be called. Qt can use this signal to know it should re-evaluate expressions that are dependent on the model, such as `backend.indexOf(...)`. This should update visibility of the option that has been inserted or removed. – Mark Nov 18 '20 at 23:44
  • That is what you assume but it is not how QML works. The slots (or Q_INVOKABLES) do not generate a binding with the QML properties, so the "visible" property is not updated – eyllanesc Nov 18 '20 at 23:47
  • Is there a way to make that possible? Otherwise I'll implement another model for the options and manually update them when I get the signal from backend – Mark Nov 19 '20 at 00:31

1 Answers1

2

You are correct about the problem. There's no binding that will re-call indexOf. One way to fix it is you could add a Connections object so you can listen for a specific signal and then manually update the visible property:

  delegate: Rectangle {
    visible: backend.indexOf(model.modelData) >= 0
    height: 25
    width: 200
    MouseArea {
        id: mouseArea1
        anchors.fill: parent
        onClicked: {
            backend.push_back(model.modelData)
        }
    }
    Text { text: model.modelData }

    Connections {
      target: backend
      onCountChanged: visible = (backend.indexOf(model.modelData) >= 0)
    }
  }
JarMan
  • 7,589
  • 1
  • 10
  • 25
  • This looks like a great workaround. I tried it though and I got `qrc:/main.qml:127:19: QML Connections: Implicitly defined onFoo properties in Connections are deprecated. Use this syntax instead: function onFoo() { ... } qrc:/main.qml:127:19: QML Connections: Cannot assign to non-existent property "onCountChanged"` Is `countChanged` meant to be a custom signal I'm supposed to implement? – Mark Nov 19 '20 at 01:37
  • Okay I fixed the deprecated message using the syntax here https://stackoverflow.com/questions/62297192/qml-connections-implicitly-defined-onfoo-properties-in-connections-are-deprecat and then I implemented `changed()` as a custom signal and it worked! – Mark Nov 19 '20 at 01:47
  • 1
    Yes, the deprecated message is an annoying addition with 5.15. I don't know why they changed the syntax. For `onCountChanged`, I assumed your model was already emitting that when you inserted an item, but yes, any signal will do. – JarMan Nov 19 '20 at 02:00
  • I also had to change the height property to a negative value (negation of the `spacing` value) to make up for the empty voids that were left over. – Mark Nov 19 '20 at 02:01