aboutsummaryrefslogtreecommitdiffstats
path: root/examples
diff options
context:
space:
mode:
authorSacha Schutz <sacha@labsquare.org>2021-02-13 21:43:05 +0100
committerCristian Maureira-Fredes <cristian.maureira-fredes@qt.io>2021-04-15 14:33:35 +0000
commit80cb8e0a3dd905edbb1226604f3e8b3e31039728 (patch)
tree0c5dcf1b367b56222484b79306311705f5834c11 /examples
parenta434c1852ce3c040e2086568d7331c68b96ea38d (diff)
Add an editable Json Model example
This is an adaptation of my code available on https://github.com/dridk/QJsonModel. Due to its success, it may be good to add it into the official documentation. Pick-to: 6.0 Task-number: PYSIDE-841 Change-Id: I5b9acddb684ba27233efa53e6b0e04291aaba46a Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
Diffstat (limited to 'examples')
-rw-r--r--examples/widgets/itemviews/jsonmodel/example.json26
-rw-r--r--examples/widgets/itemviews/jsonmodel/jsonmodel.py360
-rw-r--r--examples/widgets/itemviews/jsonmodel/jsonmodel.pyproject3
3 files changed, 389 insertions, 0 deletions
diff --git a/examples/widgets/itemviews/jsonmodel/example.json b/examples/widgets/itemviews/jsonmodel/example.json
new file mode 100644
index 000000000..3c3ecfbfd
--- /dev/null
+++ b/examples/widgets/itemviews/jsonmodel/example.json
@@ -0,0 +1,26 @@
+{
+ "id": "0001",
+ "type": "donut",
+ "name": "Cake",
+ "ppu": 0.55,
+ "batters":
+ {
+ "batter":
+ [
+ { "id": "1001", "type": "Regular" },
+ { "id": "1002", "type": "Chocolate" },
+ { "id": "1003", "type": "Blueberry" },
+ { "id": "1004", "type": "Devil's Food" }
+ ]
+ },
+ "topping":
+ [
+ { "id": "5001", "type": "None" },
+ { "id": "5002", "type": "Glazed" },
+ { "id": "5005", "type": "Sugar" },
+ { "id": "5007", "type": "Powdered Sugar" },
+ { "id": "5006", "type": "Chocolate with Sprinkles" },
+ { "id": "5003", "type": "Chocolate" },
+ { "id": "5004", "type": "Maple" }
+ ]
+}
diff --git a/examples/widgets/itemviews/jsonmodel/jsonmodel.py b/examples/widgets/itemviews/jsonmodel/jsonmodel.py
new file mode 100644
index 000000000..16beb3253
--- /dev/null
+++ b/examples/widgets/itemviews/jsonmodel/jsonmodel.py
@@ -0,0 +1,360 @@
+#############################################################################
+##
+## Copyright (C) 2021 The Qt Company Ltd.
+## Contact: https://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 json
+import sys
+from typing import Any, Iterable, List, Dict, Union
+
+from PySide6.QtWidgets import QTreeView, QApplication, QHeaderView
+from PySide6.QtCore import QAbstractItemModel, QModelIndex, QObject, Qt, QFileInfo
+
+
+class TreeItem:
+ """A Json item corresponding to a line in QTreeView"""
+
+ def __init__(self, parent: "TreeItem" = None):
+ self._parent = parent
+ self._key = ""
+ self._value = ""
+ self._value_type = None
+ self._children = []
+
+ def appendChild(self, item: "TreeItem"):
+ """Add item as a child"""
+ self._children.append(item)
+
+ def child(self, row: int) -> "TreeItem":
+ """Return the child of the current item from the given row"""
+ return self._children[row]
+
+ def parent(self) -> "TreeItem":
+ """Return the parent of the current item"""
+ return self._parent
+
+ def childCount(self) -> int:
+ """Return the number of children of the current item"""
+ return len(self._children)
+
+ def row(self) -> int:
+ """Return the row where the current item occupies in the parent"""
+ return self._parent._children.index(self) if self._parent else 0
+
+ @property
+ def key(self) -> str:
+ """Return the key name"""
+ return self._key
+
+ @key.setter
+ def key(self, key: str):
+ """Set key name of the current item"""
+ self._key = key
+
+ @property
+ def value(self) -> str:
+ """Return the value name of the current item"""
+ return self._value
+
+ @value.setter
+ def value(self, value: str):
+ """Set value name of the current item"""
+ self._value = value
+
+ @property
+ def value_type(self):
+ """Return the python type of the item's value."""
+ return self._value_type
+
+ @value_type.setter
+ def value_type(self, value):
+ """Set the python type of the item's value."""
+ self._value_type = value
+
+ @classmethod
+ def load(
+ cls, value: Union[List, Dict], parent: "TreeItem" = None, sort=True
+ ) -> "TreeItem":
+ """Create a 'root' TreeItem from a nested list or a nested dictonary
+
+ Examples:
+ with open("file.json") as file:
+ data = json.dump(file)
+ root = TreeItem.load(data)
+
+ This method is a recursive function that calls itself.
+
+ Returns:
+ TreeItem: TreeItem
+ """
+ rootItem = TreeItem(parent)
+ rootItem.key = "root"
+
+ if isinstance(value, dict):
+ items = sorted(value.items()) if sort else value.items()
+
+ for key, value in items:
+ child = cls.load(value, rootItem)
+ child.key = key
+ child.value_type = type(value)
+ rootItem.appendChild(child)
+
+ elif isinstance(value, list):
+ for index, value in enumerate(value):
+ child = cls.load(value, rootItem)
+ child.key = index
+ child.value_type = type(value)
+ rootItem.appendChild(child)
+
+ else:
+ rootItem.value = value
+ rootItem.value_type = type(value)
+
+ return rootItem
+
+
+class JsonModel(QAbstractItemModel):
+ """ An editable model of Json data """
+
+ def __init__(self, parent: QObject = None):
+ super().__init__(parent)
+
+ self._rootItem = TreeItem()
+ self._headers = ("key", "value")
+
+ def clear(self):
+ """ Clear data from the model """
+ self.load({})
+
+ def load(self, document: dict):
+ """Load model from a nested dictionary returned by json.loads()
+
+ Arguments:
+ document (dict): JSON-compatible dictionary
+ """
+
+ assert isinstance(
+ document, (dict, list, tuple)
+ ), "`document` must be of dict, list or tuple, " "not %s" % type(document)
+
+ self.beginResetModel()
+
+ self._rootItem = TreeItem.load(document)
+ self._rootItem.value_type = type(document)
+
+ self.endResetModel()
+
+ return True
+
+ def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> Any:
+ """Override from QAbstractItemModel
+
+ Return data from a json item according index and role
+
+ """
+ if not index.isValid():
+ return None
+
+ item = index.internalPointer()
+
+ if role == Qt.DisplayRole:
+ if index.column() == 0:
+ return item.key
+
+ if index.column() == 1:
+ return item.value
+
+ elif role == Qt.EditRole:
+ if index.column() == 1:
+ return item.value
+
+ def setData(self, index: QModelIndex, value: Any, role: Qt.ItemDataRole):
+ """Override from QAbstractItemModel
+
+ Set json item according index and role
+
+ Args:
+ index (QModelIndex)
+ value (Any)
+ role (Qt.ItemDataRole)
+
+ """
+ if role == Qt.EditRole:
+ if index.column() == 1:
+ item = index.internalPointer()
+ item.value = str(value)
+
+ if __binding__ in ("PySide", "PyQt4"):
+ self.dataChanged.emit(index, index)
+ else:
+ self.dataChanged.emit(index, index, [Qt.EditRole])
+
+ return True
+
+ return False
+
+ def headerData(
+ self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole
+ ):
+ """Override from QAbstractItemModel
+
+ For the JsonModel, it returns only data for columns (orientation = Horizontal)
+
+ """
+ if role != Qt.DisplayRole:
+ return None
+
+ if orientation == Qt.Horizontal:
+ return self._headers[section]
+
+ def index(self, row: int, column: int, parent=QModelIndex()) -> QModelIndex:
+ """Override from QAbstractItemModel
+
+ Return index according row, column and parent
+
+ """
+ if not self.hasIndex(row, column, parent):
+ return QModelIndex()
+
+ if not parent.isValid():
+ parentItem = self._rootItem
+ else:
+ parentItem = parent.internalPointer()
+
+ childItem = parentItem.child(row)
+ if childItem:
+ return self.createIndex(row, column, childItem)
+ else:
+ return QModelIndex()
+
+ def parent(self, index: QModelIndex) -> QModelIndex:
+ """Override from QAbstractItemModel
+
+ Return parent index of index
+
+ """
+
+ if not index.isValid():
+ return QModelIndex()
+
+ childItem = index.internalPointer()
+ parentItem = childItem.parent()
+
+ if parentItem == self._rootItem:
+ return QModelIndex()
+
+ return self.createIndex(parentItem.row(), 0, parentItem)
+
+ def rowCount(self, parent=QModelIndex()):
+ """Override from QAbstractItemModel
+
+ Return row count from parent index
+ """
+ if parent.column() > 0:
+ return 0
+
+ if not parent.isValid():
+ parentItem = self._rootItem
+ else:
+ parentItem = parent.internalPointer()
+
+ return parentItem.childCount()
+
+ def columnCount(self, parent=QModelIndex()):
+ """Override from QAbstractItemModel
+
+ Return column number. For the model, it always return 2 columns
+ """
+ return 2
+
+ def flags(self, index: QModelIndex) -> Qt.ItemFlags:
+ """Override from QAbstractItemModel
+
+ Return flags of index
+ """
+ flags = super(JsonModel, self).flags(index)
+
+ if index.column() == 1:
+ return Qt.ItemIsEditable | flags
+ else:
+ return flags
+
+ def to_json(self, item=None):
+
+ if item is None:
+ item = self._rootItem
+
+ nchild = item.childCount()
+
+ if item.value_type is dict:
+ document = {}
+ for i in range(nchild):
+ ch = item.child(i)
+ document[ch.key] = self.to_json(ch)
+ return document
+
+ elif item.value_type == list:
+ document = []
+ for i in range(nchild):
+ ch = item.child(i)
+ document.append(self.to_json(ch))
+ return document
+
+ else:
+ return item.value
+
+
+if __name__ == "__main__":
+
+ app = QApplication(sys.argv)
+ view = QTreeView()
+ model = JsonModel()
+
+ view.setModel(model)
+
+ json_path = QFileInfo(__file__).absoluteDir().filePath("example.json")
+
+ with open(json_path) as file:
+ document = json.load(file)
+ model.load(document)
+
+ view.show()
+ view.header().setSectionResizeMode(0, QHeaderView.Stretch)
+ view.setAlternatingRowColors(True)
+ view.resize(500, 300)
+ app.exec_()
diff --git a/examples/widgets/itemviews/jsonmodel/jsonmodel.pyproject b/examples/widgets/itemviews/jsonmodel/jsonmodel.pyproject
new file mode 100644
index 000000000..7d551b31c
--- /dev/null
+++ b/examples/widgets/itemviews/jsonmodel/jsonmodel.pyproject
@@ -0,0 +1,3 @@
+{
+ "files": ["jsonmodel.py", "example.json"]
+}