summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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