mirror of https://github.com/qt/qtdoc.git
533 lines
14 KiB
C++
533 lines
14 KiB
C++
|
// 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_ABSTRACTVIEWER_PRINTSUPPORT
|
||
|
#include <QPrinter>
|
||
|
#include <QPainter>
|
||
|
#endif
|
||
|
|
||
|
JsonViewer::JsonViewer(QFile *file, QWidget *parent, QMainWindow *mainWindow) :
|
||
|
AbstractViewer(file, new QTreeView(parent), mainWindow)
|
||
|
{
|
||
|
m_tree = qobject_cast<QTreeView *>(widget());
|
||
|
connect(this, &AbstractViewer::uiInitialized, this, &JsonViewer::setupJsonUi);
|
||
|
}
|
||
|
|
||
|
JsonViewer::~JsonViewer()
|
||
|
{
|
||
|
delete m_toplevel;
|
||
|
}
|
||
|
|
||
|
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();
|
||
|
}
|
||
|
|
||
|
#if defined(QT_ABSTRACTVIEWER_PRINTSUPPORT)
|
||
|
void JsonViewer::printDocument(QPrinter *printer) const
|
||
|
{
|
||
|
if (!hasContent())
|
||
|
return;
|
||
|
|
||
|
const QTextDocument doc(m_root.toJson(QJsonDocument::JsonFormat::Indented));
|
||
|
doc.print(printer);
|
||
|
}
|
||
|
|
||
|
#endif // QT_ABSTRACTVIEWER_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);
|
||
|
}
|