// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause #include "jsonviewer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT #include #include #endif void JsonViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow) { AbstractViewer::init(file, new QTreeView(parent), mainWindow); m_tree = qobject_cast(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(); 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(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(sender()); if (!action) return; const QModelIndex index = qvariant_cast(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(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(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); }