diff options
57 files changed, 2615 insertions, 1 deletions
diff --git a/dependencies.yaml b/dependencies.yaml index 971c215c..5e54cf4e 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -10,4 +10,6 @@ dependencies: required: false ../qttools: ref: 701c378276129d9099bf1f0fae6305348920ea91 - required: true + ../qtwebengine: + ref: 47095e7e5ef4a2d3f16337bddfd71c4761519e92 + required: false diff --git a/examples/demos/CMakeLists.txt b/examples/demos/CMakeLists.txt index 20cab504..c4783c46 100644 --- a/examples/demos/CMakeLists.txt +++ b/examples/demos/CMakeLists.txt @@ -21,3 +21,6 @@ if(TARGET Qt::Quick AND TARGET Qt::Network AND TARGET Qt::QmlXmlListModel) qt_internal_add_example(rssnews) qt_internal_add_example(photoviewer) endif() +if(TARGET Qt::Widgets AND TARGET Qt::PdfWidgets) + qt_internal_add_example(documentviewer) +endif() diff --git a/examples/demos/documentviewer/CMakeLists.txt b/examples/demos/documentviewer/CMakeLists.txt new file mode 100644 index 00000000..338f65cd --- /dev/null +++ b/examples/demos/documentviewer/CMakeLists.txt @@ -0,0 +1,93 @@ +cmake_minimum_required(VERSION 3.16) +project(documentviewer LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets PdfWidgets + OPTIONAL_COMPONENTS PrintSupport) + +qt_standard_project_setup() + +qt_add_executable(documentviewer + main.cpp + mainwindow.cpp mainwindow.h mainwindow.ui + abstractviewer.cpp abstractviewer.h + viewerfactory.cpp viewerfactory.h + jsonviewer.cpp jsonviewer.h + txtviewer.cpp txtviewer.h + pdfviewer.cpp pdfviewer.h + zoomselector.cpp zoomselector.h + hoverwatcher.cpp hoverwatcher.cpp +) + +set_target_properties(documentviewer PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) +# Resources: +set(documentviewer_resource_files + "images/copy@2x.png" + "images/copy.png" + "images/cut@2x.png" + "images/cut.png" + "images/go-next-view@2x.png" + "images/go-next-view-page@2x.png" + "images/go-next-view-page.png" + "images/go-next-view.png" + "images/go-previous-view@2x.png" + "images/go-previous-view-page@2x.png" + "images/go-previous-view-page.png" + "images/go-previous-view.png" + "images/magnifier@2x.png" + "images/magnifier.png" + "images/open@2x.png" + "images/open.png" + "images/paste@2x.png" + "images/paste.png" + "images/print2x.png" + "images/print.png" + "images/qt-logo@2x.png" + "images/qt-logo.png" + "images/zoom-fit-best@2x.png" + "images/zoom-fit-best.png" + "images/zoom-fit-width@2x.png" + "images/zoom-fit-width.png" + "images/zoom-in@2x.png" + "images/zoom-in.png" + "images/zoom-original@2x.png" + "images/zoom-original.png" + "images/zoom-out@2x.png" + "images/zoom-out.png" + "images/zoom-previous@2x.png" + "images/zoom-previous.png" +) + +qt6_add_resources(documentviewer "documentviewer" + PREFIX + "/demos/documentviewer" + FILES + ${documentviewer_resource_files} +) + +target_link_libraries(documentviewer PUBLIC + Qt::Core + Qt::Gui + Qt::Widgets + Qt::PdfWidgets +) + +if(TARGET Qt6::PrintSupport) + target_link_libraries(documentviewer PRIVATE Qt6::PrintSupport) +endif() + +install(TARGETS documentviewer + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/demos/documentviewer/abstractviewer.cpp b/examples/demos/documentviewer/abstractviewer.cpp new file mode 100644 index 00000000..0dbc3b26 --- /dev/null +++ b/examples/demos/documentviewer/abstractviewer.cpp @@ -0,0 +1,161 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "abstractviewer.h" +#include <QAction> +#include <QTabWidget> +#include <QToolBar> +#include <QMenu> +#include <QMenuBar> +#include <QScrollArea> +#include <QStatusBar> +#include <QMessageBox> +#include <QSettings> +#include <QApplication> + +#if defined(QT_ABSTRACTVIEWER_PRINTSUPPORT) +#include <QPrintDialog> +#endif // QT_ABSTRACTVIEWER_PRINTSUPPORT + +AbstractViewer::AbstractViewer(QFile *file, QWidget *widget, QMainWindow *mainWindow) : + m_file(file), + m_widget(widget) +{ + Q_ASSERT(widget); + Q_ASSERT(mainWindow); + m_uiAssets.mainWindow = mainWindow; +} + +AbstractViewer::~AbstractViewer() +{ + // delete all objects created by the viewer which need to be displayed + // and therefore parented on MainWindow + delete m_widget; + qDeleteAll(m_menus); + qDeleteAll(m_toolBars); +} + +void AbstractViewer::statusMessage(const QString &message, const QString &type, int timeout) +{ + const QString msg = viewerName() + (type.isEmpty() ? ": " : "/" + type + ": ") + message; + emit showMessage(msg, timeout); +} + +QToolBar *AbstractViewer::addToolBar(const QString &title) +{ + auto *bar = mainWindow()->addToolBar(title); + bar->setObjectName(QString(title).replace(" ", "")); + m_toolBars.append(bar); + return bar; +} + +QMenu *AbstractViewer::addMenu(const QString &title) +{ + QMenu *menu = new QMenu(title, menuBar()); + menuBar()->insertMenu(m_uiAssets.help, menu); + m_menus.append(menu); + return menu; +} + +QMenu *AbstractViewer::fileMenu() +{ + static constexpr QLatin1StringView name = QLatin1StringView("qtFileMenu"); + static QMenu *fileMenu = nullptr; + if (fileMenu) + return fileMenu; + + QList<QMenu *> menus = mainWindow()->findChildren<QMenu *>(); + for (auto *menu : menus) { + if (menu->objectName() == name) { + fileMenu = menu; + return fileMenu; + } + } + fileMenu = addMenu(tr("&File")); + fileMenu->setObjectName(name); + return fileMenu; +} + +void AbstractViewer::print() +{ +#ifdef QT_ABSTRACTVIEWER_PRINTSUPPORT + static const QString type = tr("Printing"); + if (!hasContent()) { + statusMessage(tr("No content to print."), type); + return; + } + + QPrinter printer(QPrinter::HighResolution); + QPrintDialog dlg(&printer, mainWindow()); + dlg.setWindowTitle(tr("Print Document")); + if (dlg.exec() == QDialog::Accepted) { + printDocument(&printer); + } else { + statusMessage(tr("Printing canceled!"), type); + return; + } + + const QPrinter::PrinterState state = printer.printerState(); + QString message = viewerName() + " :"; + switch (state) { + case QPrinter::PrinterState::Aborted: + message += tr("Printing aborted."); + break; + case QPrinter::PrinterState::Active: + message += tr("Printing active."); + break; + case QPrinter::PrinterState::Idle: + message += tr("Printing completed."); + break; + case QPrinter::PrinterState::Error: + message += tr("Printing error."); + break; + } + statusMessage(message, type); +#else + statusMessage(tr("Printing not supported!")); +#endif +} + +/*! + \brief AbstractViewer::setPrintingEnabled + Enables / disables printing. + If printing is not supported or the viewer has no content to display, + \param enabled is overridden with \c false; + The signal printingEnabledChanged is emitted if the status has changed. + */ +void AbstractViewer::maybeSetPrintingEnabled(bool enabled) +{ +#ifndef QT_ABSTRACTVIEWER_PRINTSUPPORT + enabled = false; +#else + if (!hasContent()) + enabled = false; +#endif + + if (enabled == m_printingEnabled) + return; + + m_printingEnabled = enabled; + emit printingEnabledChanged(enabled); +} + +void AbstractViewer::initViewer(QAction *back, QAction *forward, QAction *help, QTabWidget *tabs) +{ + // Viewers need back & forward buttons and a tab widget. + Q_ASSERT(back); + Q_ASSERT(forward); + Q_ASSERT(tabs); + Q_ASSERT(help); + + m_uiAssets.back = back; + m_uiAssets.forward = forward; + m_uiAssets.help = help; + m_uiAssets.tabs = tabs; + + // Tabs can be populated individually by the viewer, if it supports overview + tabs->clear(); + tabs->setVisible(supportsOverview()); + + emit uiInitialized(); +} diff --git a/examples/demos/documentviewer/abstractviewer.h b/examples/demos/documentviewer/abstractviewer.h new file mode 100644 index 00000000..702702ba --- /dev/null +++ b/examples/demos/documentviewer/abstractviewer.h @@ -0,0 +1,101 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef ABSTRACTVIEWER_H +#define ABSTRACTVIEWER_H + +#include <QWidget> +#include <QMainWindow> +#include <QFileInfo> + +#if defined(QT_PRINTSUPPORT_LIB) +#include <QtPrintSupport/qtprintsupportglobal.h> +#if QT_CONFIG(printer) +#if QT_CONFIG(printdialog) +#define QT_ABSTRACTVIEWER_PRINTSUPPORT +#include <QPrinter> +#endif // QT_CONFIG(printdialog) +#endif // QT_CONFIG(printer) +#endif // QT_PRINTSUPPORT_LIB + +class QToolBar; +class QTabWidget; +class QScrollArea; +class QStatusBar; +class AbstractViewer : public QObject +{ + Q_OBJECT + +protected: + explicit AbstractViewer(QFile *file, QWidget *widget, QMainWindow *mainWindow); + +public: + virtual ~AbstractViewer(); + + void initViewer(QAction *back, QAction *forward, QAction *help, QTabWidget *tabs); + virtual bool isModified() const { return false; } + virtual bool saveDocument() { return false; } + virtual bool saveDocumentAs() { return false; } + virtual QString viewerName() const = 0; + virtual bool supportsOverview() const { return false; } + virtual QByteArray saveState() const = 0; + virtual bool restoreState(QByteArray &) = 0; + virtual bool hasContent() const { return false; } + bool isEmpty() const { return !hasContent(); } + bool isPrintingEnabled() const { return m_printingEnabled; } + + QList<QAction *> actions() const { return m_actions; } + QWidget *widget() const { return m_widget; } + QList<QMenu *> menus() const { return m_menus; } + +#ifdef QT_ABSTRACTVIEWER_PRINTSUPPORT +protected: + virtual void printDocument(QPrinter *) const {}; +#endif + +signals: + void uiInitialized(); + void printingEnabledChanged(bool enabled); + void showMessage(const QString &message, int timeout = 8000); + void documentLoaded(const QString &fileName); + +public slots: + void print(); + +protected: + + struct UiAssets { + QMainWindow *mainWindow = nullptr; + QAction *back = nullptr; + QAction *forward = nullptr; + QAction *help = nullptr; + QTabWidget *tabs = nullptr; + } m_uiAssets; + + void statusMessage(const QString &message, const QString &type = QString(), int timeout = 8000); + QToolBar *addToolBar(const QString &); + QMenu *addMenu(const QString &); + QMenu *fileMenu(); + QMainWindow *mainWindow() const { return m_uiAssets.mainWindow; } + QStatusBar *statusBar() const { return mainWindow()->statusBar(); } + QMenuBar *menuBar() const { return mainWindow()->menuBar(); } + + std::unique_ptr<QFile> m_file; + QList<QAction *> m_actions; + QWidget *m_widget; + +protected slots: + void maybeSetPrintingEnabled(bool enabled); + inline void maybeEnablePrinting() { return maybeSetPrintingEnabled(true); } + inline void disablePrinting() { return maybeSetPrintingEnabled(false); } + +private: + QList<QMenu *> m_menus; + QList<QToolBar *> m_toolBars; + bool m_printingEnabled = false; + int m_classId = -1; + + static constexpr QLatin1StringView m_viewerName = QLatin1StringView("AbstractViewer"); +}; + +#endif // ABSTRACTVIEWER_H diff --git a/examples/demos/documentviewer/doc/images/documentviewer_open.png b/examples/demos/documentviewer/doc/images/documentviewer_open.png Binary files differnew file mode 100644 index 00000000..8a05c2fc --- /dev/null +++ b/examples/demos/documentviewer/doc/images/documentviewer_open.png diff --git a/examples/demos/documentviewer/doc/src/documentviewer.qdoc b/examples/demos/documentviewer/doc/src/documentviewer.qdoc new file mode 100644 index 00000000..33b323f6 --- /dev/null +++ b/examples/demos/documentviewer/doc/src/documentviewer.qdoc @@ -0,0 +1,207 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \title Qt Demo - Document Viewer + \ingroup mainwindow-classes + \example demos/documentviewer + \brief A Widgets application to display and print Json, text and PDF files. + Demonstrates various features to use in widget applications: + Using QSettings, query and save user preferences, manage file histories and + control cursor behavior when hovering over widgets. + \image documentviewer_open.png + \meta {tag} {demo,widgets,mainwindow} + + \e{Document Viewer} demonstrates how to use a QMainWindow with static + and dynamic tool bars, menus and actions. + + The MainWindow class provides a common application screen with general menus, + actions and a tool bar. It provides functionality to open a file, determine the + content type and keep a list of previously opened files. It stores information + in QSettings and reads settings when launched. + Depending on the opened file's content type, it creates a suitable viewer to + display it and provide printing functionality. + + \section1 Creating an executable + + To create an executable, we use a standard main.cpp file. + First, we set the application's organization name: + + \quotefromfile demos/documentviewer/main.cpp + \skipto int main + \printuntil QApplication::set + \endsection1 + + \section1 Creating an application and showing the main window + + \quotefromfile demos/documentviewer/main.cpp + \skipto QApplication app + \printuntil } + \endsection1 + + \section1 The MainWindow class + + The class constructor initializes the user interface created in Qt Designer. + It links the actions for opening a file and the about dialog to their implementation. + + \quotefromfile demos/documentviewer/mainwindow.cpp + \skipto ui->setupUi + \printuntil triggered + + The \c mainwindow.ui file provides a QTabWidget on the left, where bookmarks and thumbnails + can be displayed. It provides a QScrollArea on the right, where the viewed file contents are + displayed. + + The ViewerFactory class provides a static method to create a file type specific viewer. + + \code + m_viewer = ViewerFactory::makeViewer(&file, ui->viewArea, this, questions()); + \endcode + + If the application settings contain a section for the viewer, it is passed to the viewer's + virtual restoreState method. Afterwards, the standard UI assets are passed to the viewer + and it's display widget is displayed in the main scroll area. + + \quotefromfile demos/documentviewer/mainwindow.cpp + \skipuntil void MainWindow::openFile + \skipto m_viewer->initViewer + \printuntil } + \endsection1 + + \section1 The ViewerFactory class + + The only static method of the class takes a file, the widget where the viewed content is to be + displayed, the main window and the user questions. + Depending on the file's mime type, it creates an appropriate document viewer. + + \quotefromfile demos/documentviewer/mainwindow.cpp + \skipto AbstractViewer + \printuntil } + \endsection1 + + \section1 The AbstractViewer class + + The class provides a generalized API to view and browse through a document, save and print it. + Properties of the document and the viewer itself can be queried: Does the document have content? + Has it been modified? Is an overview (thumbnails or bookmarks) supported? + The viewer's state can be saved to and restored from a QByteArray, which the application can + access to store in its settings. + + AbstractViewer provides protected methods for classes inheriting from it, to create actions + and menus on the main window. In order to display these assets on the main window, they are + parented to it. AbstractViewer takes responsibility to remove and destroy the UI assets it + created. It inherits from QObject to provide access to signals and slots. + + \section2 Signals + + + \code + void uiInitialized(); + \endcode + + The signal is emitted when AbstractViewer has received all necessary information about UI + assets on the main window. + + \code + void printingEnabledChanged(bool enabled); + \endcode + + This signal is emitted when document printing has been enabled or disabled, e.g. because a new + document has been successfully loaded or all content has been removed. + + \code + void printStatusChanged(AbstractViewer::PrintStatus status); + \endcode + + When printing has been started, this signal notifies about the printing progress. + + \code + void documentLoaded(const QString &fileName); + \endcode + \endsection2 + + The signal notifies the application that a document has been loaded successfully. + \endsection1 + + \section1 The TextViewer class + + A simple text viewer, inheriting from AbstractViewer. + It features editing text files, copy/cut and paste, printing and saving changes. + + \section1 The JsonViewer class + + The class displays a JSON file in a QTreeView. + It loads a file into a QJsonDocument, used to populate a custom tree model with + JsonItemModel. + This part of the JSON viewer demonstrates, how to implement custom item models inheriting from + QAbstractItemModel. The JsonTreeItem class provides basic API for manipulating JSON data + and propagating it back to the underlying QJsonDocument. + + JsonViewer uses the toplevel objects as bookmarks for navigation. Other nodes (keys or + values) can be added as additional bookmarks or removed from the bookmark list. + A QLineEdit is used as a search field to navigate through the JSON tree. + \endsection1 + + \section1 The PdfViewer class + This is a fork of the QPdfViewerWidgets example. + It demonstrates the use of QScroller to smoothly flick through the document. + \endsection1 + + \section1 Additional classes for application features + + \section2 The HoverWatcher class + + The class can be used to set override cursors when the mouse is hovering over a widget and + to restore them upon departure. + In order to prevent multiple HoverWatcher instances created for the same widget, it is + implemented as a signleton per widget. + + HoverWatcher inherits from QObject and the QWidget watched becomes the instance's parent. + An event filter is used to intercept the hover events without consuming them. + + \quotefromfile demos/documentviewer/hoverwatcher.cpp + \skipto HoverWatcher::HoverWatcher + \printuntil } + + The actions watched are represented in an enum. + + \quotefromfile demos/documentviewer/hoverwatcher.h + \skipto enum HoverAction + \printuntil }; + + Static methods create watchers, check their existence for a specific QWidget or dismiss + a watcher. + + \quotefromfile demos/documentviewer/hoverwatcher.h + \skipto static HoverWatcher + \printuntil static void dismiss + + A cursor shape can be specified or unset for each HoverAction. If no cursor shape is + specified for an action, the application's override cursor will be restored when it occurs. + + \quotefromfile demos/documentviewer/hoverwatcher.h + \skipto public slots + \printuntil void unSetCursorShape + + The mouseButtons property specifies, which mouse buttons to consider for the MousePress action. + + \quotefromfile demos/documentviewer/hoverwatcher.h + \skipuntil public slots + \skipto setMouseButtons + \printuntil setMouseButton( + + Action specific signals are emitted when an action has been processed. + + \quotefromfile demos/documentviewer/hoverwatcher.h + \skipto signals + \printuntil left(); + + A general signal is emitted which passes the processed action as an argument. + + \code + void hoverAction(HoverAction action); + \endcode + \endsection2 + \endsection1 + +*/ diff --git a/examples/demos/documentviewer/hoverwatcher.cpp b/examples/demos/documentviewer/hoverwatcher.cpp new file mode 100644 index 00000000..3c0ffe1e --- /dev/null +++ b/examples/demos/documentviewer/hoverwatcher.cpp @@ -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; + } +} diff --git a/examples/demos/documentviewer/hoverwatcher.h b/examples/demos/documentviewer/hoverwatcher.h new file mode 100644 index 00000000..8ddfcd1f --- /dev/null +++ b/examples/demos/documentviewer/hoverwatcher.h @@ -0,0 +1,66 @@ +// 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> + +class QWidget; +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 diff --git a/examples/demos/documentviewer/images/copy.png b/examples/demos/documentviewer/images/copy.png Binary files differnew file mode 100644 index 00000000..2aeb2828 --- /dev/null +++ b/examples/demos/documentviewer/images/copy.png diff --git a/examples/demos/documentviewer/images/copy@2x.png b/examples/demos/documentviewer/images/copy@2x.png Binary files differnew file mode 100644 index 00000000..f4ebabba --- /dev/null +++ b/examples/demos/documentviewer/images/copy@2x.png diff --git a/examples/demos/documentviewer/images/cut.png b/examples/demos/documentviewer/images/cut.png Binary files differnew file mode 100644 index 00000000..54638e93 --- /dev/null +++ b/examples/demos/documentviewer/images/cut.png diff --git a/examples/demos/documentviewer/images/cut@2x.png b/examples/demos/documentviewer/images/cut@2x.png Binary files differnew file mode 100644 index 00000000..5a5da4fd --- /dev/null +++ b/examples/demos/documentviewer/images/cut@2x.png diff --git a/examples/demos/documentviewer/images/go-next-view-page.png b/examples/demos/documentviewer/images/go-next-view-page.png Binary files differnew file mode 100644 index 00000000..bd2a3383 --- /dev/null +++ b/examples/demos/documentviewer/images/go-next-view-page.png diff --git a/examples/demos/documentviewer/images/go-next-view-page@2x.png b/examples/demos/documentviewer/images/go-next-view-page@2x.png Binary files differnew file mode 100644 index 00000000..5ddcbbcc --- /dev/null +++ b/examples/demos/documentviewer/images/go-next-view-page@2x.png diff --git a/examples/demos/documentviewer/images/go-next-view.png b/examples/demos/documentviewer/images/go-next-view.png Binary files differnew file mode 100644 index 00000000..98b79dea --- /dev/null +++ b/examples/demos/documentviewer/images/go-next-view.png diff --git a/examples/demos/documentviewer/images/go-next-view@2x.png b/examples/demos/documentviewer/images/go-next-view@2x.png Binary files differnew file mode 100644 index 00000000..91940643 --- /dev/null +++ b/examples/demos/documentviewer/images/go-next-view@2x.png diff --git a/examples/demos/documentviewer/images/go-previous-view-page.png b/examples/demos/documentviewer/images/go-previous-view-page.png Binary files differnew file mode 100644 index 00000000..ecd3768e --- /dev/null +++ b/examples/demos/documentviewer/images/go-previous-view-page.png diff --git a/examples/demos/documentviewer/images/go-previous-view-page@2x.png b/examples/demos/documentviewer/images/go-previous-view-page@2x.png Binary files differnew file mode 100644 index 00000000..f0d91c9f --- /dev/null +++ b/examples/demos/documentviewer/images/go-previous-view-page@2x.png diff --git a/examples/demos/documentviewer/images/go-previous-view.png b/examples/demos/documentviewer/images/go-previous-view.png Binary files differnew file mode 100644 index 00000000..086bd9a1 --- /dev/null +++ b/examples/demos/documentviewer/images/go-previous-view.png diff --git a/examples/demos/documentviewer/images/go-previous-view@2x.png b/examples/demos/documentviewer/images/go-previous-view@2x.png Binary files differnew file mode 100644 index 00000000..900860ce --- /dev/null +++ b/examples/demos/documentviewer/images/go-previous-view@2x.png diff --git a/examples/demos/documentviewer/images/magnifier.png b/examples/demos/documentviewer/images/magnifier.png Binary files differnew file mode 100644 index 00000000..6eb457d9 --- /dev/null +++ b/examples/demos/documentviewer/images/magnifier.png diff --git a/examples/demos/documentviewer/images/magnifier@2x.png b/examples/demos/documentviewer/images/magnifier@2x.png Binary files differnew file mode 100644 index 00000000..ed84af18 --- /dev/null +++ b/examples/demos/documentviewer/images/magnifier@2x.png diff --git a/examples/demos/documentviewer/images/open.png b/examples/demos/documentviewer/images/open.png Binary files differnew file mode 100644 index 00000000..45fa2883 --- /dev/null +++ b/examples/demos/documentviewer/images/open.png diff --git a/examples/demos/documentviewer/images/open@2x.png b/examples/demos/documentviewer/images/open@2x.png Binary files differnew file mode 100644 index 00000000..12c2c3c1 --- /dev/null +++ b/examples/demos/documentviewer/images/open@2x.png diff --git a/examples/demos/documentviewer/images/paste.png b/examples/demos/documentviewer/images/paste.png Binary files differnew file mode 100644 index 00000000..c14425ca --- /dev/null +++ b/examples/demos/documentviewer/images/paste.png diff --git a/examples/demos/documentviewer/images/paste@2x.png b/examples/demos/documentviewer/images/paste@2x.png Binary files differnew file mode 100644 index 00000000..360b0f6c --- /dev/null +++ b/examples/demos/documentviewer/images/paste@2x.png diff --git a/examples/demos/documentviewer/images/print.png b/examples/demos/documentviewer/images/print.png Binary files differnew file mode 100644 index 00000000..4ace2614 --- /dev/null +++ b/examples/demos/documentviewer/images/print.png diff --git a/examples/demos/documentviewer/images/print2x.png b/examples/demos/documentviewer/images/print2x.png Binary files differnew file mode 100644 index 00000000..1c3655be --- /dev/null +++ b/examples/demos/documentviewer/images/print2x.png diff --git a/examples/demos/documentviewer/images/qt-logo.png b/examples/demos/documentviewer/images/qt-logo.png Binary files differnew file mode 100644 index 00000000..c9171422 --- /dev/null +++ b/examples/demos/documentviewer/images/qt-logo.png diff --git a/examples/demos/documentviewer/images/qt-logo@2x.png b/examples/demos/documentviewer/images/qt-logo@2x.png Binary files differnew file mode 100644 index 00000000..95d1d09b --- /dev/null +++ b/examples/demos/documentviewer/images/qt-logo@2x.png diff --git a/examples/demos/documentviewer/images/zoom-fit-best.png b/examples/demos/documentviewer/images/zoom-fit-best.png Binary files differnew file mode 100644 index 00000000..6a13de4c --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-fit-best.png diff --git a/examples/demos/documentviewer/images/zoom-fit-best@2x.png b/examples/demos/documentviewer/images/zoom-fit-best@2x.png Binary files differnew file mode 100644 index 00000000..904b41c8 --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-fit-best@2x.png diff --git a/examples/demos/documentviewer/images/zoom-fit-width.png b/examples/demos/documentviewer/images/zoom-fit-width.png Binary files differnew file mode 100644 index 00000000..d51fbac6 --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-fit-width.png diff --git a/examples/demos/documentviewer/images/zoom-fit-width@2x.png b/examples/demos/documentviewer/images/zoom-fit-width@2x.png Binary files differnew file mode 100644 index 00000000..4d1fd0b4 --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-fit-width@2x.png diff --git a/examples/demos/documentviewer/images/zoom-in.png b/examples/demos/documentviewer/images/zoom-in.png Binary files differnew file mode 100644 index 00000000..5ae1046c --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-in.png diff --git a/examples/demos/documentviewer/images/zoom-in@2x.png b/examples/demos/documentviewer/images/zoom-in@2x.png Binary files differnew file mode 100644 index 00000000..863ef4ac --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-in@2x.png diff --git a/examples/demos/documentviewer/images/zoom-original.png b/examples/demos/documentviewer/images/zoom-original.png Binary files differnew file mode 100644 index 00000000..8aa9bb49 --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-original.png diff --git a/examples/demos/documentviewer/images/zoom-original@2x.png b/examples/demos/documentviewer/images/zoom-original@2x.png Binary files differnew file mode 100644 index 00000000..d5473007 --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-original@2x.png diff --git a/examples/demos/documentviewer/images/zoom-out.png b/examples/demos/documentviewer/images/zoom-out.png Binary files differnew file mode 100644 index 00000000..081b6d98 --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-out.png diff --git a/examples/demos/documentviewer/images/zoom-out@2x.png b/examples/demos/documentviewer/images/zoom-out@2x.png Binary files differnew file mode 100644 index 00000000..34c8e174 --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-out@2x.png diff --git a/examples/demos/documentviewer/images/zoom-previous.png b/examples/demos/documentviewer/images/zoom-previous.png Binary files differnew file mode 100644 index 00000000..0ff5c041 --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-previous.png diff --git a/examples/demos/documentviewer/images/zoom-previous@2x.png b/examples/demos/documentviewer/images/zoom-previous@2x.png Binary files differnew file mode 100644 index 00000000..e9909abc --- /dev/null +++ b/examples/demos/documentviewer/images/zoom-previous@2x.png diff --git a/examples/demos/documentviewer/jsonviewer.cpp b/examples/demos/documentviewer/jsonviewer.cpp new file mode 100644 index 00000000..3b7b4a91 --- /dev/null +++ b/examples/demos/documentviewer/jsonviewer.cpp @@ -0,0 +1,532 @@ +// 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); +} diff --git a/examples/demos/documentviewer/jsonviewer.h b/examples/demos/documentviewer/jsonviewer.h new file mode 100644 index 00000000..c9291cd2 --- /dev/null +++ b/examples/demos/documentviewer/jsonviewer.h @@ -0,0 +1,107 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef JSONVIEWER_H +#define JSONVIEWER_H + +#include "abstractviewer.h" +#include <QJsonValue> +#include <QJsonDocument> +#include <QAbstractItemModel> + +class QMainWindow; +class QTreeWidgetItem; +class QTreeView; +class QListWidget; +class QListWidgetItem; +class QLineEdit; + +class JsonViewer : public AbstractViewer +{ + Q_GADGET +public: + JsonViewer(QFile *file, QWidget *parent, QMainWindow *mainWindow); + ~JsonViewer() override; + + QString viewerName() const override { return staticMetaObject.className(); }; + QByteArray saveState() const override; + bool restoreState(QByteArray &) override; + bool supportsOverview() const override { return true; } + bool hasContent() const override; + +#if defined(QT_ABSTRACTVIEWER_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; + + int m_classId = -1; + QLineEdit *m_searchKey = nullptr; +}; + +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; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) 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; }; + Qt::ItemFlags flags(const QModelIndex &index) const override; + +private: + JsonTreeItem *m_rootItem = nullptr; + QStringList m_headers; + static JsonTreeItem *itemFromIndex(const QModelIndex &index) + {return static_cast<JsonTreeItem*>(index.internalPointer()); } +}; + +#endif //JSONVIEWER_H diff --git a/examples/demos/documentviewer/main.cpp b/examples/demos/documentviewer/main.cpp new file mode 100644 index 00000000..1540bc5a --- /dev/null +++ b/examples/demos/documentviewer/main.cpp @@ -0,0 +1,34 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "mainwindow.h" +#include <QApplication> +#include <QCommandLineParser> + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + QApplication::setOrganizationName(QApplication::translate("main", "QtExamples")); + QApplication::setApplicationName(QApplication::translate("main", "DocumentViewer")); + QApplication::setApplicationVersion("1.0"); + + QCommandLineParser parser; + parser.setApplicationDescription(QApplication::translate("main", + "A viewer for JSON, PDF and text files")); + parser.addHelpOption(); + parser.addVersionOption(); + parser.addPositionalArgument("File", QApplication::translate("main", + "JSON, PDF or text file to open")); + parser.process(app); + + const QStringList &positionalArguments = parser.positionalArguments(); + const QString &fileName = (positionalArguments.count() > 0) ? positionalArguments.at(0) + : QString(); + + MainWindow w; + w.show(); + if (!fileName.isEmpty()) + w.openFile(fileName); + + return app.exec(); +} diff --git a/examples/demos/documentviewer/mainwindow.cpp b/examples/demos/documentviewer/mainwindow.cpp new file mode 100644 index 00000000..ac134101 --- /dev/null +++ b/examples/demos/documentviewer/mainwindow.cpp @@ -0,0 +1,128 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include "abstractviewer.h" +#include "viewerfactory.h" +#include <QFileDialog> +#include <QSettings> +#include <QToolButton> +#include <QMessageBox> +#include <QMetaObject> + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow) +{ + ui->setupUi(this); + readSettings(); +} + +MainWindow::~MainWindow() +{ + saveSettings(); +} + +void MainWindow::on_actionOpen_triggered() +{ + const QString fileName = QFileDialog::getOpenFileName(this, + tr("Open Document"), + m_currentDir.absolutePath()); + + if (fileName.isEmpty()) + return; + + openFile(fileName); +} + +void MainWindow::openFile(const QString &fileName) +{ + QFile *file = new QFile(fileName); + if (!file->exists()) { + statusBar()->showMessage(tr("File %1 could not be opened").arg(fileName)); + delete file; + return; + } + + QFileInfo fileInfo(*file); + m_currentDir = fileInfo.dir(); + + // If a viewer is already open, save its state first + saveViewerSettings(); + m_viewer.reset(ViewerFactory::makeViewer(file, ui->viewArea, this)); + restoreViewerSettings(); + + ui->actionPrint->setEnabled(m_viewer->hasContent()); + connect(m_viewer.get(), &AbstractViewer::printingEnabledChanged, ui->actionPrint, &QAction::setEnabled); + connect(ui->actionPrint, &QAction::triggered, m_viewer.get(), &AbstractViewer::print); + connect(m_viewer.get(), &AbstractViewer::showMessage, statusBar(), &QStatusBar::showMessage); + + m_viewer->initViewer(ui->actionBack, ui->actionForward, ui->menuHelp->menuAction(), ui->tabWidget); + ui->scrollArea->setWidget(m_viewer->widget()); +} + +void MainWindow::on_actionAbout_triggered() +{ + QMessageBox::aboutQt(this); +} + +void MainWindow::on_actionAboutQt_triggered() +{ + QMessageBox::aboutQt(this); +} + +void MainWindow::readSettings() +{ + QSettings settings(settingsName); + + // Restore working directory + if (settings.contains(settingsDir)) + m_currentDir = QDir(settings.value(settingsDir).toString()); + else + m_currentDir = QDir::current(); + + // Restore QMainWindow state + if (settings.contains(settingsMainWindow)) { + QByteArray mainWindowState = settings.value(settingsMainWindow).toByteArray(); + restoreState(mainWindowState); + } +} + +void MainWindow::saveSettings() const +{ + QSettings settings(settingsName); + + // Save working directory + settings.setValue(settingsDir, m_currentDir.absolutePath()); + + // Save QMainWindow state + settings.setValue(settingsMainWindow, saveState()); + + settings.sync(); +} + +void MainWindow::saveViewerSettings() const +{ + if (!m_viewer) + return; + + QSettings settings(settingsName); + settings.beginGroup(settingsViewers); + settings.setValue(m_viewer->viewerName(), m_viewer->saveState()); + settings.endGroup(); + settings.sync(); +} + +void MainWindow::restoreViewerSettings() +{ + if (!m_viewer) + return; + + QSettings settings(settingsName); + settings.beginGroup(settingsViewers); + QByteArray viewerSettings = settings.value(m_viewer->viewerName(), QByteArray()).toByteArray(); + settings.endGroup(); + if (!viewerSettings.isEmpty()) + m_viewer->restoreState(viewerSettings); +} + diff --git a/examples/demos/documentviewer/mainwindow.h b/examples/demos/documentviewer/mainwindow.h new file mode 100644 index 00000000..fb262cf7 --- /dev/null +++ b/examples/demos/documentviewer/mainwindow.h @@ -0,0 +1,52 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include <QMainWindow> +#include <QDir> +#include <QFileInfo> + +class AbstractViewer; +namespace Ui { +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +public slots: + void openFile(const QString &fileName); + +private slots: + void on_actionOpen_triggered(); + void on_actionAbout_triggered(); + void on_actionAboutQt_triggered(); + +private: + void readSettings(); + void saveSettings() const; + void restoreViewerSettings(); + void saveViewerSettings() const; + + QDir m_currentDir; + std::unique_ptr<Ui::MainWindow> ui; + std::unique_ptr<AbstractViewer> m_viewer; + int m_classId = -1; + + static constexpr QLatin1StringView settingsName = QLatin1StringView("DocumentViewerExample"); + static constexpr QLatin1StringView settingsFiles = QLatin1StringView("RecentFiles"); + static constexpr QLatin1StringView settingsFileName = QLatin1StringView("FileName"); + static constexpr QLatin1StringView settingsDir = QLatin1StringView("WorkingDir"); + static constexpr QLatin1StringView settingsMainWindow = QLatin1StringView("MainWindow"); + static constexpr QLatin1StringView settingsQuestions = QLatin1StringView("Questions"); + static constexpr QLatin1StringView settingsViewers = QLatin1StringView("Viewers"); +}; + +#endif // MAINWINDOW_H diff --git a/examples/demos/documentviewer/mainwindow.ui b/examples/demos/documentviewer/mainwindow.ui new file mode 100644 index 00000000..815384eb --- /dev/null +++ b/examples/demos/documentviewer/mainwindow.ui @@ -0,0 +1,270 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>983</width> + <height>602</height> + </rect> + </property> + <property name="windowTitle"> + <string>Document Viewer Demo</string> + </property> + <property name="windowIcon"> + <iconset resource="../../../../../build/dev/qtdoc/examples/demos/documentviewer/.rcc/documentviewer.qrc"> + <normaloff>:/demos/documentviewer/images/qt-logo.png</normaloff>:/demos/documentviewer/images/qt-logo.png</iconset> + </property> + <widget class="QWidget" name="centralwidget"> + <property name="enabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QWidget" name="viewArea" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QTabWidget" name="tabWidget"> + <property name="tabPosition"> + <enum>QTabWidget::West</enum> + </property> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="bookmarkTab"> + <attribute name="title"> + <string>Pages</string> + </attribute> + </widget> + <widget class="QWidget" name="pagesTab"> + <attribute name="title"> + <string>Bookmarks</string> + </attribute> + </widget> + </widget> + <widget class="QScrollArea" name="scrollArea"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>800</width> + <height>0</height> + </size> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>798</width> + <height>479</height> + </rect> + </property> + </widget> + </widget> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>983</width> + <height>23</height> + </rect> + </property> + <widget class="QMenu" name="qtFileMenu"> + <property name="title"> + <string>File</string> + </property> + <addaction name="actionOpen"/> + <addaction name="actionPrint"/> + <addaction name="actionQuit"/> + </widget> + <widget class="QMenu" name="menuHelp"> + <property name="title"> + <string>Help</string> + </property> + <addaction name="actionAbout"/> + <addaction name="actionAboutQt"/> + </widget> + <addaction name="qtFileMenu"/> + <addaction name="menuHelp"/> + </widget> + <widget class="QStatusBar" name="statusbar"/> + <widget class="QToolBar" name="mainToolBar"> + <property name="windowTitle"> + <string>toolBar</string> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="actionOpen"/> + <addaction name="actionPrint"/> + <addaction name="separator"/> + <addaction name="actionBack"/> + <addaction name="actionForward"/> + <addaction name="separator"/> + </widget> + <action name="actionOpen"> + <property name="icon"> + <iconset theme="document-open"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Open</string> + </property> + <property name="shortcut"> + <string>Ctrl+O</string> + </property> + </action> + <action name="actionAbout"> + <property name="icon"> + <iconset theme="help-about"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>about documentviewer</string> + </property> + <property name="toolTip"> + <string>Show information about the Document Viewer deomo.</string> + </property> + <property name="shortcut"> + <string>Ctrl+H</string> + </property> + </action> + <action name="actionForward"> + <property name="icon"> + <iconset theme="go-next"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>actionForward</string> + </property> + <property name="toolTip"> + <string>One step forward</string> + </property> + <property name="shortcut"> + <string>Right</string> + </property> + </action> + <action name="actionBack"> + <property name="icon"> + <iconset theme="go-previous"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>actionBack</string> + </property> + <property name="toolTip"> + <string>One step back</string> + </property> + <property name="shortcut"> + <string>Left</string> + </property> + </action> + <action name="actionPrint"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="icon"> + <iconset theme="document-print"> + <normaloff>:/demos/documentviewer/images/print.png</normaloff>:/demos/documentviewer/images/print.png</iconset> + </property> + <property name="text"> + <string>Print</string> + </property> + <property name="toolTip"> + <string>Print current file</string> + </property> + <property name="shortcut"> + <string>Ctrl+P</string> + </property> + </action> + <action name="actionAboutQt"> + <property name="icon"> + <iconset resource="../../../../../build/dev/qthttpserver/examples/httpserver/simple/.rcc/assets.qrc"> + <normaloff>:/demos/documentviewer/images/qt-logo.png</normaloff> + <normalon>:/demos/documentviewer/images/qt-logo.png</normalon>:/demos/documentviewer/images/qt-logo.png</iconset> + </property> + <property name="text"> + <string>About Qt</string> + </property> + <property name="toolTip"> + <string>Show Qt license information</string> + </property> + <property name="shortcut"> + <string>Ctrl+I</string> + </property> + </action> + <action name="actionRecent"> + <property name="icon"> + <iconset theme="document-open-recent"> + <normaloff>.</normaloff>.</iconset> + </property> + <property name="text"> + <string>Open</string> + </property> + <property name="shortcut"> + <string>Meta+R</string> + </property> + </action> + <action name="actionQuit"> + <property name="icon"> + <iconset theme="application-exit"/> + </property> + <property name="text"> + <string>Quit</string> + </property> + <property name="toolTip"> + <string>Quit the application</string> + </property> + <property name="shortcut"> + <string>Ctrl+Q</string> + </property> + </action> + </widget> + <resources> + <include location="../../../../../build/dev/qtdoc/examples/demos/documentviewer/.rcc/documentviewer.qrc"/> + <include location="../../../../../build/dev/qthttpserver/examples/httpserver/simple/.rcc/assets.qrc"/> + </resources> + <connections> + <connection> + <sender>actionQuit</sender> + <signal>triggered()</signal> + <receiver>MainWindow</receiver> + <slot>close()</slot> + <hints> + <hint type="sourcelabel"> + <x>-1</x> + <y>-1</y> + </hint> + <hint type="destinationlabel"> + <x>491</x> + <y>300</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/examples/demos/documentviewer/pdfviewer.cpp b/examples/demos/documentviewer/pdfviewer.cpp new file mode 100644 index 00000000..b6093c0b --- /dev/null +++ b/examples/demos/documentviewer/pdfviewer.cpp @@ -0,0 +1,217 @@ +// 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 <QPdfBookmarkModel> +#include <QPdfDocument> +#include <QPdfPageNavigator> +#include <QPdfPageSelector> +#include <QListView> +#include <QPdfView> +#include <QStandardPaths> +#include <QtMath> +#include <QSpinBox> +#include <QToolBar> +#include <QMainWindow> +#include <QTreeView> +#include <QListWidget> +#include <QScrollBar> +#include <QEvent> +#include <QMouseEvent> +#include <QScroller> +#ifdef QT_ABSTRACTVIEWER_PRINTSUPPORT +#include <QPrinter> +#include <QPainter> +#endif + +Q_LOGGING_CATEGORY(lcExample, "qt.examples.pdfviewer") + +PdfViewer::PdfViewer(QFile *file, QWidget *parent, QMainWindow *mainWindow) : + AbstractViewer(file, new QPdfView(parent), mainWindow), m_document(new QPdfDocument(this)) + +{ + m_pdfView = qobject_cast<QPdfView *>(widget()); + connect(this, &AbstractViewer::uiInitialized, this, &PdfViewer::initPdfViewer); +} +PdfViewer::~PdfViewer() +{ + delete m_pages; + delete m_bookmarks; + delete m_document; +} + +void PdfViewer::initPdfViewer() +{ + m_toolBar = addToolBar("PDF"); + m_zoomSelector = new ZoomSelector(m_toolBar); + m_pageSelector = new QPdfPageSelector(m_toolBar); + + m_toolBar->insertWidget(m_uiAssets.forward, m_pageSelector); + + connect(m_pageSelector, &QSpinBox::valueChanged, this, &PdfViewer::pageSelected); + connect(m_pageSelector, &QSpinBox::valueChanged, this, &PdfViewer::pageSelected); + connect(m_pdfView->pageNavigator(), &QPdfPageNavigator::backAvailableChanged, m_uiAssets.back, &QAction::setEnabled); + m_actionBack = m_uiAssets.back; + m_actionForward = m_uiAssets.forward; + connect(m_uiAssets.back, &QAction::triggered, this, &PdfViewer::onActionBackTriggered); + connect(m_uiAssets.forward, &QAction::triggered, this, &PdfViewer::onActionForwardTriggered); + + m_toolBar->addSeparator(); + m_toolBar->addWidget(m_zoomSelector); + + auto *actionZoomIn = m_toolBar->addAction("Zoom in"); + actionZoomIn->setToolTip("Increase zoom level"); + actionZoomIn->setIcon(QIcon(":/demos/documentviewer/images/zoom-in.png")); + m_toolBar->addAction(actionZoomIn); + connect(actionZoomIn, &QAction::triggered, this, &PdfViewer::onActionZoomInTriggered); + + auto *actionZoomOut = m_toolBar->addAction("Zoom out"); + actionZoomOut->setToolTip("Decrease zoom level"); + actionZoomOut->setIcon(QIcon(":/demos/documentviewer/images/zoom-out.png")); + m_toolBar->addAction(actionZoomOut); + connect(actionZoomOut, &QAction::triggered, this, &PdfViewer::onActionZoomOutTriggered); + + auto *nav = m_pdfView->pageNavigator(); + connect(nav, &QPdfPageNavigator::currentPageChanged, m_pageSelector, &QSpinBox::setValue); + 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, "Pages"); + m_uiAssets.tabs->addTab(m_bookmarks, "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() ? documentTitle : QStringLiteral("PDF Viewer")); + pageSelected(0); + m_pageSelector->setMaximum(m_document->pageCount() - 1); + + statusMessage(tr("Opened PDF file %1").arg(m_file->fileName())); + qCDebug(lcExample) << "Opened file" << m_file->fileName(); + + maybeEnablePrinting(); +} + +bool PdfViewer::hasContent() const +{ + return m_document ? m_document->pageCount() > 0 : false; +} + +#if defined(QT_ABSTRACTVIEWER_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_ABSTRACTVIEWER_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(); +} diff --git a/examples/demos/documentviewer/pdfviewer.h b/examples/demos/documentviewer/pdfviewer.h new file mode 100644 index 00000000..d70ca81c --- /dev/null +++ b/examples/demos/documentviewer/pdfviewer.h @@ -0,0 +1,71 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef PDFVIEWER_H +#define PDFVIEWER_H + +#include "abstractviewer.h" +#include <QLoggingCategory> + +Q_DECLARE_LOGGING_CATEGORY(lcExample) + +class QMainWindow; +class QPdfDocument; +class QPdfView; +class QPdfPageSelector; +class QListView; +class QTabWidget; +class QTreeView; +class ZoomSelector; +class PdfViewer : public AbstractViewer +{ +public: + PdfViewer(QFile *file, QWidget *parent, QMainWindow *mainWindow); + + ~PdfViewer() override; + QString viewerName() const override { return staticMetaObject.className(); }; + bool supportsOverview() const override { return true; } + bool hasContent() const override; + QByteArray saveState() const override { return QByteArray(); } + bool restoreState(QByteArray &) override { return true; } + +#if defined(QT_ABSTRACTVIEWER_PRINTSUPPORT) +protected: + void printDocument(QPrinter *printer) const override; +#endif // QT_ABSTRACTVIEWER_PRINTSUPPORT + +public slots: + void openPdfFile(); + +private slots: + void initPdfViewer(); + void bookmarkSelected(const QModelIndex &index); + void pageSelected(int page); + + // action handlers + void onActionOpenTriggered(); + void onActionQuitTriggered(); + void onActionZoomInTriggered(); + void onActionZoomOutTriggered(); + void onActionPreviousPageTriggered(); + void onActionNextPageTriggered(); + void onActionBackTriggered(); + void onActionForwardTriggered(); + +private: + void populateQuestions(); + + const qreal zoomMultiplier = qSqrt(2.0); + static constexpr int maxIconWidth = 200; + QToolBar *m_toolBar = nullptr; + ZoomSelector *m_zoomSelector; + QPdfPageSelector *m_pageSelector; + QPdfDocument *m_document; + QPdfView *m_pdfView; + QAction *m_actionForward = nullptr; + QAction *m_actionBack = nullptr; + QTreeView *m_bookmarks = nullptr; + QListView *m_pages = nullptr; +}; + +#endif //PDFVIEWER_H diff --git a/examples/demos/documentviewer/txtviewer.cpp b/examples/demos/documentviewer/txtviewer.cpp new file mode 100644 index 00000000..b392c985 --- /dev/null +++ b/examples/demos/documentviewer/txtviewer.cpp @@ -0,0 +1,171 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "txtviewer.h" + +#include <QGuiApplication> +#include <QPlainTextEdit> +#include <QTextDocument> +#include <QMenu> +#include <QMenuBar> +#include <QToolBar> +#include <QFileDialog> +#include <QMetaObject> +#include <QScrollBar> +#include <QPainter> +#ifdef QT_ABSTRACTVIEWER_PRINTSUPPORT +#include <QPrinter> +#include <QPrintDialog> +#endif + +TxtViewer::TxtViewer(QFile *file, QWidget *parent, QMainWindow *mainWindow) : + AbstractViewer(file, new QPlainTextEdit(parent), mainWindow) +{ + m_textEdit = qobject_cast<QPlainTextEdit *>(widget()); + connect(this, &AbstractViewer::uiInitialized, this, &TxtViewer::setupTxtUi); +} + +TxtViewer::~TxtViewer() +{ +} + +void TxtViewer::setupTxtUi() +{ + QMenu *editMenu = addMenu(tr("&Edit")); + QToolBar *editToolBar = addToolBar(tr("Edit")); +#ifndef QT_NO_CLIPBOARD + const QIcon cutIcon = QIcon::fromTheme("edit-cut", QIcon(":/demos/documentviewer/images/cut.png")); + 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", QIcon(":/demos/documentviewer/images/copy.png")); + 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", QIcon(":/demos/documentviewer/images/paste.png")); + 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); + }); +} + +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(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(m_file->fileName()), type); + maybeEnablePrinting(); +} + +bool TxtViewer::hasContent() const +{ + return (!m_textEdit->toPlainText().isEmpty()); +} + +#if defined(QT_ABSTRACTVIEWER_PRINTSUPPORT) +void TxtViewer::printDocument(QPrinter *printer) const +{ + if (!hasContent()) + return; + + m_textEdit->print(printer); +} + +#endif // QT_ABSTRACTVIEWER_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(file->fileName()), file->errorString(); + } + QGuiApplication::restoreOverrideCursor(); + + if (!errorMessage.isEmpty()) { + statusMessage(errorMessage); + return false; + } + + statusMessage(tr("File %1 saved").arg(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(); +} diff --git a/examples/demos/documentviewer/txtviewer.h b/examples/demos/documentviewer/txtviewer.h new file mode 100644 index 00000000..2b3ba953 --- /dev/null +++ b/examples/demos/documentviewer/txtviewer.h @@ -0,0 +1,42 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef TXTVIEWER_H +#define TXTVIEWER_H + +#include "abstractviewer.h" +#include <QPointer> + +class QMainWindow; +class QPlainTextEdit; +class QLabel; +class TxtViewer : public AbstractViewer +{ +public: + TxtViewer(QFile *file, QWidget *parent, QMainWindow *mainWindow); + ~TxtViewer() override; + QString viewerName() const override { return staticMetaObject.className(); }; + bool saveDocument() override { return saveFile(m_file.get()); }; + bool saveDocumentAs() override; + bool hasContent() const override; + QByteArray saveState() const override { return QByteArray(); } + bool restoreState(QByteArray &) override { return true; } + +#if defined(QT_ABSTRACTVIEWER_PRINTSUPPORT) +protected: + void printDocument(QPrinter *printer) const override; +#endif // QT_ABSTRACTVIEWER_PRINTSUPPORT + +private slots: + void setupTxtUi(); + void documentWasModified(); + +private: + void openFile(); + bool saveFile (QFile *file); + + int m_classId; + QPlainTextEdit *m_textEdit; +}; + +#endif //TXTVIEWER_H diff --git a/examples/demos/documentviewer/viewerfactory.cpp b/examples/demos/documentviewer/viewerfactory.cpp new file mode 100644 index 00000000..aaceaf54 --- /dev/null +++ b/examples/demos/documentviewer/viewerfactory.cpp @@ -0,0 +1,32 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include <QWidget> +#include <QMimeDatabase> +#include <QMimeType> +#include "viewerfactory.h" + +#include "abstractviewer.h" +#include "pdfviewer.h" +#include "txtviewer.h" +#include "jsonviewer.h" + +AbstractViewer *ViewerFactory::makeViewer(QFile *file, QWidget *displayWidget, + QMainWindow *mainWindow) +{ + Q_ASSERT(file); + + const QFileInfo info(*file); + QMimeDatabase db; + const auto mimeType = db.mimeTypeForFile(info); + + if (mimeType.inherits("application/json")) + return new JsonViewer(file, displayWidget, mainWindow); + if (mimeType.inherits("text/plain")) + return new TxtViewer(file, displayWidget, mainWindow); + if (mimeType.inherits("application/pdf")) + return new PdfViewer(file, displayWidget, mainWindow); + + // Default to text viewer + return new TxtViewer(file, displayWidget, mainWindow); +} diff --git a/examples/demos/documentviewer/viewerfactory.h b/examples/demos/documentviewer/viewerfactory.h new file mode 100644 index 00000000..1a180068 --- /dev/null +++ b/examples/demos/documentviewer/viewerfactory.h @@ -0,0 +1,19 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef VIEWERFACTORY_H +#define VIEWERFACTORY_H + +class AbstractViewer; +class QWidget; +class QMainWindow; +class Questions; +class QFile; +class ViewerFactory +{ +public: + ViewerFactory() = delete; + static AbstractViewer *makeViewer(QFile *file, QWidget *displayWidget, QMainWindow *mainWindow); +}; + +#endif // VIEWERFACTORY_H diff --git a/examples/demos/documentviewer/zoomselector.cpp b/examples/demos/documentviewer/zoomselector.cpp new file mode 100644 index 00000000..2994b567 --- /dev/null +++ b/examples/demos/documentviewer/zoomselector.cpp @@ -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); + } +} diff --git a/examples/demos/documentviewer/zoomselector.h b/examples/demos/documentviewer/zoomselector.h new file mode 100644 index 00000000..7c35bdab --- /dev/null +++ b/examples/demos/documentviewer/zoomselector.h @@ -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 |