0
votes

I follow the Using C++ Models with Qt Quick Views and AbstractItemModel example project in Qt 5.5. Screen class is the model data; ScreenManager is derived from QAbstractListModel acting as the model source in QML GridView. In the GridView, I also add a MouseArea and some animation in delegate to implement items drag and drop.

My expected result is when dragging a screen around, whenever it's on top of another screen, the target screen will be moved to the last position of dragged screen until button release to drop the dragged screen. All the movement should use the animation declared in QML.

Now it could show screens correctly and recognize the selected screen. But it fails to swap the dragged and dropped screens. The underlying list has swapped the element but it doesn't reflect to the view.

A similar question is this, I try to use beginMoveRows and endMoveRows. But my program crashes on calling endMoveRows. layoutChanged would rearrange the whole model. Because I have animation on grid item movement. layoutChanged would cause non affected screens shifting from top left to their original position.

Edit: endMoveRows crash is caused by invalid operation described here.

Edit: the GridView has a 3 * 5 items. Since it's in a QList, I assume I only need to move on rows.

Screen.h

class Screen
{
    public:
    Screen(QString name, int gridId, bool active = false);

    QString name() const;
    int gridId() const;
    bool active() const;

    void setActive(bool a);

private:
    QString m_name;
    int m_gridId;
    bool m_active;
};

ScreenManager.h

#include "Screen.h"

#include <QAbstractListModel>

class ScreenManager : public QAbstractListModel
{
    Q_OBJECT
public:
    enum ScreenRoles {
        NameRole = Qt::UserRole + 1,
        GridIDRole,
        ActiveRole
    };

    ScreenManager();

    void addScreen(const Screen& screen);
    int rowCount(const QModelIndex& parent = QModelIndex()) const;
    QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;

    Q_INVOKABLE int getScreenGridId(int index);
    Q_INVOKABLE bool getScreenActive(int index);
    Q_INVOKABLE void swapScreens(int index1, int index2);

protected:
    QHash<int, QByteArray> roleNames() const;

private:
    QList<Screen> m_screens;
};

ScreenManager.cpp

#include "ScreenManager.h"

#include "Screen.h"

ScreenManager::ScreenManager()
{
    int index = 0;
    for (;index < 15; index++) {
        addScreen(Screen(QString ("Screen%1").arg(index), index, true));
    }
}

void ScreenManager::addScreen(const Screen& screen)
{
    beginInsertRows(QModelIndex(), rowCount(), rowCount());
    m_screens << screen;
    endInsertRows();
}

int ScreenManager::rowCount(const QModelIndex& parent) const {
    Q_UNUSED(parent);
    return m_screens.count();
}

QVariant ScreenManager::data(const QModelIndex& index, int role) const
{
    if (index.row() < 0 || index.row() >= m_screens.count())
        return QVariant();

    const Screen& screen = m_screens[index.row()];
    if (role == NameRole)
        return screen.name();
    else if (role == GridIDRole)
        return screen.gridId();
    else if (role == ActiveRole)
        return screen.active();
    return QVariant();
}

QHash<int, QByteArray> ScreenManager::roleNames() const
{
    QHash<int, QByteArray> roles;
    roles[NameRole] = "name";
    roles[GridIDRole] = "gridId";
    roles[ActiveRole] = "active";
    return roles;
}

int ScreenManager::getScreenGridId(int index)
{
    return  m_screens.at(index).gridId();
}

bool ScreenManager::getScreenActive(int index)
{
    return  m_screens.at(index).active();
}

void ScreenManager::swapScreens(int index1, int index2)
{
    int min = index1 < index2 ? index1 : index2;
    int max = index1 > index2 ? index1 : index2;
    bool r = beginMoveRows(QModelIndex(), min, min, QModelIndex(), max);
    r = beginMoveRows(QModelIndex(), max-1, max-1, QModelIndex(), min);
    m_screens.swap(index1, index2);
    endMoveRows();
}

QML GridView related code

