Aleph0
Aleph0

Reputation: 6074

Context depending drag and drop in QListView

In one of my projects I have to manage a list of items, that can be rearranged in their orders by using drag and drop.

Now, all items came with a priority, that cannot be changed by the user. There is an restriction on the order of the elements in the list, namely that elements with a lower priority must come first, but elements with the same priority can be in interchanged.

For example, the following list is sane:

(A,1),(B,1),(C,1),(D,2),(E,3)

whereas the following is broken:

(A,1),(B,1),(E,3),(D,2)

The following code shows a starting point of my problem:

#include <QApplication>
#include <QFrame>
#include <QHBoxLayout>
#include <QListView>
#include <QStandardItemModel>

QStandardItem* create(const QString& text, int priority) {
    auto ret = new QStandardItem(text);
    ret->setData(priority);
    return ret;
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    auto frame = new QFrame;
    frame->setLayout(new QVBoxLayout);
    auto view = new QListView;
    frame->layout()->addWidget(view);
    auto model = new QStandardItemModel;
    view->setModel(model);
    model->appendRow(create("1. A", 1));
    model->appendRow(create("1. B", 1));
    model->appendRow(create("2. X", 2));
    model->appendRow(create("2. Y", 2));
    model->appendRow(create("2. Z", 2));

    view->setDragEnabled(true);
    view->viewport()->setAcceptDrops(true);
    view->setDropIndicatorShown(true);
    view->setDragDropMode(QAbstractItemView::DragDropMode::InternalMove);
    view->setDefaultDropAction(Qt::DropAction::MoveAction);
    view->setDragDropOverwriteMode(false);

    frame->show();
    return a.exec();
}

Now, the DefaultDropAction must change context depended on the item going to be moved and also the item where it is going to be dropped.

If the priorities of the two elements are equal, then I have a MoveAction. In case the priorities of the two elements differ, I have a IgnoreAction.

Can this behavior be achieved without implementing my on QListView and what can be achieved by adapting a custom QAbstractItemModel.

A possible workaround might be even to abandon the drag and drop interface and using the arrow up and down keys to move items around. Or even more general an action with cut and paste operation. But, I really prefer to stick with drag and drop interface.

Upvotes: 0

Views: 374

Answers (1)

Maxim Paperno
Maxim Paperno

Reputation: 4859

You could reimplement QStandardItemModel and override the canDropMimeData() method. There are other ways, though they would probably be more involved if you're happy with QStandardItemModel already. Implementing your own model could have performance advantages, especially if your data structure is fairly simple (like a single-column list). This would also let you customize the drag/drop behavior much more in general.

Note that this ignores the action type entirely (QStandardItemModel only allows move and copy by default). Moving an item onto another item will remove the destination item entirely -- which may not be what you want but is a separate issue (see comments in code below).

You could also implement the same logic in dropMimeData() method (before calling the base class method), but I'm not sure I see any advantage. And by using canDropMimeData() the user also gets visual feedback about what is and isn't going to work.


#include <QStandardItemModel>

class ItemModel : public QStandardItemModel
{
    public:
        using QStandardItemModel::QStandardItemModel;

        bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override
        {
            if (!QStandardItemModel::canDropMimeData(data, action, row, column, parent))
                return false;

            const int role = Qt::UserRole + 1;  // what QStandardItem uses for setData() by default
            int originPriority;
            int destPriority;

            // Find destination item priority.
            if (parent.isValid()) {
                // dropping onto an item
                // Note: if you don't want MoveAction to overwrite items you could:
                //   if (action == Qt::MoveAction) return false;
                destPriority = parent.data(role).toInt();
            }
            else if (row > -1) {
                // dropping between items
                destPriority = this->data(index(row, 0), role).toInt();
            }
            else {
                // dropping somewhere else onto the view, treat it as drop after last item in model
                destPriority = this->data(index(rowCount() - 1, 0), role).toInt();
            }

            // Need to find priority of item(s) being dragged (encoded in mime data). Could be several.
            // This part decodes the mime data in a way compatible with how QAbstractItemModel encoded it.
            // (QStandardItemModel includes it in the mime data alongside its own version)
            QByteArray ba = data->data(QAbstractItemModel::mimeTypes().first());
            QDataStream ds(&ba, QIODevice::ReadOnly);
            while (!ds.atEnd()) {
                int r, c;
                QMap<int, QVariant> v;
                ds >> r >> c >> v;
                // If there were multiple columns of data we could also do a
                //   check on the column number, for example.
                originPriority = v.value(role).toInt();
                if (originPriority != destPriority)
                    break;  //return false;  Could exit here but keep going to print our debug info.
            }

            qDebug() << "Drop parent:" << parent << "row:" << row << 
                        "destPriority:" << destPriority << "originPriority:" << originPriority;

            if (originPriority != destPriority)
                return false;

            return true;
        }
};

For reference, here's how QAbstractItemModel encodes data (and decodes it in the next method down).

ADDED: OK, it was bugging me a bit, so here is a more efficient version... :-) It saves a lot of decoding time by embedding the dragged item's priority right into the mime data when the drag starts.

#include <QStandardItemModel>

#define PRIORITY_MIME_TYPE   QStringLiteral("application/x-priority-data")

class ItemModel : public QStandardItemModel
{
    public:
        using QStandardItemModel::QStandardItemModel;

        QMimeData *mimeData(const QModelIndexList &indexes) const override
        {
            QMimeData *mdata = QStandardItemModel::mimeData(indexes);
            if (!mdata)
                return nullptr;

            // Add our own priority data for more efficient evaluation in canDropMimeData()
            const int role = Qt::UserRole + 1;  // data role for priority value
            int priority = -1;
            bool ok;

            for (const QModelIndex &idx : indexes) {
                // Priority of selected item
                const int thisPriority = idx.data(role).toInt(&ok);
                // When dragging multiple items, check that the priorities of all selected items are the same.
                if (!ok || (priority > -1 && thisPriority != priority))
                    return nullptr;  // Cannot drag items with different priorities;

                priority = thisPriority;
            }
            if (priority < 0)
                return nullptr;  // couldn't find a priority, cancel the drag.

            // Encode the priority data
            QByteArray ba;
            ba.setNum(priority);
            mdata->setData(PRIORITY_MIME_TYPE, ba);

            return mdata;
        }

        bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override
        {
            if (!QStandardItemModel::canDropMimeData(data, action, row, column, parent))
                return false;
            if (!data->hasFormat(PRIORITY_MIME_TYPE))
                return false;

            const int role = Qt::UserRole + 1;  // what QStandardItem uses for setData() by default
            int destPriority = -1;
            bool ok = false;

            // Find destination item priority.
            if (parent.isValid()) {
                // dropping onto an item
                destPriority = parent.data(role).toInt(&ok);
            }
            else if (row > -1) {
                // dropping between items
                destPriority = this->data(index(row, 0), role).toInt(&ok);
            }
            else {
                // dropping somewhere else onto the view, treat it as drop after last item in model
                destPriority = this->data(index(rowCount() - 1, 0), role).toInt(&ok);
            }
            if (!ok || destPriority < 0)
                return false;

            // Get priority of item(s) being dragged which we encoded in mimeData() method.
            const int originPriority = data->data(PRIORITY_MIME_TYPE).toInt(&ok);

            qDebug() << "Drop parent:" << parent << "row:" << row
                     << "destPriority:" << destPriority << "originPriority:" << originPriority;

            if (!ok || originPriority != destPriority)
                return false;

            return true;
        }
};

Upvotes: 1

Related Questions