QListView, internal drag-n-drop with custom data



  • I have a custom view based on QListView, and a custom model on QAbstractListModel, that stores a QList of type <Shape*>. I have the basics working (display, insert, remove and edit data).

    I am trying to enable internal drag and drop now, so that the user can move shapes above or below one another in the list (think layers in Photoshop).

    I am returning the proper flags to only be able to pick up valid indexes and only to able to drop on empty space in between indexes, and not overwrite (or at least I think I am):

    Qt::ItemFlags ShapeListModel::flags(const QModelIndex& index) const
    {
        if (index.isValid())
        {
            return  QAbstractListModel::flags(index)| Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled | Qt::ItemIsEditable;
        }
    
        return Qt::ItemIsSelectable|Qt::ItemIsDragEnabled| Qt::ItemIsDropEnabled|Qt::ItemIsEnabled;
    }
    

    The drag and drop is properly enabled and I can do what I want except that the entry simply gets deleted instead of moved.

    The problem is, from what I gather, that Qt::MoveAction apparently consists of two calls: one to insertRows and another for removeRows. I do not normally use those functions, instead I have a custom API that inserts the shape into the list, when the user has finished drawing one.

    So then I decided to implement insertRows and removeRows functions solely for the purpose of drag and drop.
    RemoveRows part is fine and easy, but insertRows is giving me a headache. Because...

    bool ShapeListModel::insertRows(int row, int count, const QModelIndex& parent)
    {
        beginInsertRows(QModelIndex(), row, row);
        shapeList.insert(row, *SHAPE*);
        endInsertRows();
        return true;
    }
    

    The second parameter to insert would need to be an object of type Shape* (the one that is actually getting moved) and I am having trouble retrieving the proper object and passing it into this function. I thought about storing it into a smart pointer, but I don't know how to get the index that was clicked on at the beginning of the drag-n-drop operation.

    Tried signals and slots currentChanged, selectionChanged from the view, but those do not work for some reason (probably because the model is not connected at construction but afterwards with setModel).

    Any suggestions appreciated

    Thanks


  • Moderators

    @6ort said:

    RemoveRows part is fine and easy, but insertRows is giving me a headache. Because...

    bool ShapeListModel::insertRows(int row, int count, const QModelIndex& parent)
    {
        beginInsertRows(QModelIndex(), row, row);
        shapeList.insert(row, *SHAPE*);
        endInsertRows();
        return true;
    }
    

    insertRows() of the model is supposed to just insert the rows but not to fill them with data yet.
    You will receive a separate setData() call for that.


  • Moderators

    Also note the following about Qt's standard drag-n-drop implementation:

    • Only QVariant data for all item-roles from 0 to Qt::UserRole are encoded into the drop-data
    • The data you return from your model's data() implementation is encoded into the drag-data (Mime-Type: application/x-qabstractitemmodeldatalist)

    So in case this isn't enough for your case to identify the dragged data you would need to implement the drag-n-drop support by yourself.

    By overriding the following methods of your model:

    1. mimeType()
    2. mimeData()
    3. dropMimeData()
    4. optional: supportedDropActions()


  • @raven-worx

    In that case I am not sure how to do that for a QList. To insert a row without actually adding data to it, there is no such function in QList. The closest similar thing would be to call shapeList.reserve(shapeList.size() + 1); which works, but again just "eats" the object on drop because no data is actually copied.
    I also set a breakpoint for setData and it does not get called when drag-n-dropping so I am not sure of the relevance of it here. setData works fine when I double click on an item in the list to change it's name.

    About your second post: even though the list internally handles objects of type Shape*, the data function only returns their name which is a QString and as such I believe a part of QVariant.

    The main problem is how to get the index of the shape I clicked on and want to move, so that I can copy it to the position that I dropped it on. I wish I could just use QList's move function which would do exactly what I want, but again I would need to know the origin index which I don't know how to get, from the drag-n-drop.

    Is it really necessary to reimplement all of those things for something as basic as this?


  • Lifetime Qt Champion

    Hi,

    What kind of data should be dragged/dropped around ?


  • Moderators

    @6ort said:

    @raven-worx

    In that case I am not sure how to do that for a QList. To insert a row without actually adding data to it, there is no such function in QList. The closest similar thing would be to call shapeList.reserve(shapeList.size() + 1); which works, but again just "eats" the object on drop because no data is actually copied.

    correct me if i am wrong, but you are storing pointers in the list right?
    So you can simply insert a NULL-pointer.

    I also set a breakpoint for setData and it does not get called when drag-n-dropping so I am not sure of the relevance of it here. setData works fine when I double click on an item in the list to change it's name.

    hmm AFAIR it should work this way... you can set a breakpoint in dropMimeData() and step-through to see where the data gets set then.

    The main problem is how to get the index of the shape I clicked on and want to move, so that I can copy it to the position that I dropped it on. I wish I could just use QList's move function which would do exactly what I want, but again I would need to know the origin index which I don't know how to get, from the drag-n-drop.

    this isn't that simple, since you could also drag multiple indexes at the same time (multi-selection).
    So this is a generic approach Qt is going here.

    Is it really necessary to reimplement all of those things for something as basic as this?

    probably the easiest will be to reimplement at least the 3 mentioned methods.
    Simply call the base implementation for mimeData() and mimeTypes() and add a custom mime-type and any information you like (e.g. the index)
    In dropMimeData() check for your custom mime-type. If it's present do your internal moving, else call the base implemention.



  • @raven-worx

    1. You're correct. I am now inserting a nullptr in the insertRows() function, and then later in setData I use the QList::replace() function to actually put a shape into that index.

    2. The reason it didn't work is that I used the pre-compiled Qt binaries. On the binaries, for some reason (probably that I don't know how to set it up properly), putting a breakpoint on any function that is defined in the source code does not trigger the debugger to stop there at all.
      I fixed that by deleting Qt, compiling it from the sources (with some headache), installing Qt Creator again and pointing it to the kit. Now I can put breakpoints into the source code and the debugger will actually stop on them, revealing that the call to setData actually does happen when dropping rows.

    3. After all this I came upon a solution that seems to work, I'd just like an opinion if this is a good way to go about it, it might need some polish.

    I figured I didn't want to reimplement the whole drag and drop system just to be able to pass some indexes through it, when I am already displaying the shape's names in the view, so I thought... to determine the index, I can just iterate through my list and find the shape that has the same name as the one that I clicked on. Here's what I do in setData:

    bool ShapeListModel::setData(const QModelIndex& index, const QVariant& value, int role)
    {
    //This role is used when double-clicking on an item
        if (index.isValid() && role == Qt::EditRole) {
            if (value.toString() != shapeList.at(index.row())->getName() && !value.toString().isEmpty())
            {
                shapeList[index.row()]->setName(value.toString());
                emit dataChanged(index, index);
                return true;
            }
        }
    //And this one when drag and dropping
        else if (role == Qt::DisplayRole)
        {
            Shape* shape {nullptr};
            for (int i = 0; i < shapeList.size(); i++)
            {
                if (i == index.row())
                {
                    //do nothing
                }
                else
                {
                    if (shapeList.at(i)->getName() == value.toString())
                    {
                        shape = shapeList.at(i);
                        if (shape)
                        {
                            shapeList.replace(index.row(), shape);
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }
    

    So basically, I do a for loop on my shapeList to find the shape which has the name displayed in the QVariant "value" (the name that I clicked on in the view). Once I found it, I set a pointer to its address. Then I simply replace the contents of the empty row (the one with the nullptr) with the shape that I clicked on. Afterwards the call to removeRows takes care of the shape's original index. This works fine. The only requirement is that each shape has a unique name, which is probably a good idea to do anyways. The only thing I'm not sure about is the "else if (role == Qt::DisplayRole)", but according to the debugger that is the set role when drag and dropping.

    What do you think of this implementation?


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.