Enginio C++ Examples - Todos

The Todo example shows how to use the EnginioModel with Qt Widgets.

In this example, a simple list of objects is displayed in a QListView. Each item in the list is a "To Do" object which can be "done" or "not yet done". Todos can be added, removed, or altered.

In this simple schema the objects will have two properties that are added to the default properties (such as creation date which always exists): a string "title" and a bool "completed". The object type will be created when a call to create, or in this case EnginoModel::append(), is made.

A todo object will look like this (in JSON):

{
  "title": "Buy Milk",
  "completed": false
}

The first step is to create a TodosModel which inherits EnginioModel, and defines the main roles which will be used. As we are interested in the To Do title and the completed information we need to define these two roles.

class TodosModel : public EnginioModel
{
    Q_OBJECT

public:
    enum Role
    {
        TitleRole = Enginio::CustomPropertyRole,
        CompletedRole
    };

By default, views (for example QListView) use the Qt::ItemDataRole role to display or edit the content. The newly created EnginioModel is empty and defines basic roles. Most roles are created dynamically, based on the JSON datastructure. They have no predefined value in the Qt::ItemDataRole enum. EnginioModel automatically populates itself as soon as the query and client properties have been set. When the data is downloaded, the model resets itself, and sets up the internal data cache and roles names. EnginioModel guesses the role names based on heuristics. It may be wrong if not all objects received from the backend have exactly the same structure. For example, a property can be missing in certain objects. To protect against such cases, we overload roleNames(). Overriding roleNames() can also be used to match default Qt roles to the named ones.

QHash<int, QByteArray> TodosModel::roleNames() const
{
    QHash<int, QByteArray> roles = EnginioModel::roleNames();
    roles.insert(TitleRole, "title");
    roles.insert(Qt::DisplayRole, "title");
    roles.insert(Qt::EditRole, "title");
    roles.insert(CompletedRole, "completed");
    return roles;
}

In this example we map the Qt::DisplayRole and Qt::EditRole to the title property in the JSON. This way the right string is shown by default and editing works as expected.

Remember to always call the base class implementation to avoid situations in which the internal cache is not in sync.

By default EnginioModel operates on QJsonValue, and that is what the data() function returns inside the QVariant, but standard views, such as QListView, use predefined roles which do not map directly to our roles. That is why we need to write a mapping between them:

QVariant TodosModel::data(const QModelIndex &index, int role) const
{
    if (role == Qt::FontRole) {
        bool completed = EnginioModel::data(index, CompletedRole).value<QJsonValue>().toBool();
        QFont font;
        font.setPointSize(20);
        font.setStrikeOut(completed);
        return font;
    }

    if (role == Qt::TextColorRole) {
        bool completed = EnginioModel::data(index, CompletedRole).value<QJsonValue>().toBool();
        return completed ? QColor("#999") : QColor("#333");
    }

    if (role == CompletedRole)
        return EnginioModel::data(index, CompletedRole).value<QJsonValue>().toBool();

    // fallback to base class
    return EnginioModel::data(index, role);
}

As we have our model defined, we need to create an instance of EnginioClient:

m_client = new EnginioClient(this);
m_client->setBackendId(EnginioBackendId);

It is used by the model to connect to the Enginio backend. Next we need to construct and configure our model too. The configuration is based on two steps, assigning an EnginioClient instance and by creating a query.

m_model = new TodosModel(this);
m_model->setClient(m_client);

QJsonObject query;
query["objectType"] = QString::fromUtf8("objects.todos");
m_model->setQuery(query);

The model has to be assigned to a view. In our case it is a QListView.

m_view->setModel(m_model);

To make the application fully functional, a way to add and remove "To Dos" is needed. To do so, we need to connect the correct buttons to slots for adding a new item:

void MainWindow::appendItem()
{
    bool ok;
    QString text = QInputDialog::getText(this, tr("Create a new To Do")
                                         , tr("Title:"), QLineEdit::Normal
                                         , "", &ok);
    if (ok && !text.isEmpty()){
        QJsonObject object;
        object["title"] = text;
        object["completed"] = false; // By default a new To Do is not completed
        EnginioReply *reply = m_model->append(object);
        QObject::connect(reply, &EnginioReply::finished, reply, &EnginioReply::deleteLater);
    }
}

and for removing it:

void MainWindow::removeItem()
{
    QModelIndex index = m_view->currentIndex();
    EnginioReply *reply = m_model->remove(index.row());
    QObject::connect(reply, &EnginioReply::finished, reply, &EnginioReply::deleteLater);
}

Files: