diff options
Diffstat (limited to 'examples/demos/documentviewer/jsonviewer/jsonviewer.py')
-rw-r--r-- | examples/demos/documentviewer/jsonviewer/jsonviewer.py | 396 |
1 files changed, 396 insertions, 0 deletions
diff --git a/examples/demos/documentviewer/jsonviewer/jsonviewer.py b/examples/demos/documentviewer/jsonviewer/jsonviewer.py new file mode 100644 index 000000000..1e5a34ca0 --- /dev/null +++ b/examples/demos/documentviewer/jsonviewer/jsonviewer.py @@ -0,0 +1,396 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import json + +from PySide6.QtWidgets import (QLabel, QLineEdit, QListWidget, + QListWidgetItem, QMenu, QTreeView) +from PySide6.QtGui import (QAction, QIcon, QKeySequence, + QPixmap, QTextDocument) +from PySide6.QtCore import (QAbstractItemModel, QDir, + QIODevice, QModelIndex, + QPoint, QSize, Qt, Slot) + +from abstractviewer import AbstractViewer + + +def resizeToContents(tree): + for i in range(0, tree.header().count()): + tree.resizeColumnToContents(i) + + +class JsonTreeItem: + + def __init__(self, parent=None): + self._key = "" + self._value = None + self._children = [] + self._parent = parent + + def key(self): + return self._key + + def value(self): + return self._value + + def appendChild(self, item): + self._children.append(item) + + def child(self, row): + return self._children[row] + + def parent(self): + return self._parent + + def childCount(self): + return len(self._children) + + def row(self): + if self._parent: + return self._parent._children.index(self) + return 0 + + def setKey(self, key): + self._key = key + + def setValue(self, value): + self._value = value + + @staticmethod + def load(value, parent=None): + rootItem = JsonTreeItem(parent) + rootItem.setKey("root") + + if isinstance(value, dict): + for key, val in value.items(): + child = JsonTreeItem.load(val, rootItem) + child.setKey(key) + rootItem.appendChild(child) + + elif isinstance(value, list): + for index, val in enumerate(value): + child = JsonTreeItem.load(val, rootItem) + child.setKey(f"{index}") + rootItem.appendChild(child) + + else: + rootItem.setValue(value) + + return rootItem + + +class JsonItemModel(QAbstractItemModel): + + def columnCount(self, index=QModelIndex()): + return 2 + + def itemFromIndex(self, index): + return index.internalPointer() + + def __init__(self, doc, parent): + super().__init__(parent) + self._textItem = JsonTreeItem() + + # Append header lines + self._headers = ["Key", "Value"] + + # Reset the model. Root can either be a value or an array. + self.beginResetModel() + self._textItem = JsonTreeItem.load(doc) if doc else JsonTreeItem() + self.endResetModel() + + def data(self, index, role): + if not index.isValid(): + return None + + item = self.itemFromIndex(index) + 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() + return None + + def headerData(self, section, orientation, role): + return (self._headers[section] + if role == Qt.DisplayRole and orientation == Qt.Horizontal else None) + + def index(self, row, column, parent=QModelIndex()): + if not self.hasIndex(row, column, parent): + return None + + parentItem = JsonTreeItem() + + if not parent.isValid(): + parentItem = self._textItem + else: + parentItem = self.itemFromIndex(parent) + + childItem = parentItem.child(row) + if childItem: + return self.createIndex(row, column, childItem) + return None + + def parent(self, index): + if not index.isValid(): + return None + + childItem = self.itemFromIndex(index) + parentItem = childItem.parent() + + if parentItem == self._textItem: + return QModelIndex() + + return self.createIndex(parentItem.row(), 0, parentItem) + + def rowCount(self, parent=QModelIndex()): + parentItem = JsonTreeItem() + if parent.column() > 0: + return 0 + + if not parent.isValid(): + parentItem = self._textItem + else: + parentItem = self.itemFromIndex(parent) + return parentItem.childCount() + + +class JsonViewer(AbstractViewer): + + def __init__(self): + super().__init__() + self._tree = QTreeView() + self._toplevel = None + self._text = "" + self._searchKey = None + self.uiInitialized.connect(self.setupJsonUi) + + def init(self, file, parent, mainWindow): + self._tree = QTreeView(parent) + super().init(file, self._tree, mainWindow) + + def viewerName(self): + return "JsonViewer" + + def supportedMimeTypes(self): + return ["application/json"] + + @Slot() + def setupJsonUi(self): + # Build Menus and toolbars + menu = self.addMenu("Json") + tb = self.addToolBar("Json Actions") + + zoomInIcon = QIcon.fromTheme(QIcon.ThemeIcon.ZoomIn) + a = menu.addAction(zoomInIcon, "&+Expand all", self._tree.expandAll) + tb.addAction(a) + a.setPriority(QAction.LowPriority) + a.setShortcut(QKeySequence.New) + + zoomOutIcon = QIcon.fromTheme(QIcon.ThemeIcon.ZoomOut) + a = menu.addAction(zoomOutIcon, "&-Collapse all", self._tree.collapseAll) + tb.addAction(a) + a.setPriority(QAction.LowPriority) + a.setShortcut(QKeySequence.New) + + if not self._searchKey: + self._searchKey = QLineEdit(tb) + + label = QLabel(tb) + magnifier = QPixmap(":/icons/images/magnifier.png").scaled(QSize(28, 28)) + label.setPixmap(magnifier) + tb.addWidget(label) + tb.addWidget(self._searchKey) + self._searchKey.textEdited.connect(self._tree.keyboardSearch) + + if not self.openJsonFile(): + return + + # Populate bookmarks with toplevel + self._uiAssets_tabs.clear() + self._toplevel = QListWidget(self._uiAssets_tabs) + self._uiAssets_tabs.addTab(self._toplevel, "Bookmarks") + for i in range(0, self._tree.model().rowCount()): + index = self._tree.model().index(i, 0) + self._toplevel.addItem(index.data()) + item = self._toplevel.item(i) + item.setData(Qt.UserRole, index) + item.setToolTip(f"Toplevel Item {i}") + + self._toplevel.setAcceptDrops(True) + self._tree.setDragEnabled(True) + self._tree.setContextMenuPolicy(Qt.CustomContextMenu) + self._toplevel.setContextMenuPolicy(Qt.CustomContextMenu) + + self._toplevel.itemClicked.connect(self.onTopLevelItemClicked) + self._toplevel.itemDoubleClicked.connect(self.onTopLevelItemDoubleClicked) + self._toplevel.customContextMenuRequested.connect(self.onBookmarkMenuRequested) + self._tree.customContextMenuRequested.connect(self.onJsonMenuRequested) + + # Connect back and forward + self._uiAssets_back.triggered.connect(self._back) + self._uiAssets_forward.triggered.connect(self._forward) + + @Slot() + def _back(self): + index = self._tree.indexAbove(self._tree.currentIndex()) + if index.isValid(): + self._tree.setCurrentIndex(index) + + @Slot() + def _forward(self): + current = self._tree.currentIndex() + next = self._tree.indexBelow(current) + if next.isValid(): + self._tree.setCurrentIndex(next) + return + # Expand last item to go beyond + if not self._tree.isExpanded(current): + self._tree.expand(current) + next = self._tree.indexBelow(current) + if next.isValid(): + self._tree.setCurrentIndex(next) + + def openJsonFile(self): + self.disablePrinting() + file_name = QDir.toNativeSeparators(self._file.fileName()) + type = "open" + self._file.open(QIODevice.ReadOnly) + self._text = self._file.readAll().data().decode("utf-8") + self._file.close() + + data = None + message = None + try: + data = json.loads(self._text) + message = f"Json document {file_name} opened" + model = JsonItemModel(data, self) + self._tree.setModel(model) + except ValueError as e: + message = f"Unable to parse Json document from {file_name}: {e}" + self.statusMessage(message, type) + self.maybeEnablePrinting() + + return self._tree.model() is not None + + def indexOf(self, item): + return QModelIndex(item.data(Qt.UserRole)) + + @Slot(QListWidgetItem) + def onTopLevelItemClicked(self, item): + """Move to the clicked toplevel index""" + # return in the unlikely case that the tree has not been built + if not self._tree.model(): + return + + index = self.indexOf(item) + if not index.isValid(): + return + + self._tree.setCurrentIndex(index) + + @Slot(QListWidgetItem) + def onTopLevelItemDoubleClicked(self, item): + """Toggle double clicked index between collaps/expand""" + + # return in the unlikely case that the tree has not been built + if not self._tree.model(): + return + + index = self.indexOf(item) + if not index.isValid(): + return + + if self._tree.isExpanded(index): + self._tree.collapse(index) + return + + # Make sure the node and all parents are expanded + while index.isValid(): + self._tree.expand(index) + index = index.parent() + + @Slot(QPoint) + def onJsonMenuRequested(self, pos): + index = self._tree.indexAt(pos) + if not index.isValid(): + return + + # Don't show a context menu, if the index is already a bookmark + for i in range(0, self._toplevel.count()): + if self.indexOf(self._toplevel.item(i)) == index: + return + + menu = QMenu(self._tree) + action = QAction("Add bookmark") + action.setData(index) + menu.addAction(action) + action.triggered.connect(self.onBookmarkAdded) + menu.exec(self._tree.mapToGlobal(pos)) + + @Slot(QPoint) + def onBookmarkMenuRequested(self, pos): + item = self._toplevel.itemAt(pos) + if not item: + return + + # Don't delete toplevel items + index = self.indexOf(item) + if not index.parent().isValid(): + return + + menu = QMenu() + action = QAction("Delete bookmark") + action.setData(self._toplevel.row(item)) + menu.addAction(action) + action.triggered.connect(self.onBookmarkDeleted) + menu.exec(self._toplevel.mapToGlobal(pos)) + + @Slot() + def onBookmarkAdded(self): + action = self.sender() + if not action: + return + + index = action.data() + if not index.isValid(): + return + + item = QListWidgetItem(index.data(Qt.DisplayRole), self._toplevel) + item.setData(Qt.UserRole, index) + + # Set a tooltip that shows where the item is located in the tree + parent = index.parent() + tooltip = index.data(Qt.DisplayRole).toString() + while parent.isValid(): + tooltip = parent.data(Qt.DisplayRole).toString() + "." + tooltip + parent = parent.parent() + + item.setToolTip(tooltip) + + @Slot() + def onBookmarkDeleted(self): + action = self.sender() + if not action: + return + + row = action.data().toInt() + if row < 0 or row >= self._toplevel.count(): + return + + self._toplevel.takeItem(row) + + def hasContent(self): + return bool(self._text) + + def supportsOverview(self): + return True + + def printDocument(self, printer): + if not self.hasContent(): + return + doc = QTextDocument(self._text) + doc.print_(printer) |