aboutsummaryrefslogtreecommitdiffstats
path: root/examples/demos/documentviewer/jsonviewer/jsonviewer.py
diff options
context:
space:
mode:
Diffstat (limited to 'examples/demos/documentviewer/jsonviewer/jsonviewer.py')
-rw-r--r--examples/demos/documentviewer/jsonviewer/jsonviewer.py396
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)