From 01a8e9f30d4396378e97a31e4b6d2ee489cb0ac9 Mon Sep 17 00:00:00 2001 From: Jimmy Girardet Date: Wed, 9 Sep 2020 08:00:11 +0200 Subject: examples: add QAbstractListModel/QML Interactive example to add, remove and move elements inside a ListView (QML) from a QAbstractListModel (Python). A screenshot is included. Task-number: PYSIDE-841 Change-Id: I1c4d7868860c7482930fbb729cb4c2b503c01d88 Reviewed-by: Christian Tismer --- .../declarative/editingmodel/MovingRectangle.qml | 115 +++++++++++++ .../declarative/editingmodel/doc/editingmodel.rst | 14 ++ .../editingmodel/doc/qabstractlistmodelqml.png | Bin 0 -> 45810 bytes examples/declarative/editingmodel/main.py | 59 +++++++ examples/declarative/editingmodel/main.pyproject | 3 + examples/declarative/editingmodel/main.qml | 143 ++++++++++++++++ examples/declarative/editingmodel/model.py | 187 +++++++++++++++++++++ 7 files changed, 521 insertions(+) create mode 100644 examples/declarative/editingmodel/MovingRectangle.qml create mode 100644 examples/declarative/editingmodel/doc/editingmodel.rst create mode 100644 examples/declarative/editingmodel/doc/qabstractlistmodelqml.png create mode 100644 examples/declarative/editingmodel/main.py create mode 100644 examples/declarative/editingmodel/main.pyproject create mode 100644 examples/declarative/editingmodel/main.qml create mode 100644 examples/declarative/editingmodel/model.py diff --git a/examples/declarative/editingmodel/MovingRectangle.qml b/examples/declarative/editingmodel/MovingRectangle.qml new file mode 100644 index 000000000..0d835af1c --- /dev/null +++ b/examples/declarative/editingmodel/MovingRectangle.qml @@ -0,0 +1,115 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt for Python examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +import QtQuick +import QtQuick.Controls + +Rectangle { + id: root + property int modelIndex + property Item dragParent + property Item sizeParent + property alias text: zone.text + property alias bgColor: root.color + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + color: backgroundColor + anchors.fill: sizeParent + border.color: "yellow" + border.width: 0 + TextArea { + id: zone + anchors.centerIn: parent + text: display + onTextChanged: model.edit = text + } + + MouseArea { + id: zoneMouseArea + anchors.fill: parent + + acceptedButtons: Qt.MiddleButton + onClicked: function(mouse) { + if (mouse.button == Qt.MiddleButton) + lv.model.remove(index) + else + mouse.accepted = false + } + } + DragHandler { + id: dragHandler + xAxis { + + enabled: true + minimum: 0 + maximum: lv.width - droparea.width + } + yAxis.enabled: false + acceptedButtons: Qt.LeftButton + } + Drag.active: dragHandler.active + Drag.source: root + Drag.hotSpot.x: width / 2 + + states: [ + State { + when: dragHandler.active + ParentChange { + target: root + parent: root.dragParent + } + + AnchorChanges { + target: root + anchors.horizontalCenter: undefined + anchors.verticalCenter: undefined + } + PropertyChanges { + target: root + opacity: 0.6 + border.width: 3 + } + } + ] +} diff --git a/examples/declarative/editingmodel/doc/editingmodel.rst b/examples/declarative/editingmodel/doc/editingmodel.rst new file mode 100644 index 000000000..d76bebc22 --- /dev/null +++ b/examples/declarative/editingmodel/doc/editingmodel.rst @@ -0,0 +1,14 @@ +QAbstractListModel in QML +========================= + +This example shows how to add, remove and move items inside a QML +ListView, but showing and editing the data via roles using a +QAbstractListModel from Python. + +You can add new elements and reset the view using the two top buttons, +remove elements by 'middle click' the element, and move the elements +with a 'left click' plus dragging the item around. + +.. image:: qabstractlistmodelqml.png + :width: 400 + :alt: QAbstractListModel/ListView Screenshot diff --git a/examples/declarative/editingmodel/doc/qabstractlistmodelqml.png b/examples/declarative/editingmodel/doc/qabstractlistmodelqml.png new file mode 100644 index 000000000..6e181fba1 Binary files /dev/null and b/examples/declarative/editingmodel/doc/qabstractlistmodelqml.png differ diff --git a/examples/declarative/editingmodel/main.py b/examples/declarative/editingmodel/main.py new file mode 100644 index 000000000..6aee0d224 --- /dev/null +++ b/examples/declarative/editingmodel/main.py @@ -0,0 +1,59 @@ +############################################################################# +## +## Copyright (C) 2021 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +import sys +from pathlib import Path + +from PySide6.QtCore import QUrl +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType + +from model import BaseModel + +if __name__ == "__main__": + app = QGuiApplication(sys.argv) + qmlRegisterType(BaseModel, "BaseModel", 1, 0, "BaseModel") + engine = QQmlApplicationEngine() + qml_file = Path(__file__).parent / "main.qml" + engine.load(QUrl.fromLocalFile(qml_file)) + + if not engine.rootObjects(): + sys.exit(-1) + sys.exit(app.exec()) diff --git a/examples/declarative/editingmodel/main.pyproject b/examples/declarative/editingmodel/main.pyproject new file mode 100644 index 000000000..71272a973 --- /dev/null +++ b/examples/declarative/editingmodel/main.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["model.py","main.qml","main.py","MovingRectangle.qml"] +} diff --git a/examples/declarative/editingmodel/main.qml b/examples/declarative/editingmodel/main.qml new file mode 100644 index 000000000..8624be6cf --- /dev/null +++ b/examples/declarative/editingmodel/main.qml @@ -0,0 +1,143 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt for Python examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Window +import BaseModel + +Window { + title: "Moving Rectangle" + width: 800 + height: 480 + visible: true + id: mainWindow + + Column { + spacing: 20 + anchors.fill: parent + id: mainColumn + Text { + padding: 20 + font.pointSize: 10 + width: 600 + wrapMode: Text.Wrap + text: "This example shows how to add, remove and move items inside a QML ListView.\n +It shows and edits data via roles using QAbstractListModel on the Python side.\n +Use the 'Middle click' on top of a rectangle to remove an item.\n +'Left click' and drag to move the items." + } + + Button { + anchors { + left: mainColumn.left + right: mainColumn.right + margins: 30 + } + text: "Reset view" + onClicked: lv.model.reset() + } + + Button { + anchors { + left: mainColumn.left + right: mainColumn.right + margins: 30 + } + text: "Add element" + onClicked: lv.model.append() + } + + ListView { + id: lv + anchors { + left: mainColumn.left + right: mainColumn.right + margins: 30 + } + + height: 200 + model: BaseModel {} + orientation: ListView.Horizontal + displaced: Transition { + NumberAnimation { + properties: "x,y" + easing.type: Easing.OutQuad + } + } + delegate: DropArea { + id: droparea + width: ratio * lv.width + height: lv.height + + onEntered: function (drag) { + let dragindex = drag.source.modelIndex + if (index === dragindex) + return + lv.model.move(dragindex, index) + } + + MovingRectangle { + modelIndex: index + dragParent: lv + sizeParent: droparea + } + } + + MouseArea { + id: lvMousearea + anchors.fill: lv + z: -1 + } + Rectangle { + id: lvBackground + anchors.fill: lv + anchors.margins: -border.width + color: "white" + border.color: "black" + border.width: 5 + z: -1 + } + Component.onCompleted: { + lv.model.reset() + } + } + } +} diff --git a/examples/declarative/editingmodel/model.py b/examples/declarative/editingmodel/model.py new file mode 100644 index 000000000..99736e714 --- /dev/null +++ b/examples/declarative/editingmodel/model.py @@ -0,0 +1,187 @@ +############################################################################# +## +## Copyright (C) 2021 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + + +from PySide6.QtCore import (QAbstractListModel, QByteArray, QModelIndex, Qt, + Slot) +from PySide6.QtGui import QColor + + +class BaseModel(QAbstractListModel): + + RatioRole = Qt.UserRole + 1 + + def __init__(self, parent=None): + super().__init__(parent=parent) + self.db = [] + + def rowCount(self, parent=QModelIndex()): + return len(self.db) + + def roleNames(self): + default = super().roleNames() + default[self.RatioRole] = QByteArray(b"ratio") + default[Qt.BackgroundRole] = QByteArray(b"backgroundColor") + return default + + def data(self, index, role: int): + if not self.db: + ret = None + elif not index.isValid(): + ret = None + elif role == Qt.DisplayRole: + ret = self.db[index.row()]["text"] + elif role == Qt.BackgroundRole: + ret = self.db[index.row()]["bgColor"] + elif role == self.RatioRole: + ret = self.db[index.row()]["ratio"] + else: + ret = None + return ret + + def setData(self, index, value, role): + if not index.isValid(): + return False + if role == Qt.EditRole: + self.db[index.row()]["text"] = value + return True + + @Slot(result=bool) + def append(self): + """Slot to append a row at the end""" + return self.insertRow(self.rowCount()) + + def insertRow(self, row): + """Insert a single row at row""" + return self.insertRows(row, 0) + + def insertRows(self, row: int, count, index=QModelIndex()): + """Insert n rows (n = 1 + count) at row""" + + self.beginInsertRows(QModelIndex(), row, row + count) + + # start database work + if len(self.db): + newid = max(x["id"] for x in self.db) + 1 + else: + newid = 1 + for i in range(count + 1): # at least one row + self.db.insert( + row, {"id": newid, "text": "new", "bgColor": QColor("purple"), "ratio": 0.2} + ) + # end database work + self.endInsertRows() + return True + + @Slot(int, int, result=bool) + def move(self, source: int, target: int): + """Slot to move a single row from source to target""" + return self.moveRow(QModelIndex(), source, QModelIndex(), target) + + def moveRow(self, sourceParent, sourceRow, dstParent, dstChild): + """Move a single row""" + return self.moveRows(sourceParent, sourceRow, 0, dstParent, dstChild) + + def moveRows(self, sourceParent, sourceRow, count, dstParent, dstChild): + """Move n rows (n=1+ count) from sourceRow to dstChild""" + + if sourceRow == dstChild: + return False + + elif sourceRow > dstChild: + end = dstChild + + else: + end = dstChild + 1 + + self.beginMoveRows(QModelIndex(), sourceRow, sourceRow + count, QModelIndex(), end) + + # start database work + pops = self.db[sourceRow : sourceRow + count + 1] + if sourceRow > dstChild: + self.db = ( + self.db[:dstChild] + + pops + + self.db[dstChild:sourceRow] + + self.db[sourceRow + count + 1 :] + ) + else: + start = self.db[:sourceRow] + middle = self.db[dstChild : dstChild + 1] + endlist = self.db[dstChild + count + 1 :] + self.db = start + middle + pops + endlist + # end database work + + self.endMoveRows() + return True + + @Slot(int, result=bool) + def remove(self, row: int): + """Slot to remove one row""" + return self.removeRow(row) + + def removeRow(self, row, parent=QModelIndex()): + """Remove one row at index row""" + return self.removeRows(row, 0, parent) + + def removeRows(self, row: int, count: int, parent=QModelIndex()): + """Remove n rows (n=1+count) starting at row""" + self.beginRemoveRows(QModelIndex(), row, row + count) + + # start database work + self.db = self.db[:row] + self.db[row + count + 1 :] + # end database work + + self.endRemoveRows() + return True + + @Slot(result=bool) + def reset(self): + self.beginResetModel() + self.resetInternalData() # should work without calling it ? + self.endResetModel() + return True + + def resetInternalData(self): + self.db = [ + {"id": 3, "bgColor": QColor("red"), "ratio": 0.15, "text": "first"}, + {"id": 1, "bgColor": QColor("blue"), "ratio": 0.1, "text": "second"}, + {"id": 2, "bgColor": QColor("green"), "ratio": 0.2, "text": "third"}, + ] -- cgit v1.2.3