ScreenManager {
    id : screenManager
}
GridView {
        id: gridView
        x: 82
        y: 113
        width: cellWidth * 5
        height: cellHeight * 3
        clip: true
        anchors.bottom: parent.bottom
        anchors.bottomMargin: 70
        anchors.topMargin: 100
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.top: parent.top
        flickableDirection: Flickable.HorizontalAndVerticalFlick
        cellWidth: 90; cellHeight: 90;
        property bool ignoreMovementAnimation: true

        MouseArea {
            id: gridViewMouseArea
            hoverEnabled: true
            preventStealing : true
            property int currentGridId: -1
            property int preIndex
            property int index: gridView.indexAt(mouseX, mouseY)
            anchors.fill: parent
            onPressAndHold: {
                currentGridId = screenManager.getScreenGridId(index)
                preIndex = index
                preIndexBackup = preIndex
                gridView.ignoreMovementAnimation = false
            }
            onReleased: currentGridId = -1
            onPositionChanged: {
                if (currentGridId != -1 && index != -1 && index != preIndex) {
                    if (screenManager.getScreenActive(index)) {
                        screenManager.swapScreens(preIndex, index)
                        preIndex = index
                    }
                }
            }
        }

        model: screenManager
        delegate: Component {
            Item {
                id: gridViewDelegate
                width: gridView.cellWidth; height: gridView.cellHeight

                Image {
                    id: itemImage
                    parent: gridView
                    x: gridViewDelegate.x + 5
                    y: gridViewDelegate.y + 5
                    width: gridViewDelegate.width - 10
                    height: gridViewDelegate.height - 10;
                    fillMode: Image.PreserveAspectFit
                    smooth: true
                    source: "qrc:/res/image/screen_icon.png"
                    visible: active

                    Text {
                        text: name
                        anchors.horizontalCenter: parent.horizontalCenter
                        anchors.verticalCenter: parent.verticalCenter
                    }

                    Rectangle {
                        anchors.fill: parent;
                        border.color: "grey"
                        border.width: 6
                        color: "transparent"; radius: 5
                        visible: itemImage.state === "active"
                    }

                    // specify the movement's animation for non-active screen icons
                    Behavior on x {
                        enabled: !gridView.ignoreMovementAnimation && itemImage.state !== "active"
                        NumberAnimation { duration: 400; easing.type: Easing.OutBack }
                    }
                    Behavior on y {
                        enabled: !gridView.ignoreMovementAnimation && itemImage.state !== "active"
                        NumberAnimation { duration: 400; easing.type: Easing.OutBack }
                    }

                    // specify the shaking animation for non-active screen icons when hold one icon
                    SequentialAnimation on rotation {
                        NumberAnimation { to:  2; duration: 60 }
                        NumberAnimation { to: -2; duration: 120 }
                        NumberAnimation { to:  0; duration: 60 }
                        running: gridViewMouseArea.currentGridId != -1 && itemImage.state !== "active"
                        loops: Animation.Infinite
                        alwaysRunToEnd: true
                    }

                    // specify the active screen's new position and size
                    states: State {
                        name: "active"
                        when: gridViewMouseArea.currentGridId == gridId
                        PropertyChanges {
                            target: itemImage
                            x: gridViewMouseArea.mouseX - width/2
                            y: gridViewMouseArea.mouseY - height/2
                            scale: 0.5
                            z: 10
                        }
                    }

                    // specify the scale speed for the active screen icon
                    transitions: Transition {
                        NumberAnimation { property: "scale"; duration: 200}
                    }
                }
            }
        }
    }

main.cpp

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

    qmlRegisterType<ScreenManager>("com.gui", 1, 0, "ScreenManager");

    QQmlApplicationEngine engine(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}
1

1 Answers

0
votes

It turns out I need to deal some corner cases to avoid no-op or invalid move operation.

void ScreenManager::swapScreens(int index1, int index2)
{
    int min = index1 < index2 ? index1 : index2;
    int max = index1 > index2 ? index1 : index2;
    m_screens.swap(index1, index2);
    beginMoveRows(QModelIndex(), max, max, QModelIndex(), min);
    endMoveRows();

    if (max - min > 1) {
        beginMoveRows(QModelIndex(), min + 1, min + 1, QModelIndex(), max + 1);
        endMoveRows();
    }
}