diff options
Diffstat (limited to 'examples/widgets/itemviews/jsonmodel/jsonmodel.py')
-rw-r--r-- | examples/widgets/itemviews/jsonmodel/jsonmodel.py | 320 |
1 files changed, 320 insertions, 0 deletions
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() |