making clangd happy and making script to compile/build with run.sh
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# Copyright (C) 2023 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
add_subdirectory(jsonviewer)
|
||||
add_subdirectory(txtviewer)
|
||||
|
||||
if(TARGET Qt6::PdfWidgets)
|
||||
add_subdirectory(pdfviewer)
|
||||
endif()
|
||||
@@ -0,0 +1,35 @@
|
||||
# Copyright (C) 2023 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
qt_add_plugin(jsonviewer
|
||||
CLASS_NAME JsonViewer
|
||||
jsonviewer.cpp jsonviewer.h
|
||||
)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets
|
||||
OPTIONAL_COMPONENTS PrintSupport)
|
||||
|
||||
set_target_properties(jsonviewer PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/app"
|
||||
)
|
||||
|
||||
target_include_directories(jsonviewer PRIVATE
|
||||
../../app
|
||||
)
|
||||
|
||||
target_link_libraries(jsonviewer PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
abstractviewer
|
||||
)
|
||||
|
||||
if(TARGET Qt6::PrintSupport)
|
||||
target_link_libraries(jsonviewer PRIVATE Qt6::PrintSupport)
|
||||
endif()
|
||||
|
||||
install(TARGETS jsonviewer
|
||||
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}/plugins"
|
||||
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}/plugins"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include "jsonviewer.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QToolBar>
|
||||
#include <QTreeView>
|
||||
|
||||
#include <QDrag>
|
||||
#include <QEvent>
|
||||
#include <QMouseEvent>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QMimeData>
|
||||
|
||||
#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
#include <QPrinter>
|
||||
#include <QPainter>
|
||||
#endif
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
JsonViewer::JsonViewer()
|
||||
{
|
||||
connect(this, &AbstractViewer::uiInitialized, this, &JsonViewer::setupJsonUi);
|
||||
}
|
||||
|
||||
void JsonViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow)
|
||||
{
|
||||
AbstractViewer::init(file, new QTreeView(parent), mainWindow);
|
||||
m_tree = qobject_cast<QTreeView *>(widget());
|
||||
}
|
||||
|
||||
JsonViewer::~JsonViewer()
|
||||
{
|
||||
delete m_toplevel;
|
||||
}
|
||||
|
||||
QStringList JsonViewer::supportedMimeTypes() const
|
||||
{
|
||||
return {"application/json"_L1};
|
||||
}
|
||||
|
||||
void JsonViewer::setupJsonUi()
|
||||
{
|
||||
// Build Menus and toolbars
|
||||
QMenu *menu = addMenu(tr("Json"));
|
||||
QToolBar *tb = addToolBar(tr("Json Actions"));
|
||||
|
||||
const QIcon zoomInIcon = QIcon::fromTheme("zoom-in"_L1);
|
||||
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"_L1);
|
||||
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"_L1).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, tr("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(tr("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(QDir::toNativeSeparators(m_file->fileName()),
|
||||
err.errorString()), type);
|
||||
return false;
|
||||
}
|
||||
|
||||
statusMessage(tr("Json document %1 opened")
|
||||
.arg(QDir::toNativeSeparators(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(tr("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(tr("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() + "->"_L1 + 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(QString::fromUtf8(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"_L1);
|
||||
|
||||
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"_L1);
|
||||
m_headers.append("Value"_L1);
|
||||
}
|
||||
|
||||
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"_L1);
|
||||
m_headers.append("Value"_L1);
|
||||
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 {};
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#ifndef JSONVIEWER_H
|
||||
#define JSONVIEWER_H
|
||||
|
||||
#include "viewerinterfaces.h"
|
||||
|
||||
#include <QJsonValue>
|
||||
#include <QJsonDocument>
|
||||
#include <QAbstractItemModel>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QTreeView;
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
class QLineEdit;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class JsonViewer : public ViewerInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.DocumentViewer.ViewerInterface/1.0" FILE "jsonviewer.json")
|
||||
Q_INTERFACES(ViewerInterface)
|
||||
public:
|
||||
JsonViewer();
|
||||
~JsonViewer() override;
|
||||
|
||||
void init(QFile *file, QWidget *parent, QMainWindow *mainWindow) override;
|
||||
QString viewerName() const override { return QLatin1StringView(staticMetaObject.className()); };
|
||||
QStringList supportedMimeTypes() const override;
|
||||
QByteArray saveState() const override;
|
||||
bool restoreState(QByteArray &) override;
|
||||
bool supportsOverview() const override { return true; }
|
||||
bool hasContent() const override;
|
||||
|
||||
#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
protected:
|
||||
void printDocument(QPrinter *printer) const override;
|
||||
#endif // QT_ABSTRACTVIEWER_PRINTSUPPORT
|
||||
|
||||
private slots:
|
||||
void setupJsonUi();
|
||||
void onTopLevelItemClicked(QListWidgetItem *item);
|
||||
void onTopLevelItemDoubleClicked(QListWidgetItem *item);
|
||||
void onJsonMenuRequested(const QPoint &pos);
|
||||
void onBookmarkMenuRequested(const QPoint &pos);
|
||||
void onBookmarkAdded();
|
||||
void onBookmarkDeleted();
|
||||
|
||||
private:
|
||||
bool openJsonFile();
|
||||
|
||||
QTreeView *m_tree;
|
||||
QListWidget *m_toplevel = nullptr;
|
||||
QJsonDocument m_root;
|
||||
|
||||
QPointer<QLineEdit> m_searchKey;
|
||||
};
|
||||
|
||||
class JsonTreeItem
|
||||
{
|
||||
public:
|
||||
JsonTreeItem(JsonTreeItem *parent = nullptr);
|
||||
~JsonTreeItem();
|
||||
void appendChild(JsonTreeItem *item);
|
||||
JsonTreeItem *child(int row);
|
||||
JsonTreeItem *parent();
|
||||
int childCount() const;
|
||||
int row() const;
|
||||
void setKey(const QString& key);
|
||||
void setValue(const QVariant& value);
|
||||
void setType(const QJsonValue::Type& type);
|
||||
QString key() const { return m_key; };
|
||||
QVariant value() const { return m_value; };
|
||||
QJsonValue::Type type() const { return m_type; };
|
||||
|
||||
static JsonTreeItem* load(const QJsonValue& value, JsonTreeItem *parent = nullptr);
|
||||
|
||||
private:
|
||||
QString m_key;
|
||||
QVariant m_value;
|
||||
QJsonValue::Type m_type;
|
||||
QList<JsonTreeItem*> m_children;
|
||||
JsonTreeItem *m_parent = nullptr;
|
||||
};
|
||||
|
||||
class JsonItemModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit JsonItemModel(QObject *parent = nullptr);
|
||||
JsonItemModel(const QJsonDocument& doc, QObject *parent = nullptr);
|
||||
~JsonItemModel();
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
||||
QModelIndex index(int row, int column,const QModelIndex &parent = QModelIndex()) const override;
|
||||
QModelIndex parent(const QModelIndex &index) const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex & = QModelIndex()) const override { return 2; };
|
||||
|
||||
private:
|
||||
JsonTreeItem *m_rootItem = nullptr;
|
||||
QStringList m_headers;
|
||||
static JsonTreeItem *itemFromIndex(const QModelIndex &index)
|
||||
{return static_cast<JsonTreeItem*>(index.internalPointer()); }
|
||||
};
|
||||
|
||||
#endif //JSONVIEWER_H
|
||||
@@ -0,0 +1 @@
|
||||
{ "Keys": [ "jsonviewer" ] }
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright (C) 2023 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets PdfWidgets
|
||||
OPTIONAL_COMPONENTS PrintSupport)
|
||||
|
||||
qt_add_plugin(pdfviewer
|
||||
CLASS_NAME PdfViewer
|
||||
pdfviewer.cpp pdfviewer.h
|
||||
zoomselector.cpp zoomselector.h
|
||||
hoverwatcher.cpp hoverwatcher.h
|
||||
)
|
||||
|
||||
set_target_properties(pdfviewer PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/app"
|
||||
)
|
||||
|
||||
target_include_directories(pdfviewer PRIVATE
|
||||
../../app
|
||||
..
|
||||
)
|
||||
|
||||
target_link_libraries(pdfviewer PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
Qt6::PdfWidgets
|
||||
abstractviewer
|
||||
)
|
||||
|
||||
if(TARGET Qt6::PrintSupport)
|
||||
target_link_libraries(pdfviewer PRIVATE Qt6::PrintSupport)
|
||||
endif()
|
||||
|
||||
install(TARGETS pdfviewer
|
||||
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}/plugins"
|
||||
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}/plugins"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
#include "hoverwatcher.h"
|
||||
#include <QGuiApplication>
|
||||
#include <QWidget>
|
||||
#include <QMouseEvent>
|
||||
|
||||
HoverWatcher::HoverWatcher(QWidget *watched)
|
||||
: QObject(watched), m_watched(watched)
|
||||
{
|
||||
Q_ASSERT(watched);
|
||||
m_cursorShapes[Entered].emplace(Qt::OpenHandCursor);
|
||||
m_cursorShapes[MousePress].emplace(Qt::ClosedHandCursor);
|
||||
m_cursorShapes[MouseRelease].emplace(Qt::OpenHandCursor);
|
||||
// no default for Left => restore override cursor
|
||||
m_watched->installEventFilter(this);
|
||||
}
|
||||
|
||||
HoverWatcher::~HoverWatcher()
|
||||
{
|
||||
m_watched->removeEventFilter(this);
|
||||
}
|
||||
|
||||
typedef QHash<QWidget *, HoverWatcher*> WatchMap;
|
||||
Q_GLOBAL_STATIC(WatchMap, qt_allHoverWatchers)
|
||||
|
||||
HoverWatcher *HoverWatcher::watcher(QWidget *watched)
|
||||
{
|
||||
if (qt_allHoverWatchers()->contains(watched))
|
||||
return qt_allHoverWatchers()->value(watched);
|
||||
|
||||
HoverWatcher *watcher = new HoverWatcher(watched);
|
||||
qt_allHoverWatchers()->insert(watched, watcher);
|
||||
return watcher;
|
||||
}
|
||||
|
||||
/*!
|
||||
\overload Const version of watcher
|
||||
*/
|
||||
const HoverWatcher *HoverWatcher::watcher(const QWidget *watched)
|
||||
{
|
||||
return watcher(const_cast<QWidget *>(watched));
|
||||
}
|
||||
|
||||
void HoverWatcher::dismiss(QWidget *watched)
|
||||
{
|
||||
if (!hasWatcher(watched))
|
||||
return;
|
||||
|
||||
delete qt_allHoverWatchers()->take(watched);
|
||||
}
|
||||
|
||||
bool HoverWatcher::hasWatcher(QWidget *widget)
|
||||
{
|
||||
return qt_allHoverWatchers()->contains(widget);
|
||||
}
|
||||
|
||||
static constexpr HoverWatcher::HoverAction toHoverAction(QEvent::Type et)
|
||||
{
|
||||
switch (et) {
|
||||
case QEvent::Type::Enter:
|
||||
return HoverWatcher::HoverAction::Entered;
|
||||
case QEvent::Type::Leave:
|
||||
return HoverWatcher::HoverAction::Left;
|
||||
case QEvent::Type::MouseButtonPress:
|
||||
return HoverWatcher::HoverAction::MousePress;
|
||||
case QEvent::Type::MouseButtonRelease:
|
||||
return HoverWatcher::HoverAction::MouseRelease;
|
||||
default:
|
||||
return HoverWatcher::HoverAction::Ignore;
|
||||
}
|
||||
}
|
||||
|
||||
void HoverWatcher::handleAction (HoverWatcher::HoverAction action)
|
||||
{
|
||||
const Qt::CursorShape newShape = cursorShape(action);
|
||||
if (QGuiApplication::overrideCursor()
|
||||
&& (QGuiApplication::overrideCursor()->shape() == newShape
|
||||
|| action == HoverAction::Ignore)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QGuiApplication::setOverrideCursor(cursorShape(action));
|
||||
emit hoverAction(action);
|
||||
|
||||
switch (action) {
|
||||
case HoverAction::Entered:
|
||||
emit entered();
|
||||
break;
|
||||
case HoverAction::Left:
|
||||
emit left();
|
||||
break;
|
||||
case HoverAction::MousePress:
|
||||
emit mousePressed();
|
||||
break;
|
||||
case HoverAction::MouseRelease: {
|
||||
emit mouseReleased();
|
||||
}
|
||||
break;
|
||||
case HoverAction::Ignore:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool HoverWatcher::hasShape(HoverAction action) const
|
||||
{
|
||||
return action != HoverAction::Ignore && m_cursorShapes[action].has_value();
|
||||
}
|
||||
|
||||
void HoverWatcher::setApplicationCursor(HoverAction action) const
|
||||
{
|
||||
if (!hasShape(action)) {
|
||||
QGuiApplication::restoreOverrideCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
QGuiApplication::setOverrideCursor(cursorShape(action));
|
||||
}
|
||||
|
||||
bool HoverWatcher::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
Q_ASSERT(obj == m_watched); // don't install event filters elsewhere
|
||||
|
||||
// Ignore irrelevant events
|
||||
const auto action = toHoverAction(event->type());
|
||||
if (action == HoverAction::Ignore)
|
||||
return false;
|
||||
|
||||
// React to a QScroller having been installed or removed
|
||||
// A Scroller sends a fake mouse release to QPoint (-1, -1)
|
||||
// => needs to be ignored and end of scrolling processed instead
|
||||
static bool hasScroller = false;
|
||||
if (QScroller::hasScroller(m_watched) != hasScroller) {
|
||||
hasScroller = QScroller::hasScroller(m_watched);
|
||||
static QMetaObject::Connection con;
|
||||
if (hasScroller) {
|
||||
con = connect(QScroller::scroller(m_watched), &QScroller::stateChanged,
|
||||
this, &HoverWatcher::handleScrollerStateChange);
|
||||
} else {
|
||||
disconnect(con);
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore fake mouse release event sent by scroller
|
||||
if (action == HoverAction::MouseRelease && hasScroller) {
|
||||
QMouseEvent *me = static_cast<QMouseEvent *>(event);
|
||||
if (me->pos().x() < -9000000 )
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore unpermitted mouse buttons
|
||||
if (action == HoverAction::MousePress) {
|
||||
QMouseEvent *me = static_cast<QMouseEvent *>(event);
|
||||
if (!m_mouseButtons.testFlag(me->button()))
|
||||
return false;
|
||||
}
|
||||
|
||||
handleAction(action);
|
||||
return false;
|
||||
}
|
||||
|
||||
Qt::CursorShape HoverWatcher::cursorShape(HoverAction type) const
|
||||
{
|
||||
const Qt::CursorShape fallback = Qt::ArrowCursor;
|
||||
if (type == HoverAction::Ignore)
|
||||
return fallback;
|
||||
|
||||
return m_cursorShapes[type].value_or(fallback);
|
||||
}
|
||||
|
||||
void HoverWatcher::setCursorShape(HoverAction type, Qt::CursorShape shape)
|
||||
{
|
||||
if (type == HoverAction::Ignore)
|
||||
return;
|
||||
m_cursorShapes[type].emplace(shape);
|
||||
}
|
||||
|
||||
void HoverWatcher::unSetCursorShape(HoverAction type)
|
||||
{
|
||||
if (type == HoverAction::Ignore)
|
||||
return;
|
||||
m_cursorShapes[type].reset();
|
||||
}
|
||||
|
||||
void HoverWatcher::setMouseButtons(Qt::MouseButtons buttons)
|
||||
{
|
||||
m_mouseButtons = buttons;
|
||||
}
|
||||
|
||||
void HoverWatcher::setMouseButton(Qt::MouseButton button, bool enable)
|
||||
{
|
||||
m_mouseButtons.setFlag(button, enable);;
|
||||
}
|
||||
|
||||
/*!
|
||||
\brief This slot handles a QScroller state change, in case the watched
|
||||
widget uses a scroller. It translates \param state into the appropriate
|
||||
action.
|
||||
*/
|
||||
void HoverWatcher::handleScrollerStateChange(QScroller::State state)
|
||||
{
|
||||
switch (state) {
|
||||
case QScroller::State::Pressed:
|
||||
case QScroller::State::Dragging:
|
||||
case QScroller::State::Scrolling:
|
||||
handleAction(HoverAction::MousePress);
|
||||
break;
|
||||
case QScroller::State::Inactive:
|
||||
handleAction(HoverAction::MouseRelease);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#ifndef HOVERWATCHER_H
|
||||
#define HOVERWATCHER_H
|
||||
#include <QObject>
|
||||
#include <QEvent>
|
||||
#include <QScroller>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QWidget;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class HoverWatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
explicit HoverWatcher(QWidget *watched);
|
||||
static QMap<QWidget *, HoverWatcher *> m_hoverWatchers;
|
||||
|
||||
public:
|
||||
~HoverWatcher();
|
||||
|
||||
enum HoverAction {
|
||||
Entered,
|
||||
MousePress,
|
||||
MouseRelease,
|
||||
Left,
|
||||
Ignore
|
||||
};
|
||||
Q_ENUM(HoverAction);
|
||||
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
Qt::CursorShape cursorShape(HoverAction type) const;
|
||||
Qt::MouseButtons mouseButtons() const { return m_mouseButtons; }
|
||||
|
||||
static HoverWatcher *watcher(QWidget *watched);
|
||||
static const HoverWatcher *watcher(const QWidget *watched);
|
||||
static bool hasWatcher(QWidget *widget);
|
||||
static void dismiss(QWidget *watched);
|
||||
|
||||
public slots:
|
||||
void setCursorShape(HoverAction type, Qt::CursorShape shape);
|
||||
void unSetCursorShape(HoverAction type);
|
||||
void setMouseButtons(Qt::MouseButtons buttons);
|
||||
void setMouseButton(Qt::MouseButton button, bool enable);
|
||||
|
||||
signals:
|
||||
void entered();
|
||||
void mousePressed();
|
||||
void mouseReleased();
|
||||
void left();
|
||||
void hoverAction(HoverAction action);
|
||||
|
||||
private slots:
|
||||
void handleScrollerStateChange(QScroller::State state);
|
||||
|
||||
private:
|
||||
QWidget *m_watched;
|
||||
std::array<std::optional<Qt::CursorShape>, HoverAction::Ignore> m_cursorShapes;
|
||||
Qt::MouseButtons m_mouseButtons = Qt::MouseButton::LeftButton;
|
||||
void handleAction(HoverAction action);
|
||||
void setApplicationCursor(HoverAction action) const;
|
||||
bool hasShape(HoverAction action) const;
|
||||
};
|
||||
|
||||
#endif // HOVERWATCHER_H
|
||||
@@ -0,0 +1,266 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include "pdfviewer.h"
|
||||
#include "zoomselector.h"
|
||||
#include "hoverwatcher.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QEvent>
|
||||
#include <QFile>
|
||||
#include <QMouseEvent>
|
||||
|
||||
#include <QPdfBookmarkModel>
|
||||
#include <QPdfDocument>
|
||||
#include <QPdfPageNavigator>
|
||||
#include <QPdfView>
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6,6,0)
|
||||
#include <QPdfPageSelector>
|
||||
#endif
|
||||
|
||||
#include <QtMath>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <QListView>
|
||||
#include <QPdfView>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <QtMath>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <QListView>
|
||||
#include <QListWidget>
|
||||
#include <QMainWindow>
|
||||
#include <QScrollBar>
|
||||
#include <QScroller>
|
||||
#include <QSpinBox>
|
||||
#include <QToolBar>
|
||||
#include <QTreeView>
|
||||
|
||||
#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
#include <QPrinter>
|
||||
#include <QPainter>
|
||||
#endif
|
||||
|
||||
Q_LOGGING_CATEGORY(lcExample, "qt.examples.pdfviewer")
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
PdfViewer::PdfViewer()
|
||||
{
|
||||
connect(this, &AbstractViewer::uiInitialized, this, &PdfViewer::initPdfViewer);
|
||||
}
|
||||
|
||||
void PdfViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow)
|
||||
{
|
||||
AbstractViewer::init(file, new QPdfView(parent), mainWindow);
|
||||
m_document = new QPdfDocument(this);
|
||||
m_pdfView = qobject_cast<QPdfView *>(widget());
|
||||
}
|
||||
|
||||
void PdfViewer::cleanup()
|
||||
{
|
||||
delete m_pageSelector;
|
||||
m_pageSelector = nullptr;
|
||||
delete m_zoomSelector;
|
||||
m_zoomSelector = nullptr;
|
||||
delete m_pages;
|
||||
m_pages = nullptr;
|
||||
delete m_bookmarks;
|
||||
m_bookmarks = nullptr;
|
||||
delete m_document;
|
||||
m_document = nullptr;
|
||||
AbstractViewer::cleanup();
|
||||
}
|
||||
|
||||
PdfViewer::~PdfViewer()
|
||||
{
|
||||
PdfViewer::cleanup();
|
||||
}
|
||||
|
||||
QStringList PdfViewer::supportedMimeTypes() const
|
||||
{
|
||||
return {"application/pdf"_L1};
|
||||
}
|
||||
|
||||
void PdfViewer::initPdfViewer()
|
||||
{
|
||||
m_toolBar = addToolBar(tr("PDF"));
|
||||
m_zoomSelector = new ZoomSelector(m_toolBar);
|
||||
|
||||
auto *nav = m_pdfView->pageNavigator();
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6,6,0)
|
||||
m_pageSelector = new QPdfPageSelector(m_toolBar);
|
||||
m_toolBar->insertWidget(m_uiAssets.forward, m_pageSelector);
|
||||
m_pageSelector->setDocument(m_document);
|
||||
connect(m_pageSelector, &QPdfPageSelector::currentPageChanged,
|
||||
this, &PdfViewer::pageSelected);
|
||||
connect(m_pageSelector, &QPdfPageSelector::currentPageChanged,
|
||||
this, &PdfViewer::pageSelected);
|
||||
connect(nav, &QPdfPageNavigator::currentPageChanged,
|
||||
m_pageSelector, &QPdfPageSelector::setCurrentPage);
|
||||
#endif
|
||||
|
||||
connect(m_pdfView->pageNavigator(), &QPdfPageNavigator::backAvailableChanged,
|
||||
m_uiAssets.back, &QAction::setEnabled);
|
||||
m_actionBack = m_uiAssets.back;
|
||||
m_actionForward = m_uiAssets.forward;
|
||||
m_connections.append(connect(m_uiAssets.back, &QAction::triggered,
|
||||
this, &PdfViewer::onActionBackTriggered));
|
||||
m_connections.append(connect(m_uiAssets.forward, &QAction::triggered,
|
||||
this, &PdfViewer::onActionForwardTriggered));
|
||||
|
||||
m_toolBar->addSeparator();
|
||||
m_toolBar->addWidget(m_zoomSelector);
|
||||
|
||||
auto *actionZoomIn = m_toolBar->addAction(tr("Zoom in"));
|
||||
actionZoomIn->setToolTip(tr("Increase zoom level"));
|
||||
actionZoomIn->setIcon(QIcon(":/demos/documentviewer/images/zoom-in.png"_L1));
|
||||
m_toolBar->addAction(actionZoomIn);
|
||||
connect(actionZoomIn, &QAction::triggered, this, &PdfViewer::onActionZoomInTriggered);
|
||||
|
||||
auto *actionZoomOut = m_toolBar->addAction(tr("Zoom out"));
|
||||
actionZoomOut->setToolTip(tr("Decrease zoom level"));
|
||||
actionZoomOut->setIcon(QIcon(":/demos/documentviewer/images/zoom-out.png"_L1));
|
||||
m_toolBar->addAction(actionZoomOut);
|
||||
connect(actionZoomOut, &QAction::triggered, this, &PdfViewer::onActionZoomOutTriggered);
|
||||
|
||||
connect(nav, &QPdfPageNavigator::backAvailableChanged, m_actionBack, &QAction::setEnabled);
|
||||
connect(nav, &QPdfPageNavigator::forwardAvailableChanged, m_actionForward, &QAction::setEnabled);
|
||||
|
||||
connect(m_zoomSelector, &ZoomSelector::zoomModeChanged, m_pdfView, &QPdfView::setZoomMode);
|
||||
connect(m_zoomSelector, &ZoomSelector::zoomFactorChanged, m_pdfView, &QPdfView::setZoomFactor);
|
||||
m_zoomSelector->reset();
|
||||
|
||||
QPdfBookmarkModel *bookmarkModel = new QPdfBookmarkModel(this);
|
||||
bookmarkModel->setDocument(m_document);
|
||||
m_uiAssets.tabs->clear();
|
||||
m_bookmarks = new QTreeView(m_uiAssets.tabs);
|
||||
connect(m_bookmarks, &QAbstractItemView::activated, this, &PdfViewer::bookmarkSelected);
|
||||
m_bookmarks->setModel(bookmarkModel);
|
||||
m_pdfView->setDocument(m_document);
|
||||
m_pdfView->setPageMode(QPdfView::PageMode::MultiPage);
|
||||
|
||||
openPdfFile();
|
||||
if (!m_document->pageCount())
|
||||
return;
|
||||
|
||||
m_pages = new QListView(m_uiAssets.tabs);
|
||||
m_pages->setModel(m_document->pageModel());
|
||||
connect(m_pages->selectionModel(), &QItemSelectionModel::currentRowChanged, m_pages, [&]
|
||||
(const QModelIndex ¤t, const QModelIndex &previous){
|
||||
if (previous == current)
|
||||
return;
|
||||
|
||||
auto *nav = m_pdfView->pageNavigator();
|
||||
const int &row = current.row();
|
||||
if (nav->currentPage() == row)
|
||||
return;
|
||||
|
||||
nav->jump(row, QPointF(), nav->currentZoom());
|
||||
});
|
||||
|
||||
connect(m_pdfView->pageNavigator(), &QPdfPageNavigator::currentPageChanged, m_pages, [&](int page){
|
||||
if (m_pages->currentIndex().row() == page)
|
||||
return;
|
||||
|
||||
m_pages->setCurrentIndex(m_pages->model()->index(page, 0));
|
||||
});
|
||||
|
||||
m_uiAssets.tabs->addTab(m_pages, tr("Pages"));
|
||||
m_uiAssets.tabs->addTab(m_bookmarks, tr("Bookmarks"));
|
||||
QScroller::grabGesture(m_pdfView->viewport(), QScroller::ScrollerGestureType::LeftMouseButtonGesture);
|
||||
HoverWatcher::watcher(m_pdfView->viewport());
|
||||
}
|
||||
|
||||
void PdfViewer::openPdfFile()
|
||||
{
|
||||
disablePrinting();
|
||||
|
||||
if (m_file->open(QIODevice::ReadOnly))
|
||||
m_document->load(m_file.get());
|
||||
|
||||
const auto documentTitle = m_document->metaData(QPdfDocument::MetaDataField::Title).toString();
|
||||
statusMessage(documentTitle.isEmpty() ? "PDF Viewer"_L1 : documentTitle);
|
||||
pageSelected(0);
|
||||
|
||||
statusMessage(tr("Opened PDF file %1")
|
||||
.arg(QDir::toNativeSeparators(m_file->fileName())));
|
||||
qCDebug(lcExample) << "Opened file" << m_file->fileName();
|
||||
|
||||
maybeEnablePrinting();
|
||||
}
|
||||
|
||||
bool PdfViewer::hasContent() const
|
||||
{
|
||||
return m_document ? m_document->pageCount() > 0 : false;
|
||||
}
|
||||
|
||||
#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
void PdfViewer::printDocument(QPrinter *printer) const
|
||||
{
|
||||
if (!hasContent())
|
||||
return;
|
||||
|
||||
QPainter painter;
|
||||
painter.begin(printer);
|
||||
const QRect pageRect = printer->pageRect(QPrinter::Unit::DevicePixel).toRect();
|
||||
const QSize pageSize = pageRect.size();
|
||||
for (int i = 0; i < m_document->pageCount(); ++i) {
|
||||
if (i > 0)
|
||||
printer->newPage();
|
||||
const QImage &page = m_document->render(i, pageSize);
|
||||
painter.drawImage(pageRect, page);
|
||||
}
|
||||
painter.end();
|
||||
}
|
||||
#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
|
||||
void PdfViewer::bookmarkSelected(const QModelIndex &index)
|
||||
{
|
||||
if (!index.isValid())
|
||||
return;
|
||||
|
||||
const int page = index.data(int(QPdfBookmarkModel::Role::Page)).toInt();
|
||||
const qreal zoomLevel = index.data(int(QPdfBookmarkModel::Role::Level)).toReal();
|
||||
m_pdfView->pageNavigator()->jump(page, {}, zoomLevel);
|
||||
}
|
||||
|
||||
void PdfViewer::pageSelected(int page)
|
||||
{
|
||||
auto nav = m_pdfView->pageNavigator();
|
||||
nav->jump(page, {}, nav->currentZoom());
|
||||
}
|
||||
|
||||
void PdfViewer::onActionZoomInTriggered()
|
||||
{
|
||||
m_pdfView->setZoomFactor(m_pdfView->zoomFactor() * zoomMultiplier);
|
||||
}
|
||||
|
||||
void PdfViewer::onActionZoomOutTriggered()
|
||||
{
|
||||
m_pdfView->setZoomFactor(m_pdfView->zoomFactor() / zoomMultiplier);
|
||||
}
|
||||
|
||||
void PdfViewer::onActionPreviousPageTriggered()
|
||||
{
|
||||
auto nav = m_pdfView->pageNavigator();
|
||||
nav->jump(nav->currentPage() - 1, {}, nav->currentZoom());
|
||||
}
|
||||
|
||||
void PdfViewer::onActionNextPageTriggered()
|
||||
{
|
||||
auto nav = m_pdfView->pageNavigator();
|
||||
nav->jump(nav->currentPage() + 1, {}, nav->currentZoom());
|
||||
}
|
||||
|
||||
void PdfViewer::onActionBackTriggered()
|
||||
{
|
||||
m_pdfView->pageNavigator()->back();
|
||||
}
|
||||
|
||||
void PdfViewer::onActionForwardTriggered()
|
||||
{
|
||||
m_pdfView->pageNavigator()->forward();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#ifndef PDFVIEWER_H
|
||||
#define PDFVIEWER_H
|
||||
|
||||
#include "viewerinterfaces.h"
|
||||
#include <QLoggingCategory>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcExample)
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QMainWindow;
|
||||
class QPdfDocument;
|
||||
class QPdfView;
|
||||
class QPdfPageSelector;
|
||||
class QListView;
|
||||
class QTabWidget;
|
||||
class QTreeView;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class ZoomSelector;
|
||||
class PdfViewer : public ViewerInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.DocumentViewer.ViewerInterface" FILE "pdfviewer.json")
|
||||
Q_INTERFACES(ViewerInterface)
|
||||
public:
|
||||
PdfViewer();
|
||||
~PdfViewer() override;
|
||||
void init(QFile *file, QWidget *parent, QMainWindow *mainWindow) override;
|
||||
void cleanup() override;
|
||||
QString viewerName() const override { return QLatin1StringView(staticMetaObject.className()); };
|
||||
QStringList supportedMimeTypes() const override;
|
||||
bool supportsOverview() const override { return true; }
|
||||
bool hasContent() const override;
|
||||
QByteArray saveState() const override { return QByteArray(); }
|
||||
bool restoreState(QByteArray &) override { return true; }
|
||||
|
||||
#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
protected:
|
||||
void printDocument(QPrinter *printer) const override;
|
||||
#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
|
||||
public slots:
|
||||
void openPdfFile();
|
||||
|
||||
private slots:
|
||||
void initPdfViewer();
|
||||
void bookmarkSelected(const QModelIndex &index);
|
||||
void pageSelected(int page);
|
||||
|
||||
// action handlers
|
||||
void onActionZoomInTriggered();
|
||||
void onActionZoomOutTriggered();
|
||||
void onActionPreviousPageTriggered();
|
||||
void onActionNextPageTriggered();
|
||||
void onActionBackTriggered();
|
||||
void onActionForwardTriggered();
|
||||
|
||||
private:
|
||||
void populateQuestions();
|
||||
|
||||
const qreal zoomMultiplier = qSqrt(2.0);
|
||||
QToolBar *m_toolBar = nullptr;
|
||||
ZoomSelector *m_zoomSelector = nullptr;
|
||||
QPdfPageSelector *m_pageSelector = nullptr;
|
||||
QPdfDocument *m_document = nullptr;
|
||||
QPdfView *m_pdfView = nullptr;
|
||||
QAction *m_actionForward = nullptr;
|
||||
QAction *m_actionBack = nullptr;
|
||||
QTreeView *m_bookmarks = nullptr;
|
||||
QListView *m_pages = nullptr;
|
||||
};
|
||||
|
||||
#endif //PDFVIEWER_H
|
||||
@@ -0,0 +1 @@
|
||||
{ "Keys": [ "pdfviewer" ] }
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include "zoomselector.h"
|
||||
|
||||
#include <QLineEdit>
|
||||
|
||||
ZoomSelector::ZoomSelector(QWidget *parent)
|
||||
: QComboBox(parent)
|
||||
{
|
||||
setEditable(true);
|
||||
|
||||
addItem(tr("Fit Width"));
|
||||
addItem(tr("Fit Page"));
|
||||
addItem(tr("12%"));
|
||||
addItem(tr("25%"));
|
||||
addItem(tr("33%"));
|
||||
addItem(tr("50%"));
|
||||
addItem(tr("66%"));
|
||||
addItem(tr("75%"));
|
||||
addItem(tr("100%"));
|
||||
addItem(tr("125%"));
|
||||
addItem(tr("150%"));
|
||||
addItem(tr("200%"));
|
||||
addItem(tr("400%"));
|
||||
|
||||
connect(this, &QComboBox::currentTextChanged,
|
||||
this, &ZoomSelector::onCurrentTextChanged);
|
||||
|
||||
connect(lineEdit(), &QLineEdit::editingFinished,
|
||||
this, [this](){onCurrentTextChanged(lineEdit()->text()); });
|
||||
}
|
||||
|
||||
void ZoomSelector::setZoomFactor(qreal zoomFactor)
|
||||
{
|
||||
setCurrentText(QString::number(qRound(zoomFactor * 100)) + QLatin1String("%"));
|
||||
}
|
||||
|
||||
void ZoomSelector::reset()
|
||||
{
|
||||
setCurrentIndex(8); // 100%
|
||||
}
|
||||
|
||||
void ZoomSelector::onCurrentTextChanged(const QString &text)
|
||||
{
|
||||
if (text == QLatin1String("Fit Width")) {
|
||||
emit zoomModeChanged(QPdfView::ZoomMode::FitToWidth);
|
||||
} else if (text == QLatin1String("Fit Page")) {
|
||||
emit zoomModeChanged(QPdfView::ZoomMode::FitInView);
|
||||
} else {
|
||||
qreal factor = 1.0;
|
||||
|
||||
QString withoutPercent(text);
|
||||
withoutPercent.remove(QLatin1Char('%'));
|
||||
|
||||
bool ok = false;
|
||||
const int zoomLevel = withoutPercent.toInt(&ok);
|
||||
if (ok)
|
||||
factor = zoomLevel / 100.0;
|
||||
|
||||
emit zoomModeChanged(QPdfView::ZoomMode::Custom);
|
||||
emit zoomFactorChanged(factor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#ifndef ZOOMSELECTOR_H
|
||||
#define ZOOMSELECTOR_H
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QPdfView>
|
||||
|
||||
class ZoomSelector : public QComboBox
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ZoomSelector(QWidget *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void setZoomFactor(qreal zoomFactor);
|
||||
|
||||
void reset();
|
||||
|
||||
signals:
|
||||
void zoomModeChanged(QPdfView::ZoomMode zoomMode);
|
||||
void zoomFactorChanged(qreal zoomFactor);
|
||||
|
||||
private slots:
|
||||
void onCurrentTextChanged(const QString &text);
|
||||
};
|
||||
|
||||
#endif // ZOOMSELECTOR_H
|
||||
@@ -0,0 +1,35 @@
|
||||
# Copyright (C) 2023 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets
|
||||
OPTIONAL_COMPONENTS PrintSupport)
|
||||
|
||||
qt_add_plugin(txtviewer
|
||||
CLASS_NAME TxtViewer
|
||||
txtviewer.cpp txtviewer.h
|
||||
)
|
||||
|
||||
set_target_properties(txtviewer PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/app"
|
||||
)
|
||||
|
||||
target_include_directories(txtviewer PRIVATE
|
||||
../../app
|
||||
)
|
||||
|
||||
target_link_libraries(txtviewer PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
abstractviewer
|
||||
)
|
||||
|
||||
if(TARGET Qt6::PrintSupport)
|
||||
target_link_libraries(txtviewer PRIVATE Qt6::PrintSupport)
|
||||
endif()
|
||||
|
||||
install(TARGETS jsonviewer
|
||||
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}/plugins"
|
||||
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}/plugins"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
//! [init]
|
||||
#include "txtviewer.h"
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QMainWindow>
|
||||
#include <QMenu>
|
||||
#include <QMenuBar>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QScrollBar>
|
||||
#include <QToolBar>
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QPainter>
|
||||
#include <QTextDocument>
|
||||
|
||||
#include <QDir>
|
||||
|
||||
#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
#include <QPrinter>
|
||||
#include <QPrintDialog>
|
||||
#endif
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
TxtViewer::TxtViewer()
|
||||
{
|
||||
connect(this, &AbstractViewer::uiInitialized, this, &TxtViewer::setupTxtUi);
|
||||
}
|
||||
|
||||
TxtViewer::~TxtViewer() = default;
|
||||
|
||||
void TxtViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow)
|
||||
{
|
||||
AbstractViewer::init(file, new QPlainTextEdit(parent), mainWindow);
|
||||
m_textEdit = qobject_cast<QPlainTextEdit *>(widget());
|
||||
}
|
||||
|
||||
QStringList TxtViewer::supportedMimeTypes() const
|
||||
{
|
||||
return {"text/plain"_L1};
|
||||
}
|
||||
|
||||
void TxtViewer::setupTxtUi()
|
||||
{
|
||||
QMenu *editMenu = addMenu(tr("&Edit"));
|
||||
QToolBar *editToolBar = addToolBar(tr("Edit"));
|
||||
#ifndef QT_NO_CLIPBOARD
|
||||
const QIcon cutIcon = QIcon::fromTheme("edit-cut"_L1,
|
||||
QIcon(":/demos/documentviewer/images/cut.png"_L1));
|
||||
QAction *cutAct = new QAction(cutIcon, tr("Cu&t"), this);
|
||||
cutAct->setShortcuts(QKeySequence::Cut);
|
||||
cutAct->setStatusTip(tr("Cut the current selection's contents to the "
|
||||
"clipboard"));
|
||||
connect(cutAct, &QAction::triggered, m_textEdit, &QPlainTextEdit::cut);
|
||||
editMenu->addAction(cutAct);
|
||||
editToolBar->addAction(cutAct);
|
||||
|
||||
const QIcon copyIcon = QIcon::fromTheme("edit-copy"_L1,
|
||||
QIcon(":/demos/documentviewer/images/copy.png"_L1));
|
||||
QAction *copyAct = new QAction(copyIcon, tr("&Copy"), this);
|
||||
copyAct->setShortcuts(QKeySequence::Copy);
|
||||
copyAct->setStatusTip(tr("Copy the current selection's contents to the "
|
||||
"clipboard"));
|
||||
connect(copyAct, &QAction::triggered, m_textEdit, &QPlainTextEdit::copy);
|
||||
editMenu->addAction(copyAct);
|
||||
editToolBar->addAction(copyAct);
|
||||
|
||||
const QIcon pasteIcon = QIcon::fromTheme("edit-paste"_L1,
|
||||
QIcon(":/demos/documentviewer/images/paste.png"_L1));
|
||||
QAction *pasteAct = new QAction(pasteIcon, tr("&Paste"), this);
|
||||
pasteAct->setShortcuts(QKeySequence::Paste);
|
||||
pasteAct->setStatusTip(tr("Paste the clipboard's contents into the current "
|
||||
"selection"));
|
||||
connect(pasteAct, &QAction::triggered, m_textEdit, &QPlainTextEdit::paste);
|
||||
editMenu->addAction(pasteAct);
|
||||
editToolBar->addAction(pasteAct);
|
||||
|
||||
menuBar()->addSeparator();
|
||||
|
||||
cutAct->setEnabled(false);
|
||||
copyAct->setEnabled(false);
|
||||
connect(m_textEdit, &QPlainTextEdit::copyAvailable, cutAct, &QAction::setEnabled);
|
||||
connect(m_textEdit, &QPlainTextEdit::copyAvailable, copyAct, &QAction::setEnabled);
|
||||
#endif // !QT_NO_CLIPBOARD
|
||||
|
||||
openFile();
|
||||
|
||||
connect(m_textEdit, &QPlainTextEdit::textChanged, this, [&](){
|
||||
maybeSetPrintingEnabled(hasContent());
|
||||
});
|
||||
|
||||
connect(m_uiAssets.back, &QAction::triggered, m_textEdit, [&](){
|
||||
auto *bar = m_textEdit->verticalScrollBar();
|
||||
if (bar->value() > bar->minimum())
|
||||
bar->setValue(bar->value() - 1);
|
||||
});
|
||||
|
||||
connect(m_uiAssets.forward, &QAction::triggered, m_textEdit, [&](){
|
||||
auto *bar = m_textEdit->verticalScrollBar();
|
||||
if (bar->value() < bar->maximum())
|
||||
bar->setValue(bar->value() + 1);
|
||||
});
|
||||
}
|
||||
//! [init]
|
||||
|
||||
//! [open]
|
||||
void TxtViewer::openFile()
|
||||
{
|
||||
const QString type = tr("open");
|
||||
if (!m_file->open(QFile::ReadOnly | QFile::Text)) {
|
||||
statusMessage(tr("Cannot read file %1:\n%2.")
|
||||
.arg(QDir::toNativeSeparators(m_file->fileName()),
|
||||
m_file->errorString()), type);
|
||||
return;
|
||||
}
|
||||
|
||||
QTextStream in(m_file.get());
|
||||
#ifndef QT_NO_CURSOR
|
||||
QGuiApplication::setOverrideCursor(Qt::WaitCursor);
|
||||
#endif
|
||||
if (!m_textEdit->toPlainText().isEmpty()) {
|
||||
m_textEdit->clear();
|
||||
disablePrinting();
|
||||
}
|
||||
m_textEdit->setPlainText(in.readAll());
|
||||
#ifndef QT_NO_CURSOR
|
||||
QGuiApplication::restoreOverrideCursor();
|
||||
#endif
|
||||
|
||||
statusMessage(tr("File %1 loaded.")
|
||||
.arg(QDir::toNativeSeparators(m_file->fileName())), type);
|
||||
maybeEnablePrinting();
|
||||
}
|
||||
//! [open]
|
||||
|
||||
//! [infoPrintAndSave]
|
||||
bool TxtViewer::hasContent() const
|
||||
{
|
||||
return (!m_textEdit->toPlainText().isEmpty());
|
||||
}
|
||||
|
||||
#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
void TxtViewer::printDocument(QPrinter *printer) const
|
||||
{
|
||||
if (!hasContent())
|
||||
return;
|
||||
|
||||
m_textEdit->print(printer);
|
||||
}
|
||||
#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
|
||||
bool TxtViewer::saveFile(QFile *file)
|
||||
{
|
||||
QString errorMessage;
|
||||
|
||||
QGuiApplication::setOverrideCursor(Qt::WaitCursor);
|
||||
if (file->open(QFile::WriteOnly | QFile::Text)) {
|
||||
QTextStream out(file);
|
||||
out << m_textEdit->toPlainText();
|
||||
} else {
|
||||
errorMessage = tr("Cannot open file %1 for writing:\n%2.")
|
||||
.arg(QDir::toNativeSeparators(file->fileName())),
|
||||
file->errorString();
|
||||
}
|
||||
QGuiApplication::restoreOverrideCursor();
|
||||
|
||||
if (!errorMessage.isEmpty()) {
|
||||
statusMessage(errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
statusMessage(tr("File %1 saved")
|
||||
.arg(QDir::toNativeSeparators(file->fileName())));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TxtViewer::saveDocumentAs()
|
||||
{
|
||||
QFileDialog dialog(mainWindow());
|
||||
dialog.setWindowModality(Qt::WindowModal);
|
||||
dialog.setAcceptMode(QFileDialog::AcceptSave);
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return false;
|
||||
|
||||
const QStringList &files = dialog.selectedFiles();
|
||||
if (files.isEmpty())
|
||||
return false;
|
||||
|
||||
//newFile();
|
||||
m_file->setFileName(files.first());
|
||||
return saveDocument();
|
||||
}
|
||||
//! [infoPrintAndSave]
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#ifndef TXTVIEWER_H
|
||||
#define TXTVIEWER_H
|
||||
|
||||
#include "viewerinterfaces.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QPlainTextEdit;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
//! [interfacing]
|
||||
class TxtViewer : public ViewerInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.DocumentViewer.ViewerInterface" FILE "txtviewer.json")
|
||||
Q_INTERFACES(ViewerInterface)
|
||||
//! [interfacing]
|
||||
//! [classDefinition]
|
||||
public:
|
||||
TxtViewer();
|
||||
~TxtViewer() override;
|
||||
void init(QFile *file, QWidget *parent, QMainWindow *mainWindow) override;
|
||||
QString viewerName() const override { return QLatin1StringView(staticMetaObject.className()); };
|
||||
QStringList supportedMimeTypes() const override;
|
||||
bool saveDocument() override { return saveFile(m_file.get()); };
|
||||
bool saveDocumentAs() override;
|
||||
bool hasContent() const override;
|
||||
QByteArray saveState() const override { return {}; }
|
||||
bool restoreState(QByteArray &) override { return true; }
|
||||
bool supportsOverview() const override { return false; }
|
||||
|
||||
#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
protected:
|
||||
void printDocument(QPrinter *printer) const override;
|
||||
#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT
|
||||
|
||||
private slots:
|
||||
void setupTxtUi();
|
||||
|
||||
private:
|
||||
void openFile();
|
||||
bool saveFile (QFile *file);
|
||||
|
||||
QPlainTextEdit *m_textEdit;
|
||||
};
|
||||
//! [classDefinition]
|
||||
|
||||
#endif //TXTVIEWER_H
|
||||
@@ -0,0 +1 @@
|
||||
{ "Keys": [ "txtviewer" ] }
|
||||
Reference in New Issue
Block a user