diff options
Diffstat (limited to 'examples/demos/documentviewer/plugins/jsonviewer/jsonviewer.cpp')
-rw-r--r-- | examples/demos/documentviewer/plugins/jsonviewer/jsonviewer.cpp | 537 |
1 files changed, 537 insertions, 0 deletions
diff --git a/examples/demos/documentviewer/plugins/jsonviewer/jsonviewer.cpp b/examples/demos/documentviewer/plugins/jsonviewer/jsonviewer.cpp new file mode 100644 index 00000000..55160a56 --- /dev/null +++ b/examples/demos/documentviewer/plugins/jsonviewer/jsonviewer.cpp @@ -0,0 +1,537 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "jsonviewer.h" +#include <QJsonDocument> +#include <QJsonArray> +#include <QJsonValue> +#include <QJsonObject> +#include <QTreeView> +#include <QMenu> +#include <QToolBar> +#include <QHeaderView> +#include <QListWidget> +#include <QEvent> +#include <QMouseEvent> +#include <QDrag> +#include <QMimeData> +#include <QLineEdit> +#include <QLabel> +#include <QApplication> +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +#include <QPrinter> +#include <QPainter> +#endif + +void JsonViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow) +{ + AbstractViewer::init(file, new QTreeView(parent), mainWindow); + m_tree = qobject_cast<QTreeView *>(widget()); + connect(this, &AbstractViewer::uiInitialized, this, &JsonViewer::setupJsonUi); +} + +JsonViewer::~JsonViewer() +{ + delete m_toplevel; +} + +QStringList JsonViewer::supportedMimeTypes() const +{ + return {"application/json"}; +} + +void JsonViewer::setupJsonUi() +{ + // Build Menus and toolbars + QMenu *menu = addMenu("Json"); + QToolBar *tb = addToolBar(tr("Json Actions")); + + const QIcon zoomInIcon = QIcon::fromTheme("zoom-in"); + QAction *a = menu->addAction(zoomInIcon, tr("&+Expand all"), m_tree, &QTreeView::expandAll); + tb->addAction(a); + a->setPriority(QAction::LowPriority); + a->setShortcut(QKeySequence::New); + + const QIcon zoomOutIcon = QIcon::fromTheme("zoom-out"); + a = menu->addAction(zoomOutIcon, tr("&-Collapse all"), m_tree, &QTreeView::collapseAll); + tb->addAction(a); + a->setPriority(QAction::LowPriority); + a->setShortcut(QKeySequence::New); + + if (!m_searchKey) { + m_searchKey = new QLineEdit(tb); + } + auto *label = new QLabel(tb); + const QPixmap magnifier = QPixmap(":/icons/images/magnifier.png").scaled(QSize(28, 28)); + label->setPixmap(magnifier); + tb->addWidget(label); + tb->addWidget(m_searchKey); + connect(m_searchKey, &QLineEdit::textEdited, m_tree, &QTreeView::keyboardSearch); + + openJsonFile(); + + if (m_root.isEmpty()) + return; + + // Populate bookmarks with toplevel + m_uiAssets.tabs->clear(); + m_toplevel = new QListWidget(m_uiAssets.tabs); + m_uiAssets.tabs->addTab(m_toplevel, "Bookmarks"); + qRegisterMetaType<QModelIndex>(); + for (int i = 0; i < m_tree->model()->rowCount(); ++i) { + const auto &index = m_tree->model()->index(i, 0); + m_toplevel->addItem(index.data().toString()); + auto *item = m_toplevel->item(i); + item->setData(Qt::UserRole, index); + item->setToolTip(QString("Toplevel Item %1").arg(i)); + } + m_toplevel->setAcceptDrops(true); + m_tree->setDragEnabled(true); + m_tree->setContextMenuPolicy(Qt::CustomContextMenu); + m_toplevel->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(m_toplevel, &QListWidget::itemClicked, this, &JsonViewer::onTopLevelItemClicked); + connect(m_toplevel, &QListWidget::itemDoubleClicked, this, &JsonViewer::onTopLevelItemDoubleClicked); + connect(m_toplevel, &QListWidget::customContextMenuRequested, this, &JsonViewer::onBookmarkMenuRequested); + connect(m_tree, &QTreeView::customContextMenuRequested, this, &JsonViewer::onJsonMenuRequested); + + // Connect back and forward + connect(m_uiAssets.back, &QAction::triggered, m_tree, [&](){ + const QModelIndex &index = m_tree->indexAbove(m_tree->currentIndex()); + if (index.isValid()) + m_tree->setCurrentIndex(index); + }); + connect(m_uiAssets.forward, &QAction::triggered, m_tree, [&](){ + QModelIndex current = m_tree->currentIndex(); + QModelIndex next = m_tree->indexBelow(current); + if (next.isValid()) { + m_tree->setCurrentIndex(next); + return; + } + + // Expand last item to go beyond + if (!m_tree->isExpanded(current)) { + m_tree->expand(current); + QModelIndex next = m_tree->indexBelow(current); + if (next.isValid()) { + m_tree->setCurrentIndex(next); + } + } + }); +} + +void resizeToContents(QTreeView *tree) +{ + for (int i = 0; i < tree->header()->count(); ++i) + tree->resizeColumnToContents(i); +} + +bool JsonViewer::openJsonFile() +{ + disablePrinting(); + + QJsonParseError err; + m_file->open(QIODevice::ReadOnly); + m_root = QJsonDocument::fromJson(m_file->readAll(), &err); + const QString type = tr("open"); + if (err.error != QJsonParseError::NoError) { + statusMessage(tr("Unable to parse Json document from %1. %2").arg( + m_file->fileName(), err.errorString()), type); + return false; + } + + statusMessage(tr("Json document %1 opened").arg(m_file->fileName()), type); + m_file->close(); + + maybeEnablePrinting(); + + JsonItemModel *model = new JsonItemModel(m_root, this); + m_tree->setModel(model); + + return true; +} + +QModelIndex indexOf(const QListWidgetItem *item) +{ + return qvariant_cast<QModelIndex>(item->data(Qt::UserRole)); +} + +// Move to the clicked toplevel index +void JsonViewer::onTopLevelItemClicked(QListWidgetItem *item) +{ + // return in the unlikely case that the tree has not been built + if (Q_UNLIKELY(!m_tree->model())) + return; + + auto index = indexOf(item); + if (Q_UNLIKELY(!index.isValid())) + return; + + m_tree->setCurrentIndex(index); +} + +// Toggle double clicked index between collaps/expand +void JsonViewer::onTopLevelItemDoubleClicked(QListWidgetItem *item) +{ + // return in the unlikely case that the tree has not been built + if (Q_UNLIKELY(!m_tree->model())) + return; + + auto index = indexOf(item); + if (Q_UNLIKELY(!index.isValid())) + return; + + if (m_tree->isExpanded(index)) { + m_tree->collapse(index); + return; + } + + // Make sure the node and all parents are expanded + while (index.isValid()) { + m_tree->expand(index); + index = index.parent(); + } +} + +void JsonViewer::onJsonMenuRequested(const QPoint &pos) +{ + const auto &index = m_tree->indexAt(pos); + if (!index.isValid()) + return; + + // Don't show a context menu, if the index is already a bookmark + for (int i = 0; i < m_toplevel->count(); ++i) { + if (indexOf(m_toplevel->item(i)) == index) + return; + } + + QMenu menu(m_tree); + QAction *action = new QAction("Add bookmark"); + action->setData(index); + menu.addAction(action); + connect(action, &QAction::triggered, this, &JsonViewer::onBookmarkAdded); + menu.exec(m_tree->mapToGlobal(pos)); +} + +void JsonViewer::onBookmarkMenuRequested(const QPoint &pos) +{ + auto *item = m_toplevel->itemAt(pos); + if (!item) + return; + + // Don't delete toplevel items + const QModelIndex index = indexOf(item); + if (!index.parent().isValid()) + return; + + QMenu menu; + QAction *action = new QAction("Delete bookmark"); + action->setData(m_toplevel->row(item)); + menu.addAction(action); + connect(action, &QAction::triggered, this, &JsonViewer::onBookmarkDeleted); + menu.exec(m_toplevel->mapToGlobal(pos)); +} + +void JsonViewer::onBookmarkAdded() +{ + const QAction *action = qobject_cast<QAction *>(sender()); + if (!action) + return; + + const QModelIndex index = qvariant_cast<QModelIndex>(action->data()); + if (!index.isValid()) + return; + + auto *item = new QListWidgetItem(index.data(Qt::DisplayRole).toString(), m_toplevel); + item->setData(Qt::UserRole, index); + + // Set a tooltip that shows where the item is located in the tree + QModelIndex parent = index.parent(); + QString tooltip = index.data(Qt::DisplayRole).toString(); + while (parent.isValid()) { + tooltip = parent.data(Qt::DisplayRole).toString() + "->" + tooltip; + parent = parent.parent(); + } + item->setToolTip(tooltip); +} + +void JsonViewer::onBookmarkDeleted() +{ + const QAction *action = qobject_cast<QAction *>(sender()); + if (!action) + return; + + const int row = action->data().toInt(); + if (row < 0 || row >= m_toplevel->count()) + return; + + delete m_toplevel->takeItem(row); +} + +bool JsonViewer::hasContent() const +{ + return !m_root.isEmpty(); +} + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +void JsonViewer::printDocument(QPrinter *printer) const +{ + if (!hasContent()) + return; + + const QTextDocument doc(m_root.toJson(QJsonDocument::JsonFormat::Indented)); + doc.print(printer); +} + +#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT + +QByteArray JsonViewer::saveState() const +{ + QByteArray array; + QDataStream stream(&array, QIODevice::WriteOnly); + stream << QString(viewerName()); + stream << m_tree->header()->saveState(); + return array; +} + +bool JsonViewer::restoreState(QByteArray &array) +{ + QDataStream stream(&array, QIODevice::ReadOnly); + QString viewer; + stream >> viewer; + if (viewer != viewerName()) + return false; + QByteArray header; + stream >> header; + return m_tree->header()->restoreState(header); +} + +JsonTreeItem::JsonTreeItem(JsonTreeItem *parent) +{ + m_parent = parent; +} + +JsonTreeItem::~JsonTreeItem() +{ + qDeleteAll(m_children); +} + +void JsonTreeItem::appendChild(JsonTreeItem *item) +{ + m_children.append(item); +} + +JsonTreeItem *JsonTreeItem::child(int row) +{ + return m_children.value(row); +} + +JsonTreeItem *JsonTreeItem::parent() +{ + return m_parent; +} + +int JsonTreeItem::childCount() const +{ + return m_children.count(); +} + +int JsonTreeItem::row() const +{ + if (m_parent) + return m_parent->m_children.indexOf(const_cast<JsonTreeItem*>(this)); + + return 0; +} + +void JsonTreeItem::setKey(const QString &key) +{ + m_key = key; +} + +void JsonTreeItem::setValue(const QVariant &value) +{ + m_value = value; +} + +void JsonTreeItem::setType(const QJsonValue::Type &type) +{ + m_type = type; +} + +JsonTreeItem* JsonTreeItem::load(const QJsonValue& value, JsonTreeItem* parent) +{ + JsonTreeItem *rootItem = new JsonTreeItem(parent); + rootItem->setKey("root"); + + if (value.isObject()) { + const QStringList &keys = value.toObject().keys(); + for (const QString &key : keys) { + QJsonValue v = value.toObject().value(key); + JsonTreeItem *child = load(v, rootItem); + child->setKey(key); + child->setType(v.type()); + rootItem->appendChild(child); + } + } else if (value.isArray()) { + int index = 0; + const QJsonArray &array = value.toArray(); + for (const QJsonValue &val : array) { + JsonTreeItem *child = load(val, rootItem); + child->setKey(QString::number(index)); + child->setType(val.type()); + rootItem->appendChild(child); + ++index; + } + } else { + rootItem->setValue(value.toVariant()); + rootItem->setType(value.type()); + } + + return rootItem; +} + +JsonItemModel::JsonItemModel(QObject *parent) + : QAbstractItemModel(parent) + , m_rootItem{new JsonTreeItem} +{ + m_headers.append("Key"); + m_headers.append("Value"); +} + +JsonItemModel::JsonItemModel(const QJsonDocument &doc, QObject *parent) + : QAbstractItemModel(parent) + , m_rootItem{new JsonTreeItem} +{ + // Append header lines and return on empty document + m_headers.append("Key"); + m_headers.append("Value"); + if (doc.isNull()) + return; + + // Reset the model. Root can either be a value or an array. + beginResetModel(); + delete m_rootItem; + if (doc.isArray()) { + m_rootItem = JsonTreeItem::load(QJsonValue(doc.array())); + m_rootItem->setType(QJsonValue::Array); + + } else { + m_rootItem = JsonTreeItem::load(QJsonValue(doc.object())); + m_rootItem->setType(QJsonValue::Object); + } + endResetModel(); +} + +JsonItemModel::~JsonItemModel() +{ + delete m_rootItem; +} + +QVariant JsonItemModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return {}; + + JsonTreeItem *item = itemFromIndex(index); + + switch (role) { + case Qt::DisplayRole: + if (index.column() == 0) + return item->key(); + if (index.column() == 1) + return item->value(); + break; + case Qt::EditRole: + if (index.column() == 1) + return item->value(); + break; + default: + break; + } + return {}; +} + +bool JsonItemModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + const int column = index.column(); + if (Qt::EditRole == role && column == 1) { + JsonTreeItem *item = itemFromIndex(index); + item->setValue(value); + emit dataChanged(index, index, {Qt::EditRole}); + return true; + } + return false; +} + +QVariant JsonItemModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return {}; + + if (orientation == Qt::Horizontal) + return m_headers.value(section); + else + return {}; +} + +QModelIndex JsonItemModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) + return {}; + + JsonTreeItem *parentItem; + + if (!parent.isValid()) + parentItem = m_rootItem; + else + parentItem = itemFromIndex(parent); + + JsonTreeItem *childItem = parentItem->child(row); + if (childItem) + return createIndex(row, column, childItem); + else + return {}; +} + +QModelIndex JsonItemModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return {}; + + JsonTreeItem *childItem = itemFromIndex(index); + JsonTreeItem *parentItem = childItem->parent(); + + if (parentItem == m_rootItem) + return QModelIndex(); + + return createIndex(parentItem->row(), 0, parentItem); +} + +int JsonItemModel::rowCount(const QModelIndex &parent) const +{ + JsonTreeItem *parentItem; + if (parent.column() > 0) + return 0; + + if (!parent.isValid()) + parentItem = m_rootItem; + else + parentItem = itemFromIndex(parent); + + return parentItem->childCount(); +} + +Qt::ItemFlags JsonItemModel::flags(const QModelIndex &index) const +{ + int col = index.column(); + auto *item = itemFromIndex(index); + + auto isArray = QJsonValue::Array == item->type(); + auto isObject = QJsonValue::Object == item->type(); + + if ((col == 1) && !(isArray || isObject)) + return Qt::ItemIsEditable | QAbstractItemModel::flags(index); + else + return QAbstractItemModel::flags(index); +} |