diff options
Diffstat (limited to 'examples/widgets/itemviews/jsonmodel')
-rw-r--r-- | examples/widgets/itemviews/jsonmodel/doc/jsonmodel.png | bin | 0 -> 14837 bytes | |||
-rw-r--r-- | examples/widgets/itemviews/jsonmodel/doc/jsonmodel.rst | 8 | ||||
-rw-r--r-- | examples/widgets/itemviews/jsonmodel/example.json | 26 | ||||
-rw-r--r-- | examples/widgets/itemviews/jsonmodel/jsonmodel.py | 320 | ||||
-rw-r--r-- | examples/widgets/itemviews/jsonmodel/jsonmodel.pyproject | 3 |
5 files changed, 357 insertions, 0 deletions
diff --git a/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.png b/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.png Binary files differnew file mode 100644 index 000000000..8b5c8d0c3 --- /dev/null +++ b/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.png diff --git a/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.rst b/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.rst new file mode 100644 index 000000000..d5e2831bf --- /dev/null +++ b/examples/widgets/itemviews/jsonmodel/doc/jsonmodel.rst @@ -0,0 +1,8 @@ +JSON Model Example +================== + +Simple example to visualize the values of a JSON file. + +.. image:: jsonmodel.png + :width: 400 + :alt: JSON Model Screenshot 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..6e614c77f --- /dev/null +++ b/examples/widgets/itemviews/jsonmodel/jsonmodel.py @@ -0,0 +1,320 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import json +import sys +from typing import Any, 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, " f"not {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) + + 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"] +} |