summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAxel Spoerl <axel.spoerl@qt.io>2023-01-26 18:31:16 +0100
committerAxel Spoerl <axel.spoerl@qt.io>2023-03-10 07:51:17 +0000
commit3f133ed2d1d3d4d8800c07fe259bbb1c3ab210b1 (patch)
treece907c7b64e4ead69931577a469e0c7aaeff1f06
parenta04032e770b1b30334a22406a76d2f07ff4fbcff (diff)
downloadqtdoc-3f133ed2d1d3d4d8800c07fe259bbb1c3ab210b1.tar.gz
Add Json, text and pdf document viewer demo
Add a demo with a combined viewer for Json, pdf and text documents. The example demonstrates how to create an application, utilizing a main window with static and dynamic toolbars, menus and actions. Type specific viewers inherit from an abstract viewer class. The Json viewer implements an abstract tree model for QJsonDocument. The example demonstrates a few useful application features, implemented with QtWidgets and QSettings, to (re)store properties: - Detecting file content types with QMimeDatabase - Changing and restoring override cursor when hovering over a widget. - Basic printing support. Change-Id: Ie411669cfdbc41ac399cd1aed87e60722195c42c Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io> (cherry picked from commit 9bba48e1cad2f9c59d3407a45686e99b4cee9159) Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
-rw-r--r--dependencies.yaml4
-rw-r--r--examples/demos/CMakeLists.txt3
-rw-r--r--examples/demos/documentviewer/CMakeLists.txt93
-rw-r--r--examples/demos/documentviewer/abstractviewer.cpp161
-rw-r--r--examples/demos/documentviewer/abstractviewer.h101
-rw-r--r--examples/demos/documentviewer/doc/images/documentviewer_open.pngbin0 -> 43176 bytes
-rw-r--r--examples/demos/documentviewer/doc/src/documentviewer.qdoc207
-rw-r--r--examples/demos/documentviewer/hoverwatcher.cpp212
-rw-r--r--examples/demos/documentviewer/hoverwatcher.h66
-rw-r--r--examples/demos/documentviewer/images/copy.pngbin0 -> 1338 bytes
-rw-r--r--examples/demos/documentviewer/images/copy@2x.pngbin0 -> 2219 bytes
-rw-r--r--examples/demos/documentviewer/images/cut.pngbin0 -> 1323 bytes
-rw-r--r--examples/demos/documentviewer/images/cut@2x.pngbin0 -> 2888 bytes
-rw-r--r--examples/demos/documentviewer/images/go-next-view-page.pngbin0 -> 1125 bytes
-rw-r--r--examples/demos/documentviewer/images/go-next-view-page@2x.pngbin0 -> 2312 bytes
-rw-r--r--examples/demos/documentviewer/images/go-next-view.pngbin0 -> 990 bytes
-rw-r--r--examples/demos/documentviewer/images/go-next-view@2x.pngbin0 -> 1830 bytes
-rw-r--r--examples/demos/documentviewer/images/go-previous-view-page.pngbin0 -> 1119 bytes
-rw-r--r--examples/demos/documentviewer/images/go-previous-view-page@2x.pngbin0 -> 2067 bytes
-rw-r--r--examples/demos/documentviewer/images/go-previous-view.pngbin0 -> 1010 bytes
-rw-r--r--examples/demos/documentviewer/images/go-previous-view@2x.pngbin0 -> 1875 bytes
-rw-r--r--examples/demos/documentviewer/images/magnifier.pngbin0 -> 1006 bytes
-rw-r--r--examples/demos/documentviewer/images/magnifier@2x.pngbin0 -> 2249 bytes
-rw-r--r--examples/demos/documentviewer/images/open.pngbin0 -> 2073 bytes
-rw-r--r--examples/demos/documentviewer/images/open@2x.pngbin0 -> 1017 bytes
-rw-r--r--examples/demos/documentviewer/images/paste.pngbin0 -> 1645 bytes
-rw-r--r--examples/demos/documentviewer/images/paste@2x.pngbin0 -> 2782 bytes
-rw-r--r--examples/demos/documentviewer/images/print.pngbin0 -> 678 bytes
-rw-r--r--examples/demos/documentviewer/images/print2x.pngbin0 -> 931 bytes
-rw-r--r--examples/demos/documentviewer/images/qt-logo.pngbin0 -> 1483 bytes
-rw-r--r--examples/demos/documentviewer/images/qt-logo@2x.pngbin0 -> 1933 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-fit-best.pngbin0 -> 1067 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-fit-best@2x.pngbin0 -> 1692 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-fit-width.pngbin0 -> 905 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-fit-width@2x.pngbin0 -> 1724 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-in.pngbin0 -> 952 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-in@2x.pngbin0 -> 2100 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-original.pngbin0 -> 946 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-original@2x.pngbin0 -> 2005 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-out.pngbin0 -> 940 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-out@2x.pngbin0 -> 1971 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-previous.pngbin0 -> 946 bytes
-rw-r--r--examples/demos/documentviewer/images/zoom-previous@2x.pngbin0 -> 2049 bytes
-rw-r--r--examples/demos/documentviewer/jsonviewer.cpp532
-rw-r--r--examples/demos/documentviewer/jsonviewer.h107
-rw-r--r--examples/demos/documentviewer/main.cpp34
-rw-r--r--examples/demos/documentviewer/mainwindow.cpp128
-rw-r--r--examples/demos/documentviewer/mainwindow.h52
-rw-r--r--examples/demos/documentviewer/mainwindow.ui270
-rw-r--r--examples/demos/documentviewer/pdfviewer.cpp217
-rw-r--r--examples/demos/documentviewer/pdfviewer.h71
-rw-r--r--examples/demos/documentviewer/txtviewer.cpp171
-rw-r--r--examples/demos/documentviewer/txtviewer.h42
-rw-r--r--examples/demos/documentviewer/viewerfactory.cpp32
-rw-r--r--examples/demos/documentviewer/viewerfactory.h19
-rw-r--r--examples/demos/documentviewer/zoomselector.cpp64
-rw-r--r--examples/demos/documentviewer/zoomselector.h30
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
new file mode 100644
index 00000000..8a05c2fc
--- /dev/null
+++ b/examples/demos/documentviewer/doc/images/documentviewer_open.png
Binary files differ
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
new file mode 100644
index 00000000..2aeb2828
--- /dev/null
+++ b/examples/demos/documentviewer/images/copy.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/copy@2x.png b/examples/demos/documentviewer/images/copy@2x.png
new file mode 100644
index 00000000..f4ebabba
--- /dev/null
+++ b/examples/demos/documentviewer/images/copy@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/cut.png b/examples/demos/documentviewer/images/cut.png
new file mode 100644
index 00000000..54638e93
--- /dev/null
+++ b/examples/demos/documentviewer/images/cut.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/cut@2x.png b/examples/demos/documentviewer/images/cut@2x.png
new file mode 100644
index 00000000..5a5da4fd
--- /dev/null
+++ b/examples/demos/documentviewer/images/cut@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/go-next-view-page.png b/examples/demos/documentviewer/images/go-next-view-page.png
new file mode 100644
index 00000000..bd2a3383
--- /dev/null
+++ b/examples/demos/documentviewer/images/go-next-view-page.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/go-next-view-page@2x.png b/examples/demos/documentviewer/images/go-next-view-page@2x.png
new file mode 100644
index 00000000..5ddcbbcc
--- /dev/null
+++ b/examples/demos/documentviewer/images/go-next-view-page@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/go-next-view.png b/examples/demos/documentviewer/images/go-next-view.png
new file mode 100644
index 00000000..98b79dea
--- /dev/null
+++ b/examples/demos/documentviewer/images/go-next-view.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/go-next-view@2x.png b/examples/demos/documentviewer/images/go-next-view@2x.png
new file mode 100644
index 00000000..91940643
--- /dev/null
+++ b/examples/demos/documentviewer/images/go-next-view@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/go-previous-view-page.png b/examples/demos/documentviewer/images/go-previous-view-page.png
new file mode 100644
index 00000000..ecd3768e
--- /dev/null
+++ b/examples/demos/documentviewer/images/go-previous-view-page.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/go-previous-view-page@2x.png b/examples/demos/documentviewer/images/go-previous-view-page@2x.png
new file mode 100644
index 00000000..f0d91c9f
--- /dev/null
+++ b/examples/demos/documentviewer/images/go-previous-view-page@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/go-previous-view.png b/examples/demos/documentviewer/images/go-previous-view.png
new file mode 100644
index 00000000..086bd9a1
--- /dev/null
+++ b/examples/demos/documentviewer/images/go-previous-view.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/go-previous-view@2x.png b/examples/demos/documentviewer/images/go-previous-view@2x.png
new file mode 100644
index 00000000..900860ce
--- /dev/null
+++ b/examples/demos/documentviewer/images/go-previous-view@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/magnifier.png b/examples/demos/documentviewer/images/magnifier.png
new file mode 100644
index 00000000..6eb457d9
--- /dev/null
+++ b/examples/demos/documentviewer/images/magnifier.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/magnifier@2x.png b/examples/demos/documentviewer/images/magnifier@2x.png
new file mode 100644
index 00000000..ed84af18
--- /dev/null
+++ b/examples/demos/documentviewer/images/magnifier@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/open.png b/examples/demos/documentviewer/images/open.png
new file mode 100644
index 00000000..45fa2883
--- /dev/null
+++ b/examples/demos/documentviewer/images/open.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/open@2x.png b/examples/demos/documentviewer/images/open@2x.png
new file mode 100644
index 00000000..12c2c3c1
--- /dev/null
+++ b/examples/demos/documentviewer/images/open@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/paste.png b/examples/demos/documentviewer/images/paste.png
new file mode 100644
index 00000000..c14425ca
--- /dev/null
+++ b/examples/demos/documentviewer/images/paste.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/paste@2x.png b/examples/demos/documentviewer/images/paste@2x.png
new file mode 100644
index 00000000..360b0f6c
--- /dev/null
+++ b/examples/demos/documentviewer/images/paste@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/print.png b/examples/demos/documentviewer/images/print.png
new file mode 100644
index 00000000..4ace2614
--- /dev/null
+++ b/examples/demos/documentviewer/images/print.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/print2x.png b/examples/demos/documentviewer/images/print2x.png
new file mode 100644
index 00000000..1c3655be
--- /dev/null
+++ b/examples/demos/documentviewer/images/print2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/qt-logo.png b/examples/demos/documentviewer/images/qt-logo.png
new file mode 100644
index 00000000..c9171422
--- /dev/null
+++ b/examples/demos/documentviewer/images/qt-logo.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/qt-logo@2x.png b/examples/demos/documentviewer/images/qt-logo@2x.png
new file mode 100644
index 00000000..95d1d09b
--- /dev/null
+++ b/examples/demos/documentviewer/images/qt-logo@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-fit-best.png b/examples/demos/documentviewer/images/zoom-fit-best.png
new file mode 100644
index 00000000..6a13de4c
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-fit-best.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-fit-best@2x.png b/examples/demos/documentviewer/images/zoom-fit-best@2x.png
new file mode 100644
index 00000000..904b41c8
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-fit-best@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-fit-width.png b/examples/demos/documentviewer/images/zoom-fit-width.png
new file mode 100644
index 00000000..d51fbac6
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-fit-width.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-fit-width@2x.png b/examples/demos/documentviewer/images/zoom-fit-width@2x.png
new file mode 100644
index 00000000..4d1fd0b4
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-fit-width@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-in.png b/examples/demos/documentviewer/images/zoom-in.png
new file mode 100644
index 00000000..5ae1046c
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-in.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-in@2x.png b/examples/demos/documentviewer/images/zoom-in@2x.png
new file mode 100644
index 00000000..863ef4ac
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-in@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-original.png b/examples/demos/documentviewer/images/zoom-original.png
new file mode 100644
index 00000000..8aa9bb49
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-original.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-original@2x.png b/examples/demos/documentviewer/images/zoom-original@2x.png
new file mode 100644
index 00000000..d5473007
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-original@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-out.png b/examples/demos/documentviewer/images/zoom-out.png
new file mode 100644
index 00000000..081b6d98
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-out.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-out@2x.png b/examples/demos/documentviewer/images/zoom-out@2x.png
new file mode 100644
index 00000000..34c8e174
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-out@2x.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-previous.png b/examples/demos/documentviewer/images/zoom-previous.png
new file mode 100644
index 00000000..0ff5c041
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-previous.png
Binary files differ
diff --git a/examples/demos/documentviewer/images/zoom-previous@2x.png b/examples/demos/documentviewer/images/zoom-previous@2x.png
new file mode 100644
index 00000000..e9909abc
--- /dev/null
+++ b/examples/demos/documentviewer/images/zoom-previous@2x.png
Binary files differ
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 &current, 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