commit 45c681e7bee443e08f848bf2f6ae57f9d9741960 Author: talksik Date: Wed Feb 7 13:12:51 2024 -0800 making clangd happy and making script to compile/build with run.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a6df09c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(documentviewer LANGUAGES CXX) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets + OPTIONAL_COMPONENTS PrintSupport Pdf PdfWidgets) + +qt_standard_project_setup() + +add_compile_definitions(QT_NO_CAST_FROM_ASCII) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +if(TARGET Qt6::PrintSupport) + add_compile_definitions(QT_DOCUMENTVIEWER_PRINTSUPPORT) +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/demos/documentviewer") + +add_subdirectory(app) +add_subdirectory(plugins) diff --git a/CMakeLists.txt.user b/CMakeLists.txt.user new file mode 100644 index 0000000..af067ae --- /dev/null +++ b/CMakeLists.txt.user @@ -0,0 +1,417 @@ + + + + + + EnvironmentId + {c8be0ad5-6296-489b-9771-86900341c424} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + 0 + false + true + false + 0 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + + 0 + true + + true + true + Builtin.DefaultTidyAndClazy + 4 + true + + + + true + + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop Qt 6.6.1 GCC 64bit + Desktop Qt 6.6.1 GCC 64bit + qt.qt6.661.gcc_64_kit + 0 + 0 + 0 + + Debug + 2 + false + + -DCMAKE_GENERATOR:STRING=Ninja +-DCMAKE_BUILD_TYPE:STRING=Debug +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} + 0 + /home/talksik/Qt/Examples/Qt-6.6.1/demos/build-documentviewer-Desktop_Qt_6_6_1_GCC_64bit-Debug + + + + + all + + false + + true + Build + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + Build + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Debug + CMakeProjectManager.CMakeBuildConfiguration + + + Release + 2 + false + + -DCMAKE_GENERATOR:STRING=Ninja +-DCMAKE_BUILD_TYPE:STRING=Release +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} + /home/talksik/Qt/Examples/Qt-6.6.1/demos/build-documentviewer-Desktop_Qt_6_6_1_GCC_64bit-Release + + + + + all + + false + + true + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Release + CMakeProjectManager.CMakeBuildConfiguration + + + RelWithDebInfo + 2 + false + + -DCMAKE_GENERATOR:STRING=Ninja +-DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} + /home/talksik/Qt/Examples/Qt-6.6.1/demos/build-documentviewer-Desktop_Qt_6_6_1_GCC_64bit-RelWithDebInfo + + + + + all + + false + + true + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Release with Debug Information + CMakeProjectManager.CMakeBuildConfiguration + + + RelWithDebInfo + 2 + false + + -DCMAKE_GENERATOR:STRING=Ninja +-DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} + 0 + /home/talksik/Qt/Examples/Qt-6.6.1/demos/build-documentviewer-Desktop_Qt_6_6_1_GCC_64bit-Profile + + + + + all + + false + + true + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Profile + CMakeProjectManager.CMakeBuildConfiguration + + + MinSizeRel + 2 + false + + -DCMAKE_GENERATOR:STRING=Ninja +-DCMAKE_BUILD_TYPE:STRING=MinSizeRel +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} + /home/talksik/Qt/Examples/Qt-6.6.1/demos/build-documentviewer-Desktop_Qt_6_6_1_GCC_64bit-MinSizeRel + + + + + all + + false + + true + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Minimum Size Release + CMakeProjectManager.CMakeBuildConfiguration + + 5 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + true + true + 0 + true + + 2 + + false + + ProjectExplorer.CustomExecutableRunConfiguration + + false + true + true + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..34a3e80 --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,68 @@ +cmake_minimum_required(VERSION 3.16) +project(documentviewer LANGUAGES CXX) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) + +set(CMAKE_AUTORCC ON) + +qt_standard_project_setup() + +qt_add_library(abstractviewer STATIC + abstractviewer.cpp abstractviewer.h +) + +qt_add_executable(documentviewer + main.cpp + mainwindow.cpp mainwindow.h mainwindow.ui + viewerfactory.cpp viewerfactory.h + recentfiles.cpp recentfiles.h + recentfilemenu.cpp recentfilemenu.h + viewerinterfaces.h + documentviewer.qrc +) + +set_target_properties(documentviewer PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_link_libraries(documentviewer PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + abstractviewer +) + +target_link_libraries(abstractviewer PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets +) + +set(plugin_targets + jsonviewer + txtviewer +) + +if(TARGET pdfviewer) + list(APPEND plugin_targets pdfviewer) +endif() + +if(QT6_IS_SHARED_LIBS_BUILD) + add_dependencies(documentviewer ${plugin_targets}) +else() + target_link_libraries(documentviewer PRIVATE ${plugin_targets}) +endif() + +if(TARGET Qt6::PrintSupport) + target_link_libraries(documentviewer PRIVATE Qt6::PrintSupport) + target_link_libraries(abstractviewer PRIVATE Qt6::PrintSupport) + add_compile_definitions(QT_DOCUMENTVIEWER_PRINTSUPPORT) +endif() + +install(TARGETS documentviewer + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) + diff --git a/app/abstractviewer.cpp b/app/abstractviewer.cpp new file mode 100644 index 0000000..b9ebf8f --- /dev/null +++ b/app/abstractviewer.cpp @@ -0,0 +1,281 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "abstractviewer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +#include +#include +#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT + +using namespace Qt::StringLiterals; + +AbstractViewer::AbstractViewer() : m_file(nullptr), m_widget(nullptr) +{ +} + +void AbstractViewer::init(QFile *file, QWidget *widget, QMainWindow *mainWindow) +{ + m_file.reset(file); + m_widget = widget; + Q_ASSERT(widget); + Q_ASSERT(mainWindow); + m_uiAssets.mainWindow = mainWindow; +} + +AbstractViewer::~AbstractViewer() +{ + AbstractViewer::cleanup(); +} + +bool AbstractViewer::isEmpty() const +{ + return !hasContent(); +} + +bool AbstractViewer::isPrintingEnabled() const +{ + return m_printingEnabled; +} + +bool AbstractViewer::hasContent() const +{ + return false; +} + +bool AbstractViewer::supportsOverview() const +{ + return false; +} + +bool AbstractViewer::isModified() const +{ + return false; +} + +bool AbstractViewer::saveDocument() +{ + return false; +} + +bool AbstractViewer::saveDocumentAs() +{ + return false; +} + +QList AbstractViewer::actions() const +{ + return m_actions; +} + +QWidget *AbstractViewer::widget() const +{ + return m_widget; +} + +QList AbstractViewer::menus() const +{ + return m_menus; +} + +QMainWindow *AbstractViewer::mainWindow() const +{ + return m_uiAssets.mainWindow; +} + +QStatusBar *AbstractViewer::statusBar() const +{ + return mainWindow()->statusBar(); +} + +QMenuBar *AbstractViewer::menuBar() const +{ + return mainWindow()->menuBar(); +} + +void AbstractViewer::maybeEnablePrinting() +{ + maybeSetPrintingEnabled(true); +} + +void AbstractViewer::disablePrinting() +{ + maybeSetPrintingEnabled(false); +} + +bool AbstractViewer::isDefaultViewer() const +{ + return false; +} + +AbstractViewer *AbstractViewer::viewer() +{ + return this; +} + +const AbstractViewer *AbstractViewer::viewer() const +{ + return this; +} + +void AbstractViewer::statusMessage(const QString &message, const QString &type, int timeout) +{ + const QString msg = viewerName() + + (type.isEmpty() ? ": "_L1 : "/"_L1 + type + ": "_L1) + message; + emit showMessage(msg, timeout); +} + +QToolBar *AbstractViewer::addToolBar(const QString &title) +{ + auto *bar = mainWindow()->addToolBar(title); + QString name = title; + name.remove(u' '); + bar->setObjectName(name); + m_toolBars.append(bar); + return bar; +} + +QMenu *AbstractViewer::addMenu(const QString &title) +{ + QMenu *menu = new QMenu(title, menuBar()); + menu->setObjectName(title); + menuBar()->insertMenu(m_uiAssets.help, menu); + m_menus.append(menu); + return menu; +} + +void AbstractViewer::cleanup() +{ + // delete all objects created by the viewer which need to be displayed + // and therefore parented on MainWindow + if (m_file) + m_file.reset(); + + qDeleteAll(m_menus); + m_menus.clear(); + qDeleteAll(m_toolBars); + m_toolBars.clear(); + + for (const auto &connection : m_connections) + QObject::disconnect(connection); + + m_connections.clear(); +} + +QMenu *AbstractViewer::fileMenu() +{ + static constexpr QLatin1StringView name = QLatin1StringView("qtFileMenu"); + static QMenu *fileMenu = nullptr; + if (fileMenu) + return fileMenu; + + const QList menus = mainWindow()->findChildren(); + 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_DOCUMENTVIEWER_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() + " :"_L1; + 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_DOCUMENTVIEWER_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/app/abstractviewer.h b/app/abstractviewer.h new file mode 100644 index 0000000..15dec4f --- /dev/null +++ b/app/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 + +QT_BEGIN_NAMESPACE +class QAction; +class QFile; +class QMainWindow; +class QMenu; +class QMenuBar; +class QPrinter; +class QToolBar; +class QTabWidget; +class QScrollArea; +class QStatusBar; +QT_END_NAMESPACE + +class AbstractViewer : public QObject +{ + Q_OBJECT + +protected: + AbstractViewer(); + +public: + virtual ~AbstractViewer(); + virtual void init(QFile *file, QWidget *widget, QMainWindow *mainWindow); + void initViewer(QAction *back, QAction *forward, QAction *help, QTabWidget *tabs); + virtual bool isModified() const; + virtual bool saveDocument(); + virtual bool saveDocumentAs(); + virtual QString viewerName() const = 0; + virtual bool supportsOverview() const; + virtual QByteArray saveState() const = 0; + virtual bool restoreState(QByteArray &) = 0; + virtual bool hasContent() const; + virtual QStringList supportedMimeTypes() const = 0; + virtual bool isDefaultViewer() const; + virtual void cleanup(); + bool isEmpty() const; + bool isPrintingEnabled() const; + AbstractViewer *viewer(); + const AbstractViewer *viewer() const; + + QList actions() const; + QWidget *widget() const; + QList menus() const; + +#ifdef QT_DOCUMENTVIEWER_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; + QStatusBar *statusBar() const; + QMenuBar *menuBar() const; + + std::unique_ptr m_file; + QList m_actions; + QWidget *m_widget = nullptr; + QList m_connections; + +protected slots: + void maybeSetPrintingEnabled(bool enabled); + void maybeEnablePrinting(); + void disablePrinting(); + +private: + QList m_menus; + QList m_toolBars; + bool m_printingEnabled = false; +}; + +#endif // ABSTRACTVIEWER_H diff --git a/app/documentviewer.qrc b/app/documentviewer.qrc new file mode 100644 index 0000000..c222e49 --- /dev/null +++ b/app/documentviewer.qrc @@ -0,0 +1,42 @@ + + + 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 + images/document-open-recent.svgz + images/go-next.svgz + images/go-previous.svgz + images/help-about.svgz + + diff --git a/app/images/copy.png b/app/images/copy.png new file mode 100644 index 0000000..2aeb282 Binary files /dev/null and b/app/images/copy.png differ diff --git a/app/images/copy@2x.png b/app/images/copy@2x.png new file mode 100644 index 0000000..f4ebabb Binary files /dev/null and b/app/images/copy@2x.png differ diff --git a/app/images/cut.png b/app/images/cut.png new file mode 100644 index 0000000..54638e9 Binary files /dev/null and b/app/images/cut.png differ diff --git a/app/images/cut@2x.png b/app/images/cut@2x.png new file mode 100644 index 0000000..5a5da4f Binary files /dev/null and b/app/images/cut@2x.png differ diff --git a/app/images/document-open-recent.svgz b/app/images/document-open-recent.svgz new file mode 100644 index 0000000..3b6eaf9 Binary files /dev/null and b/app/images/document-open-recent.svgz differ diff --git a/app/images/go-next-view-page.png b/app/images/go-next-view-page.png new file mode 100644 index 0000000..bd2a338 Binary files /dev/null and b/app/images/go-next-view-page.png differ diff --git a/app/images/go-next-view-page@2x.png b/app/images/go-next-view-page@2x.png new file mode 100644 index 0000000..5ddcbbc Binary files /dev/null and b/app/images/go-next-view-page@2x.png differ diff --git a/app/images/go-next-view.png b/app/images/go-next-view.png new file mode 100644 index 0000000..98b79de Binary files /dev/null and b/app/images/go-next-view.png differ diff --git a/app/images/go-next-view@2x.png b/app/images/go-next-view@2x.png new file mode 100644 index 0000000..9194064 Binary files /dev/null and b/app/images/go-next-view@2x.png differ diff --git a/app/images/go-next.svgz b/app/images/go-next.svgz new file mode 100644 index 0000000..c6dc7d0 Binary files /dev/null and b/app/images/go-next.svgz differ diff --git a/app/images/go-previous-view-page.png b/app/images/go-previous-view-page.png new file mode 100644 index 0000000..ecd3768 Binary files /dev/null and b/app/images/go-previous-view-page.png differ diff --git a/app/images/go-previous-view-page@2x.png b/app/images/go-previous-view-page@2x.png new file mode 100644 index 0000000..f0d91c9 Binary files /dev/null and b/app/images/go-previous-view-page@2x.png differ diff --git a/app/images/go-previous-view.png b/app/images/go-previous-view.png new file mode 100644 index 0000000..086bd9a Binary files /dev/null and b/app/images/go-previous-view.png differ diff --git a/app/images/go-previous-view@2x.png b/app/images/go-previous-view@2x.png new file mode 100644 index 0000000..900860c Binary files /dev/null and b/app/images/go-previous-view@2x.png differ diff --git a/app/images/go-previous.svgz b/app/images/go-previous.svgz new file mode 100644 index 0000000..971b1d9 Binary files /dev/null and b/app/images/go-previous.svgz differ diff --git a/app/images/help-about.svgz b/app/images/help-about.svgz new file mode 100644 index 0000000..001aa7f Binary files /dev/null and b/app/images/help-about.svgz differ diff --git a/app/images/magnifier.png b/app/images/magnifier.png new file mode 100644 index 0000000..6eb457d Binary files /dev/null and b/app/images/magnifier.png differ diff --git a/app/images/magnifier@2x.png b/app/images/magnifier@2x.png new file mode 100644 index 0000000..ed84af1 Binary files /dev/null and b/app/images/magnifier@2x.png differ diff --git a/app/images/open.png b/app/images/open.png new file mode 100644 index 0000000..45fa288 Binary files /dev/null and b/app/images/open.png differ diff --git a/app/images/open@2x.png b/app/images/open@2x.png new file mode 100644 index 0000000..12c2c3c Binary files /dev/null and b/app/images/open@2x.png differ diff --git a/app/images/paste.png b/app/images/paste.png new file mode 100644 index 0000000..c14425c Binary files /dev/null and b/app/images/paste.png differ diff --git a/app/images/paste@2x.png b/app/images/paste@2x.png new file mode 100644 index 0000000..360b0f6 Binary files /dev/null and b/app/images/paste@2x.png differ diff --git a/app/images/print.png b/app/images/print.png new file mode 100644 index 0000000..4ace261 Binary files /dev/null and b/app/images/print.png differ diff --git a/app/images/print2x.png b/app/images/print2x.png new file mode 100644 index 0000000..1c3655b Binary files /dev/null and b/app/images/print2x.png differ diff --git a/app/images/qt-logo.png b/app/images/qt-logo.png new file mode 100644 index 0000000..c917142 Binary files /dev/null and b/app/images/qt-logo.png differ diff --git a/app/images/qt-logo@2x.png b/app/images/qt-logo@2x.png new file mode 100644 index 0000000..95d1d09 Binary files /dev/null and b/app/images/qt-logo@2x.png differ diff --git a/app/images/zoom-fit-best.png b/app/images/zoom-fit-best.png new file mode 100644 index 0000000..6a13de4 Binary files /dev/null and b/app/images/zoom-fit-best.png differ diff --git a/app/images/zoom-fit-best@2x.png b/app/images/zoom-fit-best@2x.png new file mode 100644 index 0000000..904b41c Binary files /dev/null and b/app/images/zoom-fit-best@2x.png differ diff --git a/app/images/zoom-fit-width.png b/app/images/zoom-fit-width.png new file mode 100644 index 0000000..d51fbac Binary files /dev/null and b/app/images/zoom-fit-width.png differ diff --git a/app/images/zoom-fit-width@2x.png b/app/images/zoom-fit-width@2x.png new file mode 100644 index 0000000..4d1fd0b Binary files /dev/null and b/app/images/zoom-fit-width@2x.png differ diff --git a/app/images/zoom-in.png b/app/images/zoom-in.png new file mode 100644 index 0000000..5ae1046 Binary files /dev/null and b/app/images/zoom-in.png differ diff --git a/app/images/zoom-in@2x.png b/app/images/zoom-in@2x.png new file mode 100644 index 0000000..863ef4a Binary files /dev/null and b/app/images/zoom-in@2x.png differ diff --git a/app/images/zoom-original.png b/app/images/zoom-original.png new file mode 100644 index 0000000..8aa9bb4 Binary files /dev/null and b/app/images/zoom-original.png differ diff --git a/app/images/zoom-original@2x.png b/app/images/zoom-original@2x.png new file mode 100644 index 0000000..d547300 Binary files /dev/null and b/app/images/zoom-original@2x.png differ diff --git a/app/images/zoom-out.png b/app/images/zoom-out.png new file mode 100644 index 0000000..081b6d9 Binary files /dev/null and b/app/images/zoom-out.png differ diff --git a/app/images/zoom-out@2x.png b/app/images/zoom-out@2x.png new file mode 100644 index 0000000..34c8e17 Binary files /dev/null and b/app/images/zoom-out@2x.png differ diff --git a/app/images/zoom-previous.png b/app/images/zoom-previous.png new file mode 100644 index 0000000..0ff5c04 Binary files /dev/null and b/app/images/zoom-previous.png differ diff --git a/app/images/zoom-previous@2x.png b/app/images/zoom-previous@2x.png new file mode 100644 index 0000000..e9909ab Binary files /dev/null and b/app/images/zoom-previous@2x.png differ diff --git a/app/main.cpp b/app/main.cpp new file mode 100644 index 0000000..c57d07f --- /dev/null +++ b/app/main.cpp @@ -0,0 +1,36 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "mainwindow.h" +#include +#include + +using namespace Qt::StringLiterals; + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + QCoreApplication::setOrganizationName("QtProject"_L1); + QCoreApplication::setApplicationName("DocumentViewer"_L1); + QCoreApplication::setApplicationVersion("1.0"_L1); + + QCommandLineParser parser; + parser.setApplicationDescription(QApplication::translate("main", + "A viewer for JSON, PDF and text files")); + parser.addHelpOption(); + parser.addVersionOption(); + parser.addPositionalArgument("File"_L1, 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/app/mainwindow.cpp b/app/mainwindow.cpp new file mode 100644 index 0000000..6d52dec --- /dev/null +++ b/app/mainwindow.cpp @@ -0,0 +1,190 @@ +// 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 "viewerfactory.h" +#include "abstractviewer.h" +#include "recentfiles.h" +#include "recentfilemenu.h" + +#include +#include +#include + +#include +#include + +using namespace Qt::StringLiterals; + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow) +{ + ui->setupUi(this); + connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::onActionOpenTriggered); + connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::onActionAboutTriggered); + connect(ui->actionAboutQt, &QAction::triggered, this, &MainWindow::onActionAboutQtTriggered); + + m_recentFiles.reset(new RecentFiles(ui->actionRecent)); + connect(m_recentFiles.get(), &RecentFiles::countChanged, this, [&](int count){ + ui->actionRecent->setText(tr("%n recent files", nullptr, count)); + }); + + readSettings(); + m_factory.reset(new ViewerFactory(ui->viewArea, this)); + const QStringList &viewers = m_factory->viewerNames(); + + const QString msg = tr("Available viewers: %1").arg(viewers.join(", "_L1)); + statusBar()->showMessage(msg); + + auto *menu = new RecentFileMenu(this, m_recentFiles.get()); + ui->actionRecent->setMenu(menu); + connect(menu, &RecentFileMenu::fileOpened, this, &MainWindow::openFile); + QWidget *w = ui->mainToolBar->widgetForAction(ui->actionRecent); + auto *button = qobject_cast(w); + if (button) + connect(ui->actionRecent, &QAction::triggered, button, &QToolButton::showMenu); +} + +MainWindow::~MainWindow() +{ + saveSettings(); +} + +void MainWindow::onActionOpenTriggered() +{ + QFileDialog fileDialog(this, tr("Open Document"), m_currentDir.absolutePath()); + while (fileDialog.exec() == QDialog::Accepted + && !openFile(fileDialog.selectedFiles().constFirst())) { + } +} + +bool MainWindow::openFile(const QString &fileName) +{ + QFile *file = new QFile(fileName); + if (!file->exists()) { + statusBar()->showMessage(tr("File %1 could not be opened") + .arg(QDir::toNativeSeparators(fileName))); + delete file; + return false; + } + + QFileInfo fileInfo(*file); + m_currentDir = fileInfo.dir(); + m_recentFiles->addFile(fileInfo.absoluteFilePath()); + + // If a viewer is already open, clean it up and save its settings + resetViewer(); + m_viewer = m_factory->viewer(file); + if (!m_viewer) { + statusBar()->showMessage(tr("File %1 can't be opened.") + .arg(QDir::toNativeSeparators(fileName))); + return false; + } + + ui->actionPrint->setEnabled(m_viewer->hasContent()); + connect(m_viewer, &AbstractViewer::printingEnabledChanged, ui->actionPrint, &QAction::setEnabled); + connect(ui->actionPrint, &QAction::triggered, m_viewer, &AbstractViewer::print); + connect(m_viewer, &AbstractViewer::showMessage, statusBar(), &QStatusBar::showMessage); + + m_viewer->initViewer(ui->actionBack, ui->actionForward, ui->menuHelp->menuAction(), ui->tabWidget); + restoreViewerSettings(); + ui->scrollArea->setWidget(m_viewer->widget()); + return true; +} + +void MainWindow::onActionAboutTriggered() +{ + const QString viewerNames = m_factory->viewerNames().join(", "_L1); + const QString mimeTypes = m_factory->supportedMimeTypes().join(u'\n'); + QString text = tr("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.\n\n" + "This version has loaded the following plugins:\n%1\n" + "\n\nIt supports the following mime types:\n%2") + .arg(viewerNames, mimeTypes); + + if (auto *def = m_factory->defaultViewer()) + text += tr("\n\nOther mime types will be displayed with %1.").arg(def->viewerName()); + + QMessageBox::about(this, tr("About Document Viewer Demo"), text); +} + +void MainWindow::onActionAboutQtTriggered() +{ + QMessageBox::aboutQt(this); +} + +void MainWindow::readSettings() +{ + QSettings settings; + + // 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); + } + + // Restore recent files + m_recentFiles->restoreFromSettings(settings, settingsFiles); +} + +void MainWindow::saveSettings() const +{ + QSettings settings; + + // Save working directory + settings.setValue(settingsDir, m_currentDir.absolutePath()); + + // Save QMainWindow state + settings.setValue(settingsMainWindow, saveState()); + + // Save recent files + m_recentFiles->saveSettings(settings, settingsFiles); + + settings.sync(); +} + +void MainWindow::saveViewerSettings() const +{ + if (!m_viewer) + return; + + QSettings settings; + settings.beginGroup(settingsViewers); + settings.setValue(m_viewer->viewerName(), m_viewer->saveState()); + settings.endGroup(); + settings.sync(); +} + +void MainWindow::resetViewer() const +{ + if (!m_viewer) + return; + + saveViewerSettings(); + m_viewer->cleanup(); +} + +void MainWindow::restoreViewerSettings() +{ + if (!m_viewer) + return; + + QSettings settings; + settings.beginGroup(settingsViewers); + QByteArray viewerSettings = settings.value(m_viewer->viewerName(), QByteArray()).toByteArray(); + settings.endGroup(); + if (!viewerSettings.isEmpty()) + m_viewer->restoreState(viewerSettings); +} + diff --git a/app/mainwindow.h b/app/mainwindow.h new file mode 100644 index 0000000..d273e54 --- /dev/null +++ b/app/mainwindow.h @@ -0,0 +1,56 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include + +class AbstractViewer; +class RecentFiles; +class ViewerFactory; + +QT_BEGIN_NAMESPACE +namespace Ui { +class MainWindow; +} +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +public slots: + bool openFile(const QString &fileName); + +private slots: + void onActionOpenTriggered(); + void onActionAboutTriggered(); + void onActionAboutQtTriggered(); + +private: + void readSettings(); + void saveSettings() const; + void restoreViewerSettings(); + void resetViewer() const; + void saveViewerSettings() const; + + QDir m_currentDir; + AbstractViewer *m_viewer = nullptr; + std::unique_ptr ui; + std::unique_ptr m_recentFiles; + std::unique_ptr m_factory; + + static constexpr QLatin1StringView settingsDir = QLatin1StringView("WorkingDir"); + static constexpr QLatin1StringView settingsMainWindow = QLatin1StringView("MainWindow"); + static constexpr QLatin1StringView settingsViewers = QLatin1StringView("Viewers"); + static constexpr QLatin1StringView settingsFiles = QLatin1StringView("RecentFiles"); +}; + +#endif // MAINWINDOW_H diff --git a/app/mainwindow.ui b/app/mainwindow.ui new file mode 100644 index 0000000..d19c246 --- /dev/null +++ b/app/mainwindow.ui @@ -0,0 +1,272 @@ + + + MainWindow + + + + 0 + 0 + 983 + 602 + + + + Document Viewer Demo + + + + :/demos/documentviewer/images/qt-logo.png:/demos/documentviewer/images/qt-logo.png + + + + true + + + + + + + + + Qt::Horizontal + + + + QTabWidget::West + + + 0 + + + + Pages + + + + + Bookmarks + + + + + + + 0 + 0 + + + + + 800 + 0 + + + + true + + + + + 0 + 0 + 798 + 479 + + + + + + + + + + + + + + + 0 + 0 + 983 + 23 + + + + + File + + + + + + + + + Help + + + + + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + + + + :/demos/documentviewer/images/open.png:/demos/documentviewer/images/open.png + + + Open + + + Ctrl+O + + + + + + :/demos/documentviewer/images/help-about.svgz:/demos/documentviewer/images/help-about.svgz + + + about documentviewer + + + Show information about the Document Viewer deomo. + + + Ctrl+H + + + + + + :/demos/documentviewer/images/go-next.svgz:/demos/documentviewer/images/go-next.svgz + + + actionForward + + + One step forward + + + Right + + + + + + :/demos/documentviewer/images/go-previous.svgz:/demos/documentviewer/images/go-previous.svgz + + + actionBack + + + One step back + + + Left + + + + + false + + + + :/demos/documentviewer/images/print2x.png:/demos/documentviewer/images/print2x.png + + + Print + + + Print current file + + + Ctrl+P + + + + + + :/demos/documentviewer/images/qt-logo.png + :/demos/documentviewer/images/qt-logo.png:/demos/documentviewer/images/qt-logo.png + + + About Qt + + + Show Qt license information + + + Ctrl+I + + + + + + :/demos/documentviewer/images/document-open-recent.svgz:/demos/documentviewer/images/document-open-recent.svgz + + + Recently opened... + + + Meta+R + + + + + + .. + + + Quit + + + Quit the application + + + Ctrl+Q + + + + + + + + + actionQuit + triggered() + MainWindow + close() + + + -1 + -1 + + + 491 + 300 + + + + + diff --git a/app/recentfilemenu.cpp b/app/recentfilemenu.cpp new file mode 100644 index 0000000..5b11218 --- /dev/null +++ b/app/recentfilemenu.cpp @@ -0,0 +1,36 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "recentfilemenu.h" +#include "recentfiles.h" + +#include + +RecentFileMenu::RecentFileMenu(QWidget *parent, RecentFiles *recent) : + QMenu(parent), m_recentFiles(recent) +{ + Q_ASSERT(recent); + connect(m_recentFiles, &RecentFiles::changed, this, &RecentFileMenu::updateList); + connect(m_recentFiles, &QObject::destroyed, this, &QObject::deleteLater); + updateList(); +} + +void RecentFileMenu::updateList() +{ + QList acts = actions(); + qDeleteAll(acts); + + if (m_recentFiles->isEmpty()) { + addAction(tr("")); + return; + } + + for (const QString &fileName : m_recentFiles->recentFiles()) { + QAction *action = addAction(fileName); + connect(action, &QAction::triggered, this, [&](){ + QAction *action = qobject_cast(sender()); + Q_ASSERT(action); + emit fileOpened(action->text()); + }); + } +} diff --git a/app/recentfilemenu.h b/app/recentfilemenu.h new file mode 100644 index 0000000..d076f63 --- /dev/null +++ b/app/recentfilemenu.h @@ -0,0 +1,28 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef RECENTFILEMENU_H +#define RECENTFILEMENU_H + +#include + +class RecentFiles; + +class RecentFileMenu : public QMenu +{ + Q_OBJECT + +public: + explicit RecentFileMenu(QWidget *parent, RecentFiles *recent); + +signals: + void fileOpened(const QString &fileName); + +private slots: + void updateList(); + +private: + RecentFiles *m_recentFiles; +}; + +#endif // RECENTFILEMENU_H diff --git a/app/recentfiles.cpp b/app/recentfiles.cpp new file mode 100644 index 0000000..250c8f3 --- /dev/null +++ b/app/recentfiles.cpp @@ -0,0 +1,128 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "recentfiles.h" +#include +#include + +// Test if file exists and can be opened +static bool testFileAccess(const QString &fileName) +{ + return QFileInfo(fileName).isReadable(); +} + +void RecentFiles::clear() +{ + if (isEmpty()) + return; + + m_files.clear(); + emit countChanged(0); +} + +void RecentFiles::addFile(const QString &fileName, EmitPolicy policy) +{ + if (!testFileAccess(fileName)) + return; + + // Remember size, as cleanup can result in a change without size change + const qsizetype c = m_files.count(); + + // Clean dangling and duplicate files + for (qsizetype i = 0; i < m_files.size(); ) { + const QString &file = m_files.at(i); + if (!testFileAccess(file)) { + removeFile(file, RemoveReason::Other); + } else if (file == fileName) { + removeFile(file, RemoveReason::Duplicate); + } else { + ++i; + } + } + + // Cut tail + while (m_files.count() > m_maxFiles) + removeFile((m_files.count() - 1), RemoveReason::Other); + + m_files.prepend(fileName); + + switch (policy) { + case EmitPolicy::NeverEmit: + return; + + case EmitPolicy::EmitWhenChanged: + emit changed(); + + if (c != m_files.count()) + emit countChanged(m_files.count()); + + return; + } +} + +void RecentFiles::addFiles(const QStringList &files) +{ + if (files.isEmpty()) + return; + + if (files.count() == 1) { + addFile(files.at(0)); + return; + } + + const qsizetype c = m_files.count(); + + for (const auto &file : files) + addFile(file, EmitPolicy::NeverEmit); + + emit changed(); + if (m_files.count() != c) + emit countChanged(m_files.count()); +} + +void RecentFiles::removeFile(qsizetype index, RemoveReason reason) +{ + if (index < 0 || index >= m_files.count()) + return; + + m_files.remove(index); + + // No emit for duplicate removal, add emits changed later. + if (reason != RemoveReason::Duplicate) + emit changed(); +} + +void RecentFiles::saveSettings(QSettings &settings, const QString &key) const +{ + settings.beginGroup(key); + settings.setValue(s_maxFiles, maxFiles()); + if (!isEmpty()) { + settings.beginWriteArray(s_fileNames, m_files.count()); + for (int index = 0; index < m_files.count(); ++index) { + settings.setArrayIndex(index); + settings.setValue(s_file, m_files.at(index)); + } + settings.endArray(); + } + settings.endGroup(); +} + +bool RecentFiles::restoreFromSettings(QSettings &settings, const QString &key) +{ + settings.beginGroup(key); + const qsizetype mxFiles = settings.value(s_maxFiles, maxFiles()).toLongLong(); + setMaxFiles(mxFiles); + m_files.clear(); // clear list without emitting + const int numberFiles = settings.beginReadArray(s_fileNames); + for (int index = 0; index < numberFiles; ++index) { + settings.setArrayIndex(index); + const QString absoluteFilePath = settings.value(s_file).toString(); + addFile(absoluteFilePath, EmitPolicy::NeverEmit); + } + settings.endArray(); + settings.endGroup(); + if (!m_files.isEmpty()) + emit changed(); + + return true; +} diff --git a/app/recentfiles.h b/app/recentfiles.h new file mode 100644 index 0000000..3eee5a6 --- /dev/null +++ b/app/recentfiles.h @@ -0,0 +1,71 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef RECENTFILES_H +#define RECENTFILES_H + +#include +#include + +QT_BEGIN_NAMESPACE +class QSettings; +QT_END_NAMESPACE + +class RecentFiles : public QObject +{ + Q_OBJECT +public: + enum class RemoveReason { + Other, + Duplicate + }; + Q_ENUM(RemoveReason) + + using QObject::QObject; + + // Access to QStringList member functions + const QStringList recentFiles() const {return m_files; } + bool isEmpty() const { return m_files.isEmpty(); } + + // Properties + qsizetype maxFiles() const { return m_maxFiles; } + void setMaxFiles(qsizetype maxFiles) { m_maxFiles = maxFiles; } + +public slots: + void addFile(const QString &fileName) { addFile(fileName, EmitPolicy::EmitWhenChanged); } + void addFiles(const QStringList &fileNames); + void removeFile(const QString &fileName) { removeFile(m_files.indexOf(fileName)); } + void removeFile(qsizetype index) {removeFile(index, RemoveReason::Other); } + void saveSettings(QSettings &settings, const QString &key) const; + bool restoreFromSettings(QSettings &settings, const QString &key); + void clear(); + +signals: + void countChanged(int count); + void changed(); + +private: + // Private removers with reason + void removeFile(qsizetype index, RemoveReason reason); + void removeFile(const QString &fileName, RemoveReason reason) {removeFile(m_files.indexOf(fileName), reason); } + + // Private adder with emit policy + enum class EmitPolicy { + EmitWhenChanged, + NeverEmit + }; + + void addFile(const QString &fileName, EmitPolicy policy); + + qsizetype m_maxFiles = 10; + + QStringList m_files; + + // Constexprs for settings + static constexpr QLatin1StringView s_maxFiles = QLatin1StringView("maxFiles"); + static constexpr QLatin1StringView s_openMode = QLatin1StringView("openMode"); + static constexpr QLatin1StringView s_fileNames = QLatin1StringView("fileNames"); + static constexpr QLatin1StringView s_file = QLatin1StringView("file"); +}; + +#endif // RECENTFILES_H diff --git a/app/viewerfactory.cpp b/app/viewerfactory.cpp new file mode 100644 index 0000000..2f20da8 --- /dev/null +++ b/app/viewerfactory.cpp @@ -0,0 +1,189 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "abstractviewer.h" +#include "viewerfactory.h" +#include "viewerinterfaces.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace Qt::StringLiterals; + +ViewerFactory::ViewerFactory(QWidget *displayWidget, QMainWindow *mainWindow, DefaultPolicy policy) + : m_defaultPolicy(policy), + m_displayWidget(displayWidget), + m_mainWindow(mainWindow) +{ + Q_ASSERT(m_displayWidget); + Q_ASSERT(m_mainWindow); + loadViewerPlugins(); +} + +ViewerFactory::~ViewerFactory() +{ + unload(); +} + +AbstractViewer *ViewerFactory::viewer(QFile *file) const +{ + Q_ASSERT(file); + + const QFileInfo info(*file); + QMimeDatabase db; + const auto mimeType = db.mimeTypeForFile(info); + + AbstractViewer *viewer = ViewerFactory::viewer(mimeType); + if (!viewer) { + qWarning() << "Mime type" << mimeType.name() << "not supported."; + return nullptr; + } + + viewer->init(file, m_displayWidget, m_mainWindow); + return viewer; +} + +void ViewerFactory::loadViewerPlugins() +{ + if (!m_viewers.isEmpty()) + return; + + // Load static plugins + const QObjectList &staticPlugins = QPluginLoader::staticInstances(); + for (auto *plugin : staticPlugins) + addViewer(plugin); + + // Load shared plugins + QDir pluginsDir = QDir(QApplication::applicationDirPath()); + +#if defined(Q_OS_WINDOWS) + pluginsDir.cd("app"_L1); +#elif defined(Q_OS_DARWIN) + if (pluginsDir.dirName() == "MacOS"_L1) { + pluginsDir.cdUp(); + pluginsDir.cdUp(); + pluginsDir.cdUp(); + } +#endif + const auto entryList = pluginsDir.entryList(QDir::Files); + for (const QString &fileName : entryList) { + QPluginLoader loader(pluginsDir.absoluteFilePath(fileName)); + QObject *plugin = loader.instance(); + if (plugin) + addViewer(plugin); +#if 0 + else + qDebug() << loader.errorString(); +#endif + } +} + +void ViewerFactory::unload() +{ + qDeleteAll(viewers()); + m_viewers.clear(); +} + +void ViewerFactory::addViewer(QObject *viewerObject) +{ + auto *interface = qobject_cast(viewerObject); + if (!interface) + return; + + // Set custom default viewer + if (interface->viewer()->isDefaultViewer()) + m_defaultViewer = interface->viewer(); + + m_viewers.insert(interface->viewerName(), interface->viewer()); +} + +QStringList ViewerFactory::viewerNames(bool showDefault) const +{ + if (!showDefault) + return m_viewers.keys(); + + QStringList list; + for (auto it = m_viewers.constBegin(); it != m_viewers.constEnd(); ++it) { + QString name = it.key(); + if ((m_defaultViewer && it.value()->isDefaultViewer()) + || (!m_defaultViewer && it.key() == "TxtViewer"_L1)) { + name += "(default)"_L1; + } + list.append(name); + } + return list; +} + +ViewerFactory::ViewerList ViewerFactory::viewers() const +{ + return m_viewers.values(); +} + +AbstractViewer *ViewerFactory::findViewer(const QString &viewerName) const +{ + const ViewerList &viewerList = viewers(); + for (auto *viewer : viewerList) { + if (viewer->viewerName() == viewerName) + return viewer; + } + qWarning() << "Plugin" << viewerName << "not loaded."; + return nullptr; +} + +AbstractViewer *ViewerFactory::viewer(const QMimeType &mimeType) const +{ + const ViewerList &viewerList = viewers(); + + for (AbstractViewer *viewer : viewerList) { + for (const QString &type : viewer->supportedMimeTypes()) { + if (mimeType.inherits(type)) + return viewer; + } + } + + AbstractViewer *viewer = defaultViewer(); + + if (m_defaultWarning) { + QMessageBox mbox; + mbox.setIcon(QMessageBox::Warning); + mbox.setText(QObject::tr("Mime type %1 not supported. Falling back to %2.") + .arg(mimeType.name(), viewer->viewerName())); + mbox.setStandardButtons(QMessageBox::Ok); + QTimer::singleShot(8000, &mbox, [&mbox](){ mbox.close(); }); + mbox.exec(); + } + + return viewer; +} + +AbstractViewer *ViewerFactory::defaultViewer() const +{ + switch (m_defaultPolicy) { + case DefaultPolicy::NeverDefault: + return nullptr; + case DefaultPolicy::DefaultToCustomViewer: + return m_defaultViewer ? m_defaultViewer : findViewer("TxtViewer"_L1); + case DefaultPolicy::DefaultToTxtViewer: + return findViewer("TxtViewer"_L1); + } + Q_UNREACHABLE(); +} + +QStringList ViewerFactory::supportedMimeTypes() const +{ + static QStringList mimeTypes; + if (!mimeTypes.isEmpty()) + return mimeTypes; + + const ViewerList &viewerList = viewers(); + for (auto viewer : viewerList) + mimeTypes.append(viewer->supportedMimeTypes()); + return mimeTypes; +} diff --git a/app/viewerfactory.h b/app/viewerfactory.h new file mode 100644 index 0000000..184ce6d --- /dev/null +++ b/app/viewerfactory.h @@ -0,0 +1,62 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef VIEWERFACTORY_H +#define VIEWERFACTORY_H + +#include +#include + +QT_BEGIN_NAMESPACE +class QMimeType; +class QWidget; +class QMainWindow; +class QFile; +QT_END_NAMESPACE + +class AbstractViewer; + +class ViewerFactory +{ +public: + enum class DefaultPolicy { + NeverDefault, + DefaultToTxtViewer, + DefaultToCustomViewer + }; + + explicit ViewerFactory(QWidget *displayWidget, QMainWindow *mainWindow, + DefaultPolicy policy = DefaultPolicy::DefaultToTxtViewer); + ~ViewerFactory(); + + DefaultPolicy defaultPolicy() const { return m_defaultPolicy; } + void setDefaultPolicy(DefaultPolicy policy) { m_defaultPolicy = policy; } + bool defaultWarning() const { return m_defaultWarning; } + void setDefaultWarning(bool on) { m_defaultWarning = on; } + + AbstractViewer *viewer(QFile *file) const; + + + using ViewerMap = QMap; + using ViewerList = QList; + QStringList viewerNames(bool showDefault = false) const; + ViewerList viewers() const; + AbstractViewer *findViewer(const QString &viewerName) const; + AbstractViewer *defaultViewer() const; + QStringList supportedMimeTypes() const; + +private: + DefaultPolicy m_defaultPolicy; + QWidget *m_displayWidget; + QMainWindow *m_mainWindow; + ViewerMap m_viewers; + AbstractViewer *m_defaultViewer = nullptr; + bool m_defaultWarning = true; + + void loadViewerPlugins(); + void addViewer(QObject *viewerObject); + AbstractViewer *viewer(const QMimeType &type) const; + void unload(); +}; + +#endif // VIEWERFACTORY_H diff --git a/app/viewerinterfaces.h b/app/viewerinterfaces.h new file mode 100644 index 0000000..34991f8 --- /dev/null +++ b/app/viewerinterfaces.h @@ -0,0 +1,35 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef VIEWERINTERFACES_H +#define VIEWERINTERFACES_H + +#include "abstractviewer.h" + +#include + +QT_BEGIN_NAMESPACE +class QPrinter; +QT_END_NAMESPACE + +class ViewerInterface : public AbstractViewer +{ +public: + virtual ~ViewerInterface() = default; + virtual QString viewerName() const override = 0; + virtual QByteArray saveState() const override = 0; + virtual bool restoreState(QByteArray &) override = 0; + virtual bool supportsOverview() const override = 0; + virtual bool hasContent() const override = 0; +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT + virtual void printDocument(QPrinter *) const override = 0; + virtual QStringList supportedMimeTypes() const override = 0; +#endif +}; + +QT_BEGIN_NAMESPACE +#define ViewerInterface_iid "org.qt-project.Qt.Examples.DocumentViewer.ViewerInterface/1.0" +Q_DECLARE_INTERFACE(ViewerInterface, ViewerInterface_iid) +QT_END_NAMESPACE + +#endif //VIEWERINTERFACES_H diff --git a/compile_commands.json b/compile_commands.json new file mode 100644 index 0000000..d50c2e3 --- /dev/null +++ b/compile_commands.json @@ -0,0 +1,67 @@ +[ +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/app", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -I/home/talksik/code/qt-tinkering/documentviewer/build/app/abstractviewer_autogen/include -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -o CMakeFiles/abstractviewer.dir/abstractviewer_autogen/mocs_compilation.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/build/app/abstractviewer_autogen/mocs_compilation.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/build/app/abstractviewer_autogen/mocs_compilation.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/app", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -I/home/talksik/code/qt-tinkering/documentviewer/build/app/abstractviewer_autogen/include -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -o CMakeFiles/abstractviewer.dir/abstractviewer.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/app/abstractviewer.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/app/abstractviewer.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/app", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -I/home/talksik/code/qt-tinkering/documentviewer/build/app/documentviewer_autogen/include -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -o CMakeFiles/documentviewer.dir/documentviewer_autogen/mocs_compilation.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/build/app/documentviewer_autogen/mocs_compilation.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/build/app/documentviewer_autogen/mocs_compilation.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/app", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -I/home/talksik/code/qt-tinkering/documentviewer/build/app/documentviewer_autogen/include -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -o CMakeFiles/documentviewer.dir/main.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/app/main.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/app/main.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/app", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -I/home/talksik/code/qt-tinkering/documentviewer/build/app/documentviewer_autogen/include -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -o CMakeFiles/documentviewer.dir/mainwindow.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/app/mainwindow.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/app/mainwindow.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/app", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -I/home/talksik/code/qt-tinkering/documentviewer/build/app/documentviewer_autogen/include -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -o CMakeFiles/documentviewer.dir/viewerfactory.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/app/viewerfactory.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/app/viewerfactory.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/app", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -I/home/talksik/code/qt-tinkering/documentviewer/build/app/documentviewer_autogen/include -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -o CMakeFiles/documentviewer.dir/recentfiles.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/app/recentfiles.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/app/recentfiles.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/app", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -I/home/talksik/code/qt-tinkering/documentviewer/build/app/documentviewer_autogen/include -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -o CMakeFiles/documentviewer.dir/recentfilemenu.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/app/recentfilemenu.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/app/recentfilemenu.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/app", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -I/home/talksik/code/qt-tinkering/documentviewer/build/app/documentviewer_autogen/include -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -o CMakeFiles/documentviewer.dir/documentviewer_autogen/EWIEGA46WW/qrc_documentviewer.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/build/app/documentviewer_autogen/EWIEGA46WW/qrc_documentviewer.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/build/app/documentviewer_autogen/EWIEGA46WW/qrc_documentviewer.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/plugins/jsonviewer", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DEPRECATED_WARNINGS -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PLUGIN -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -Djsonviewer_EXPORTS -I/home/talksik/code/qt-tinkering/documentviewer/build/plugins/jsonviewer/jsonviewer_autogen/include -I/home/talksik/code/qt-tinkering/documentviewer/plugins/jsonviewer/../../app -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -fPIC -o CMakeFiles/jsonviewer.dir/jsonviewer_autogen/mocs_compilation.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/build/plugins/jsonviewer/jsonviewer_autogen/mocs_compilation.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/build/plugins/jsonviewer/jsonviewer_autogen/mocs_compilation.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/plugins/jsonviewer", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DEPRECATED_WARNINGS -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PLUGIN -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -Djsonviewer_EXPORTS -I/home/talksik/code/qt-tinkering/documentviewer/build/plugins/jsonviewer/jsonviewer_autogen/include -I/home/talksik/code/qt-tinkering/documentviewer/plugins/jsonviewer/../../app -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -fPIC -o CMakeFiles/jsonviewer.dir/jsonviewer.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/plugins/jsonviewer/jsonviewer.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/plugins/jsonviewer/jsonviewer.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/plugins/txtviewer", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DEPRECATED_WARNINGS -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PLUGIN -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -Dtxtviewer_EXPORTS -I/home/talksik/code/qt-tinkering/documentviewer/build/plugins/txtviewer/txtviewer_autogen/include -I/home/talksik/code/qt-tinkering/documentviewer/plugins/txtviewer/../../app -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -fPIC -o CMakeFiles/txtviewer.dir/txtviewer_autogen/mocs_compilation.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/build/plugins/txtviewer/txtviewer_autogen/mocs_compilation.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/build/plugins/txtviewer/txtviewer_autogen/mocs_compilation.cpp" +}, +{ + "directory": "/home/talksik/code/qt-tinkering/documentviewer/build/plugins/txtviewer", + "command": "/usr/bin/c++ -DQT_CORE_LIB -DQT_DEPRECATED_WARNINGS -DQT_DOCUMENTVIEWER_PRINTSUPPORT -DQT_GUI_LIB -DQT_NO_CAST_FROM_ASCII -DQT_PLUGIN -DQT_PRINTSUPPORT_LIB -DQT_WIDGETS_LIB -Dtxtviewer_EXPORTS -I/home/talksik/code/qt-tinkering/documentviewer/build/plugins/txtviewer/txtviewer_autogen/include -I/home/talksik/code/qt-tinkering/documentviewer/plugins/txtviewer/../../app -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtCore -isystem /home/talksik/Qt/6.6.1/gcc_64/include -isystem /home/talksik/Qt/6.6.1/gcc_64/mkspecs/linux-g++ -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtGui -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtWidgets -isystem /home/talksik/Qt/6.6.1/gcc_64/include/QtPrintSupport -g -fPIC -fPIC -o CMakeFiles/txtviewer.dir/txtviewer.cpp.o -c /home/talksik/code/qt-tinkering/documentviewer/plugins/txtviewer/txtviewer.cpp", + "file": "/home/talksik/code/qt-tinkering/documentviewer/plugins/txtviewer/txtviewer.cpp" +} +] \ No newline at end of file diff --git a/doc/images/documentviewer_open.png b/doc/images/documentviewer_open.png new file mode 100644 index 0000000..8a05c2f Binary files /dev/null and b/doc/images/documentviewer_open.png differ diff --git a/doc/images/txtviewer_screenshot.png b/doc/images/txtviewer_screenshot.png new file mode 100644 index 0000000..8bc4761 Binary files /dev/null and b/doc/images/txtviewer_screenshot.png differ diff --git a/doc/src/documentviewer.qdoc b/doc/src/documentviewer.qdoc new file mode 100644 index 0000000..b1d729f --- /dev/null +++ b/doc/src/documentviewer.qdoc @@ -0,0 +1,288 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \title Document Viewer + \ingroup examples-widgets + \example demos/documentviewer + \examplecategory {Application Examples} + \meta {tag} {demo,widgets,mainwindow} + \brief A Widgets application to display and print JSON, text, and PDF files. + + + \e{Document Viewer} demonstrates how to use a QMainWindow with static + and dynamic toolbars, menus, and actions. Additionally, it demonstrates + the following features in widget-based applications: + + \list + \li Using QSettings to query and save user preferences, + and managing previously opened file history. + \li Controlling cursor behavior when hovering over widgets. + \li Creating dynamically loaded plugins. + \endlist + + \image documentviewer_open.png + + + \section1 Creating an application and the main window + + The application and its main window is constructed in \c main.cpp. + The main() function uses QCommandLineParser to process command line + arguments -- \e help, \e version, and an optional positional + argument, \e file. If the user provided a path to a file when + launching the application, the main window opens it: + + \quotefromfile demos/documentviewer/app/main.cpp + \skipto int main + \printuntil exec + \printline } + + + \section1 MainWindow class + + The \c MainWindow class provides an application screen with menus, + actions, and a toolbar. It can open a file, automatically detecting its + content type. It also maintains a list of previously opened files, using + QSettings to store and reload settings when launched. The MainWindow + creates a suitable \e viewer for the opened file, based on its content type, + and provides support for printing a document. + + MainWindow's constructor initializes the user interface created in Qt + Designer. The \c mainwindow.ui file provides a QTabWidget on the left, + showing bookmarks and thumbnails. On the right, there is a QScrollArea for + viewing file content. + + + \section1 ViewerFactory class + + The \c ViewerFactory class manages viewers for known file types. These viewers + are implemented as plugins. When an instance of a ViewerFactory is created, + pointers to the view area and the main window are passed to the constructor: + + \code + m_factory.reset(new ViewerFactory(ui->viewArea, this)); + \endcode + + ViewerFactory loads all available plugins on construction. It provides + a public API to query the loaded plugins, their names, and supported MIME + types: + + \quotefromfile demos/documentviewer/app/viewerfactory.h + \skipto ViewerList + \printuntil QStringList supportedMimeTypes() const; + + The \c viewer() function returns a pointer to the plugin suitable to open + the QFile passed as an argument: + + \code + m_viewer = m_factory->viewer(file); + \endcode + + If the application settings contain a section for the viewer, it's passed + to the viewer's virtual \c restoreState() function: + + \quotefromfile demos/documentviewer/app/mainwindow.cpp + \skipto MainWindow::restoreViewerSettings + \printuntil restoreState + \printline } + + Then, the standard UI assets are passed to the viewer and the main scroll + area is set to show the viewer's display widget: + + \quotefromfile demos/documentviewer/app/mainwindow.cpp + \skipuntil bool MainWindow::openFile + \skipto m_viewer->initViewer + \printuntil } + + + \section1 AbstractViewer class + + \c AbstractViewer provides a generalized API to view, save, and print a + document. Properties of both the document and the viewer can be queried: + + \list + \li Does the document have content? + \li Has it been modified? + \li Is an overview (thumbnails or bookmarks) supported? + \endlist + + AbstractViewer provides protected methods for derived classes 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 is + responsible for removing and destroying the UI assets it creates. It + inherits from QObject to implement signals and slots. + + \section2 Signals + + \c {void uiInitialized();} + + This signal is emitted after a viewer receives all necessary information + about UI assets on the main window. + + \c {void printingEnabledChanged(bool enabled);} + + This signal is emitted when document printing is either enabled or + disabled. This happens after a new document was successfully loaded, + or, for example, all content was removed. + + \c {void printStatusChanged(AbstractViewer::PrintStatus status);} + + After starting the printing process, this signal notifies about changes in + its progress. + + \c {void documentLoaded(const QString &fileName);} + + This signal notifies the application that a document was successfully + loaded. + + + \section1 TxtViewer class + + \c TxtViewer is a simple text viewer, inheriting from AbstractViewer. + It supports editing text files, copy/cut and paste, printing, and + saving changes. + + + \section1 JsonViewer class + + \c JsonViewer displays a JSON file in a QTreeView. Internally, it loads + the contents of a file into a QJsonDocument and uses it to populate a + custom tree model with \c JsonItemModel. + + The JSON viewer plugin demonstrates how to implement a custom item model + inherited from QAbstractItemModel. The \c JsonTreeItem class provides a + basic API for manipulating JSON data and propagating it back to the + underlying QJsonDocument. + + JsonViewer uses the top-level objects of the document as bookmarks for + navigation. Other nodes (keys and 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. + + + \section1 PdfViewer class + + The \c PdfViewer class (and plugin) is a fork of the \l {PDF Viewer + Widget Example}. It demonstrates the use of QScroller to smoothly + flick through a document. + + + \section1 Other relevant classes + + \section2 HoverWatcher class + + The \c HoverWatcher class sets an override cursor when hovering the + mouse over a widget, restoring it upon departure. To prevent multiple + HoverWatcher instances being created for the same widget, it is + implemented as a singleton per widget. + + HoverWatcher inherits from QObject and takes the QWidget it watches + as the instance's parent. It installs an event filter to intercept hover + events without consuming them: + + \quotefromfile demos/documentviewer/plugins/pdfviewer/hoverwatcher.cpp + \skipto HoverWatcher::HoverWatcher + \printuntil } + + The \c HoverAction enum lists the actions that HoverWatcher reacts to: + + \quotefromfile demos/documentviewer/plugins/pdfviewer/hoverwatcher.h + \skipto enum HoverAction + \printuntil }; + + Static functions create watchers, check their existence for a specific + QWidget, or dismiss a watcher: + + \quotefromfile demos/documentviewer/plugins/pdfviewer/hoverwatcher.h + \skipto static HoverWatcher + \printuntil static void dismiss + + A cursor shape can be set or unset for each HoverAction. If there is + no associated cursor shape, the application's override cursor is + restored when the action is triggered. + + \quotefromfile demos/documentviewer/plugins/pdfviewer/hoverwatcher.h + \skipto public slots + \printuntil void unSetCursorShape + + The \c mouseButtons property holds the mouse buttons to consider for a + \c MousePress action: + + \quotefromfile demos/documentviewer/plugins/pdfviewer/hoverwatcher.h + \skipuntil public slots + \skipto setMouseButtons + \printuntil setMouseButton( + + Action-specific signals are emitted after processing an action: + + \quotefromfile demos/documentviewer/plugins/pdfviewer/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 + + \section2 RecentFiles class + + \c RecentFiles is a QStringList that is specialized to manage a list of + recently opened files. + + RecentFiles has slots to add either a single file or multiple files in one + go. An entry is added to the list of recent files if the path points to a + file that exists and can be opened. If a file is already in the list, it + is removed from its original position and added to the top. + + \quotefromfile demos/documentviewer/app/recentfiles.h + \skipto public slots + \printuntil addFiles + + Files are removed from the list either by name or by index: + + \quotefromfile demos/documentviewer/app/recentfiles.h + \skipuntil public slots + \skipto removeFile + \printuntil qsizetype index + + Slots that implement saving and restoring from QSettings: + + \quotefromfile demos/documentviewer/app/recentfiles.h + \skipuntil public slots + \skipto saveSettings + \printuntil restoreFromSettings + + When restoring settings, nonexistent files are ignored. The \c maxFiles + property holds the maximum amount of recent files to store (default is + 10). + + \code + qsizetype maxFiles(); + void setMaxFiles(qsizetype maxFiles); + \endcode + + \c {RecentFiles} verifies that a file can be read before + accepting it. + + \section2 RecentFileMenu class + + \c {RecentFileMenu} is a QMenu, specialized to display a + \l{RecentFiles class}{RecentFiles} object as a submenu. + + Its constructor takes a pointer to a parent QObject and a pointer to a + RecentFiles object, the content of which it will visualize. + Its \c fileOpened() signal, triggered when the user selects a recent file + from the list, passes the absolute path to the file as an argument. + + \note \c {RecentFileMenu} is destroyed either by its parent widget, or by the + \c {RecentFiles} object passed to its constructor. + + \quotefromfile demos/documentviewer/app/recentfilemenu.h + \skipto class RecentFileMenu + \printuntil void fileOpened + \dots + \skipuntil RecentFiles + \printline } +*/ diff --git a/doc/src/txtviewer.qdoc b/doc/src/txtviewer.qdoc new file mode 100644 index 0000000..fc9447f --- /dev/null +++ b/doc/src/txtviewer.qdoc @@ -0,0 +1,111 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example demos/documentviewer/plugins/txtviewer + \title Qt Widgets - Text Viewer Plugin Example + \examplecategory {Graphics & Multimedia} + \ingroup examples-mainwindow + + \brief A widget example with menus, toolbars and a status bar. + + The Text Viewer example is a text editor built around QPlainTextEdit, + in the form of a plugin for a general-purpose \l {Document Viewer}. + + \image txtviewer_screenshot.png + + All of the code for the Text Viewer example is in the \c TxtViewer class, + which inherits \c AbstractViewer. \c AbstractViewer provides the framework + for interaction between a viewer and the main window. The application provides + \uicontrol{File}, \uicontrol{Edit}, and \uicontrol{Help} entries in the menu + bar. + + The status bar at the bottom of the main window shows a message provided by + the application to its user. + + Recently opened files are shown in the \uicontrol{File} menu. + The example can only load one file at a time. + + \section1 Class Definition + + \snippet demos/documentviewer/plugins/txtviewer/txtviewer.h interfacing + + The class definition starts with the \c Q_OBJECT macro, which handles + signals and slots. It is followed by the \c Q_PLUGIN_METADATA and + \c Q_INTERFACES macros which are necessary to register the plugin. + + The class inherits from \c ViewerInterface, which inherits from + \c AbstractViewer. The \c ViewerInterface class is used to provide an interface + between the main window application and the plugin. + + \c QPluginLoader also requires the file txtviewer.json, which has to contain + the plugin's key: + \code + { "Keys": [ "txtviewer" ] } + \endcode + + \snippet demos/documentviewer/plugins/txtviewer/txtviewer.h classDefinition + + The class defines no constructor, which means that only a standard constructor + without arguments is available. All other functions, including the destructor, + re-implement virtual functions of \c ViewerInterface. They are used to exchange + data, information, and instructions with the main application. + + No functionality is implemented to save and restore settings. + The \c supportsOverview function always returns \c false, which tells the main + application that no window for thumbnail navigation has to be displayed. + + \section1 TxtViewer Class Implementation + + \snippet demos/documentviewer/plugins/txtviewer/txtviewer.cpp init + + We start by including the header files necessary to access all classes used by + \c TxtViewer. We also include \c txtviewer.h. + + \c QPrinter and \c QPrintDialog are only included if print support is enabled + on the compilation system. + + You might wonder why we don't include these headers in \c mainwindow.h and + be done with it. The reason is that including multiple large headers from another + header file can rapidly degrade performance. Here, it wouldn't do any harm, + but it's still generally a good idea to include only the header files that are + strictly necessary from another header file. + + The implementation starts with an empty destructor. It could be completely omitted. + It's good practice to implement it empty in order to point out to code readers that + nothing needs to be done in the destructor. + + The destructor is followed by an initialization function, taking three arguments: + \list + \liĀ \c file, the pointer to the file to be opened and displayed. + \li \c parent, pointing to the \c QWidget inside which the editor shall be placed. + \li \c mainWindow, pointing to the application's main window, where menus and menu bars + are handled. + \endlist + The function calls the base init function of \c AbstractViwer. + A new QPlainTextEdit widget is created, which will display the file's contents. + Then, \c TxtViewer's setup function is connected to the base class' uiInitialized signal. + + The next function returns the list of mime types, which the text viewer supports. + Only plain text is supported. + + The last initialization function adds viewer specific UI components like menus, + icons, buttons, and tooltips. It uses functionality provided by \c AbstractViewer + to make sure that these components are removed from the application's main window, + once another file is displayed with another viewer plugin. + + \snippet demos/documentviewer/plugins/txtviewer/txtviewer.cpp open + + \c openFile opens a file, transfers its contents into the QPlainTextEdit, and prints + a status message for the user, depending on whether or not the opening was successful. + + \snippet demos/documentviewer/plugins/txtviewer/txtviewer.cpp infoPrintAndSave + + The next re-implemented function tells the main application whether or not the viewer + plugin is actually displaying content. + + If printing is supported on the compiling system, the next section implements it. + + The last two re-implementations provide functionality to save the current file or + to save it under a new name. +*/ diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..2244e68 --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +add_subdirectory(jsonviewer) +add_subdirectory(txtviewer) + +if(TARGET Qt6::PdfWidgets) + add_subdirectory(pdfviewer) +endif() diff --git a/plugins/jsonviewer/CMakeLists.txt b/plugins/jsonviewer/CMakeLists.txt new file mode 100644 index 0000000..5a2071d --- /dev/null +++ b/plugins/jsonviewer/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_add_plugin(jsonviewer + CLASS_NAME JsonViewer + jsonviewer.cpp jsonviewer.h +) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets + OPTIONAL_COMPONENTS PrintSupport) + +set_target_properties(jsonviewer PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/app" +) + +target_include_directories(jsonviewer PRIVATE + ../../app +) + +target_link_libraries(jsonviewer PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + abstractviewer +) + +if(TARGET Qt6::PrintSupport) + target_link_libraries(jsonviewer PRIVATE Qt6::PrintSupport) +endif() + +install(TARGETS jsonviewer + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}/plugins" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}/plugins" +) + diff --git a/plugins/jsonviewer/jsonviewer.cpp b/plugins/jsonviewer/jsonviewer.cpp new file mode 100644 index 0000000..e74aa19 --- /dev/null +++ b/plugins/jsonviewer/jsonviewer.cpp @@ -0,0 +1,525 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "jsonviewer.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +#include +#include +#endif + +using namespace Qt::StringLiterals; + +JsonViewer::JsonViewer() +{ + connect(this, &AbstractViewer::uiInitialized, this, &JsonViewer::setupJsonUi); +} + +void JsonViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow) +{ + AbstractViewer::init(file, new QTreeView(parent), mainWindow); + m_tree = qobject_cast(widget()); +} + +JsonViewer::~JsonViewer() +{ + delete m_toplevel; +} + +QStringList JsonViewer::supportedMimeTypes() const +{ + return {"application/json"_L1}; +} + +void JsonViewer::setupJsonUi() +{ + // Build Menus and toolbars + QMenu *menu = addMenu(tr("Json")); + QToolBar *tb = addToolBar(tr("Json Actions")); + + const QIcon zoomInIcon = QIcon::fromTheme("zoom-in"_L1); + QAction *a = menu->addAction(zoomInIcon, tr("&+Expand all"), m_tree, &QTreeView::expandAll); + tb->addAction(a); + a->setPriority(QAction::LowPriority); + a->setShortcut(QKeySequence::New); + + const QIcon zoomOutIcon = QIcon::fromTheme("zoom-out"_L1); + a = menu->addAction(zoomOutIcon, tr("&-Collapse all"), m_tree, &QTreeView::collapseAll); + tb->addAction(a); + a->setPriority(QAction::LowPriority); + a->setShortcut(QKeySequence::New); + + if (!m_searchKey) + m_searchKey = new QLineEdit(tb); + + auto *label = new QLabel(tb); + const QPixmap magnifier = QPixmap(":/icons/images/magnifier.png"_L1).scaled(QSize(28, 28)); + label->setPixmap(magnifier); + tb->addWidget(label); + tb->addWidget(m_searchKey); + connect(m_searchKey, &QLineEdit::textEdited, m_tree, &QTreeView::keyboardSearch); + + openJsonFile(); + + if (m_root.isEmpty()) + return; + + // Populate bookmarks with toplevel + m_uiAssets.tabs->clear(); + m_toplevel = new QListWidget(m_uiAssets.tabs); + m_uiAssets.tabs->addTab(m_toplevel, tr("Bookmarks")); + qRegisterMetaType(); + for (int i = 0; i < m_tree->model()->rowCount(); ++i) { + const auto &index = m_tree->model()->index(i, 0); + m_toplevel->addItem(index.data().toString()); + auto *item = m_toplevel->item(i); + item->setData(Qt::UserRole, index); + item->setToolTip(tr("Toplevel Item %1").arg(i)); + } + m_toplevel->setAcceptDrops(true); + m_tree->setDragEnabled(true); + m_tree->setContextMenuPolicy(Qt::CustomContextMenu); + m_toplevel->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(m_toplevel, &QListWidget::itemClicked, this, &JsonViewer::onTopLevelItemClicked); + connect(m_toplevel, &QListWidget::itemDoubleClicked, this, &JsonViewer::onTopLevelItemDoubleClicked); + connect(m_toplevel, &QListWidget::customContextMenuRequested, this, &JsonViewer::onBookmarkMenuRequested); + connect(m_tree, &QTreeView::customContextMenuRequested, this, &JsonViewer::onJsonMenuRequested); + + // Connect back and forward + connect(m_uiAssets.back, &QAction::triggered, m_tree, [&](){ + const QModelIndex &index = m_tree->indexAbove(m_tree->currentIndex()); + if (index.isValid()) + m_tree->setCurrentIndex(index); + }); + connect(m_uiAssets.forward, &QAction::triggered, m_tree, [&](){ + QModelIndex current = m_tree->currentIndex(); + QModelIndex next = m_tree->indexBelow(current); + if (next.isValid()) { + m_tree->setCurrentIndex(next); + return; + } + + // Expand last item to go beyond + if (!m_tree->isExpanded(current)) { + m_tree->expand(current); + QModelIndex next = m_tree->indexBelow(current); + if (next.isValid()) { + m_tree->setCurrentIndex(next); + } + } + }); +} + +void resizeToContents(QTreeView *tree) +{ + for (int i = 0; i < tree->header()->count(); ++i) + tree->resizeColumnToContents(i); +} + +bool JsonViewer::openJsonFile() +{ + disablePrinting(); + + QJsonParseError err; + m_file->open(QIODevice::ReadOnly); + m_root = QJsonDocument::fromJson(m_file->readAll(), &err); + const QString type = tr("open"); + if (err.error != QJsonParseError::NoError) { + statusMessage(tr("Unable to parse Json document from %1. %2") + .arg(QDir::toNativeSeparators(m_file->fileName()), + err.errorString()), type); + return false; + } + + statusMessage(tr("Json document %1 opened") + .arg(QDir::toNativeSeparators(m_file->fileName())), type); + m_file->close(); + + maybeEnablePrinting(); + + JsonItemModel *model = new JsonItemModel(m_root, this); + m_tree->setModel(model); + + return true; +} + +QModelIndex indexOf(const QListWidgetItem *item) +{ + return qvariant_cast(item->data(Qt::UserRole)); +} + +// Move to the clicked toplevel index +void JsonViewer::onTopLevelItemClicked(QListWidgetItem *item) +{ + // return in the unlikely case that the tree has not been built + if (Q_UNLIKELY(!m_tree->model())) + return; + + auto index = indexOf(item); + if (Q_UNLIKELY(!index.isValid())) + return; + + m_tree->setCurrentIndex(index); +} + +// Toggle double clicked index between collaps/expand +void JsonViewer::onTopLevelItemDoubleClicked(QListWidgetItem *item) +{ + // return in the unlikely case that the tree has not been built + if (Q_UNLIKELY(!m_tree->model())) + return; + + auto index = indexOf(item); + if (Q_UNLIKELY(!index.isValid())) + return; + + if (m_tree->isExpanded(index)) { + m_tree->collapse(index); + return; + } + + // Make sure the node and all parents are expanded + while (index.isValid()) { + m_tree->expand(index); + index = index.parent(); + } +} + +void JsonViewer::onJsonMenuRequested(const QPoint &pos) +{ + const auto &index = m_tree->indexAt(pos); + if (!index.isValid()) + return; + + // Don't show a context menu, if the index is already a bookmark + for (int i = 0; i < m_toplevel->count(); ++i) { + if (indexOf(m_toplevel->item(i)) == index) + return; + } + + QMenu menu(m_tree); + QAction *action = new QAction(tr("Add bookmark")); + action->setData(index); + menu.addAction(action); + connect(action, &QAction::triggered, this, &JsonViewer::onBookmarkAdded); + menu.exec(m_tree->mapToGlobal(pos)); +} + +void JsonViewer::onBookmarkMenuRequested(const QPoint &pos) +{ + auto *item = m_toplevel->itemAt(pos); + if (!item) + return; + + // Don't delete toplevel items + const QModelIndex index = indexOf(item); + if (!index.parent().isValid()) + return; + + QMenu menu; + QAction *action = new QAction(tr("Delete bookmark")); + action->setData(m_toplevel->row(item)); + menu.addAction(action); + connect(action, &QAction::triggered, this, &JsonViewer::onBookmarkDeleted); + menu.exec(m_toplevel->mapToGlobal(pos)); +} + +void JsonViewer::onBookmarkAdded() +{ + const QAction *action = qobject_cast(sender()); + if (!action) + return; + + const QModelIndex index = qvariant_cast(action->data()); + if (!index.isValid()) + return; + + auto *item = new QListWidgetItem(index.data(Qt::DisplayRole).toString(), m_toplevel); + item->setData(Qt::UserRole, index); + + // Set a tooltip that shows where the item is located in the tree + QModelIndex parent = index.parent(); + QString tooltip = index.data(Qt::DisplayRole).toString(); + while (parent.isValid()) { + tooltip = parent.data(Qt::DisplayRole).toString() + "->"_L1 + tooltip; + parent = parent.parent(); + } + item->setToolTip(tooltip); +} + +void JsonViewer::onBookmarkDeleted() +{ + const QAction *action = qobject_cast(sender()); + if (!action) + return; + + const int row = action->data().toInt(); + if (row < 0 || row >= m_toplevel->count()) + return; + + delete m_toplevel->takeItem(row); +} + +bool JsonViewer::hasContent() const +{ + return !m_root.isEmpty(); +} + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +void JsonViewer::printDocument(QPrinter *printer) const +{ + if (!hasContent()) + return; + + const QTextDocument doc(QString::fromUtf8(m_root.toJson(QJsonDocument::JsonFormat::Indented))); + doc.print(printer); +} + +#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT + +QByteArray JsonViewer::saveState() const +{ + QByteArray array; + QDataStream stream(&array, QIODevice::WriteOnly); + stream << QString(viewerName()); + stream << m_tree->header()->saveState(); + return array; +} + +bool JsonViewer::restoreState(QByteArray &array) +{ + QDataStream stream(&array, QIODevice::ReadOnly); + QString viewer; + stream >> viewer; + if (viewer != viewerName()) + return false; + QByteArray header; + stream >> header; + return m_tree->header()->restoreState(header); +} + +JsonTreeItem::JsonTreeItem(JsonTreeItem *parent) +{ + m_parent = parent; +} + +JsonTreeItem::~JsonTreeItem() +{ + qDeleteAll(m_children); +} + +void JsonTreeItem::appendChild(JsonTreeItem *item) +{ + m_children.append(item); +} + +JsonTreeItem *JsonTreeItem::child(int row) +{ + return m_children.value(row); +} + +JsonTreeItem *JsonTreeItem::parent() +{ + return m_parent; +} + +int JsonTreeItem::childCount() const +{ + return m_children.count(); +} + +int JsonTreeItem::row() const +{ + if (m_parent) + return m_parent->m_children.indexOf(const_cast(this)); + + return 0; +} + +void JsonTreeItem::setKey(const QString &key) +{ + m_key = key; +} + +void JsonTreeItem::setValue(const QVariant &value) +{ + m_value = value; +} + +void JsonTreeItem::setType(const QJsonValue::Type &type) +{ + m_type = type; +} + +JsonTreeItem* JsonTreeItem::load(const QJsonValue& value, JsonTreeItem* parent) +{ + JsonTreeItem *rootItem = new JsonTreeItem(parent); + rootItem->setKey("root"_L1); + + if (value.isObject()) { + const QStringList &keys = value.toObject().keys(); + for (const QString &key : keys) { + QJsonValue v = value.toObject().value(key); + JsonTreeItem *child = load(v, rootItem); + child->setKey(key); + child->setType(v.type()); + rootItem->appendChild(child); + } + } else if (value.isArray()) { + int index = 0; + const QJsonArray &array = value.toArray(); + for (const QJsonValue &val : array) { + JsonTreeItem *child = load(val, rootItem); + child->setKey(QString::number(index)); + child->setType(val.type()); + rootItem->appendChild(child); + ++index; + } + } else { + rootItem->setValue(value.toVariant()); + rootItem->setType(value.type()); + } + + return rootItem; +} + +JsonItemModel::JsonItemModel(QObject *parent) + : QAbstractItemModel(parent) + , m_rootItem{new JsonTreeItem} +{ + m_headers.append("Key"_L1); + m_headers.append("Value"_L1); +} + +JsonItemModel::JsonItemModel(const QJsonDocument &doc, QObject *parent) + : QAbstractItemModel(parent) + , m_rootItem{new JsonTreeItem} +{ + // Append header lines and return on empty document + m_headers.append("Key"_L1); + m_headers.append("Value"_L1); + if (doc.isNull()) + return; + + // Reset the model. Root can either be a value or an array. + beginResetModel(); + delete m_rootItem; + if (doc.isArray()) { + m_rootItem = JsonTreeItem::load(QJsonValue(doc.array())); + m_rootItem->setType(QJsonValue::Array); + + } else { + m_rootItem = JsonTreeItem::load(QJsonValue(doc.object())); + m_rootItem->setType(QJsonValue::Object); + } + endResetModel(); +} + +JsonItemModel::~JsonItemModel() +{ + delete m_rootItem; +} + +QVariant JsonItemModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return {}; + + JsonTreeItem *item = itemFromIndex(index); + + switch (role) { + case Qt::DisplayRole: + if (index.column() == 0) + return item->key(); + if (index.column() == 1) + return item->value(); + break; + case Qt::EditRole: + if (index.column() == 1) + return item->value(); + break; + default: + break; + } + return {}; +} + +QVariant JsonItemModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return {}; + + if (orientation == Qt::Horizontal) + return m_headers.value(section); + else + return {}; +} + +QModelIndex JsonItemModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) + return {}; + + JsonTreeItem *parentItem; + + if (!parent.isValid()) + parentItem = m_rootItem; + else + parentItem = itemFromIndex(parent); + + JsonTreeItem *childItem = parentItem->child(row); + if (childItem) + return createIndex(row, column, childItem); + else + return {}; +} + +QModelIndex JsonItemModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return {}; + + JsonTreeItem *childItem = itemFromIndex(index); + JsonTreeItem *parentItem = childItem->parent(); + + if (parentItem == m_rootItem) + return QModelIndex(); + + return createIndex(parentItem->row(), 0, parentItem); +} + +int JsonItemModel::rowCount(const QModelIndex &parent) const +{ + JsonTreeItem *parentItem; + if (parent.column() > 0) + return 0; + + if (!parent.isValid()) + parentItem = m_rootItem; + else + parentItem = itemFromIndex(parent); + + return parentItem->childCount(); +} diff --git a/plugins/jsonviewer/jsonviewer.h b/plugins/jsonviewer/jsonviewer.h new file mode 100644 index 0000000..58345bc --- /dev/null +++ b/plugins/jsonviewer/jsonviewer.h @@ -0,0 +1,109 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef JSONVIEWER_H +#define JSONVIEWER_H + +#include "viewerinterfaces.h" + +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QTreeView; +class QListWidget; +class QListWidgetItem; +class QLineEdit; +QT_END_NAMESPACE + +class JsonViewer : public ViewerInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.DocumentViewer.ViewerInterface/1.0" FILE "jsonviewer.json") + Q_INTERFACES(ViewerInterface) +public: + JsonViewer(); + ~JsonViewer() override; + + void init(QFile *file, QWidget *parent, QMainWindow *mainWindow) override; + QString viewerName() const override { return QLatin1StringView(staticMetaObject.className()); }; + QStringList supportedMimeTypes() const override; + QByteArray saveState() const override; + bool restoreState(QByteArray &) override; + bool supportsOverview() const override { return true; } + bool hasContent() const override; + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +protected: + void printDocument(QPrinter *printer) const override; +#endif // QT_ABSTRACTVIEWER_PRINTSUPPORT + +private slots: + void setupJsonUi(); + void onTopLevelItemClicked(QListWidgetItem *item); + void onTopLevelItemDoubleClicked(QListWidgetItem *item); + void onJsonMenuRequested(const QPoint &pos); + void onBookmarkMenuRequested(const QPoint &pos); + void onBookmarkAdded(); + void onBookmarkDeleted(); + +private: + bool openJsonFile(); + + QTreeView *m_tree; + QListWidget *m_toplevel = nullptr; + QJsonDocument m_root; + + QPointer m_searchKey; +}; + +class JsonTreeItem +{ +public: + JsonTreeItem(JsonTreeItem *parent = nullptr); + ~JsonTreeItem(); + void appendChild(JsonTreeItem *item); + JsonTreeItem *child(int row); + JsonTreeItem *parent(); + int childCount() const; + int row() const; + void setKey(const QString& key); + void setValue(const QVariant& value); + void setType(const QJsonValue::Type& type); + QString key() const { return m_key; }; + QVariant value() const { return m_value; }; + QJsonValue::Type type() const { return m_type; }; + + static JsonTreeItem* load(const QJsonValue& value, JsonTreeItem *parent = nullptr); + +private: + QString m_key; + QVariant m_value; + QJsonValue::Type m_type; + QList m_children; + JsonTreeItem *m_parent = nullptr; +}; + +class JsonItemModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit JsonItemModel(QObject *parent = nullptr); + JsonItemModel(const QJsonDocument& doc, QObject *parent = nullptr); + ~JsonItemModel(); + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QModelIndex index(int row, int column,const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override { return 2; }; + +private: + JsonTreeItem *m_rootItem = nullptr; + QStringList m_headers; + static JsonTreeItem *itemFromIndex(const QModelIndex &index) + {return static_cast(index.internalPointer()); } +}; + +#endif //JSONVIEWER_H diff --git a/plugins/jsonviewer/jsonviewer.json b/plugins/jsonviewer/jsonviewer.json new file mode 100644 index 0000000..8ced8d5 --- /dev/null +++ b/plugins/jsonviewer/jsonviewer.json @@ -0,0 +1 @@ +{ "Keys": [ "jsonviewer" ] } diff --git a/plugins/pdfviewer/CMakeLists.txt b/plugins/pdfviewer/CMakeLists.txt new file mode 100644 index 0000000..3389b70 --- /dev/null +++ b/plugins/pdfviewer/CMakeLists.txt @@ -0,0 +1,39 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets PdfWidgets + OPTIONAL_COMPONENTS PrintSupport) + +qt_add_plugin(pdfviewer + CLASS_NAME PdfViewer + pdfviewer.cpp pdfviewer.h + zoomselector.cpp zoomselector.h + hoverwatcher.cpp hoverwatcher.h +) + +set_target_properties(pdfviewer PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/app" +) + +target_include_directories(pdfviewer PRIVATE + ../../app + .. +) + +target_link_libraries(pdfviewer PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::PdfWidgets + abstractviewer +) + +if(TARGET Qt6::PrintSupport) + target_link_libraries(pdfviewer PRIVATE Qt6::PrintSupport) +endif() + +install(TARGETS pdfviewer + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}/plugins" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}/plugins" +) + diff --git a/plugins/pdfviewer/hoverwatcher.cpp b/plugins/pdfviewer/hoverwatcher.cpp new file mode 100644 index 0000000..5268581 --- /dev/null +++ b/plugins/pdfviewer/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 +#include +#include + +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 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(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(event); + if (me->pos().x() < -9000000 ) + return false; + } + + // Ignore unpermitted mouse buttons + if (action == HoverAction::MousePress) { + QMouseEvent *me = static_cast(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/plugins/pdfviewer/hoverwatcher.h b/plugins/pdfviewer/hoverwatcher.h new file mode 100644 index 0000000..22ca94c --- /dev/null +++ b/plugins/pdfviewer/hoverwatcher.h @@ -0,0 +1,69 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef HOVERWATCHER_H +#define HOVERWATCHER_H +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QWidget; +QT_END_NAMESPACE + +class HoverWatcher : public QObject +{ + Q_OBJECT + +private: + explicit HoverWatcher(QWidget *watched); + static QMap 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, 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/plugins/pdfviewer/pdfviewer.cpp b/plugins/pdfviewer/pdfviewer.cpp new file mode 100644 index 0000000..93b4c82 --- /dev/null +++ b/plugins/pdfviewer/pdfviewer.cpp @@ -0,0 +1,266 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "pdfviewer.h" +#include "zoomselector.h" +#include "hoverwatcher.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(6,6,0) +#include +#endif + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +#include +#include +#endif + +Q_LOGGING_CATEGORY(lcExample, "qt.examples.pdfviewer") + +using namespace Qt::StringLiterals; + +PdfViewer::PdfViewer() +{ + connect(this, &AbstractViewer::uiInitialized, this, &PdfViewer::initPdfViewer); +} + +void PdfViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow) +{ + AbstractViewer::init(file, new QPdfView(parent), mainWindow); + m_document = new QPdfDocument(this); + m_pdfView = qobject_cast(widget()); +} + +void PdfViewer::cleanup() +{ + delete m_pageSelector; + m_pageSelector = nullptr; + delete m_zoomSelector; + m_zoomSelector = nullptr; + delete m_pages; + m_pages = nullptr; + delete m_bookmarks; + m_bookmarks = nullptr; + delete m_document; + m_document = nullptr; + AbstractViewer::cleanup(); +} + +PdfViewer::~PdfViewer() +{ + PdfViewer::cleanup(); +} + +QStringList PdfViewer::supportedMimeTypes() const +{ + return {"application/pdf"_L1}; +} + +void PdfViewer::initPdfViewer() +{ + m_toolBar = addToolBar(tr("PDF")); + m_zoomSelector = new ZoomSelector(m_toolBar); + + auto *nav = m_pdfView->pageNavigator(); +#if QT_VERSION >= QT_VERSION_CHECK(6,6,0) + m_pageSelector = new QPdfPageSelector(m_toolBar); + m_toolBar->insertWidget(m_uiAssets.forward, m_pageSelector); + m_pageSelector->setDocument(m_document); + connect(m_pageSelector, &QPdfPageSelector::currentPageChanged, + this, &PdfViewer::pageSelected); + connect(m_pageSelector, &QPdfPageSelector::currentPageChanged, + this, &PdfViewer::pageSelected); + connect(nav, &QPdfPageNavigator::currentPageChanged, + m_pageSelector, &QPdfPageSelector::setCurrentPage); +#endif + + connect(m_pdfView->pageNavigator(), &QPdfPageNavigator::backAvailableChanged, + m_uiAssets.back, &QAction::setEnabled); + m_actionBack = m_uiAssets.back; + m_actionForward = m_uiAssets.forward; + m_connections.append(connect(m_uiAssets.back, &QAction::triggered, + this, &PdfViewer::onActionBackTriggered)); + m_connections.append(connect(m_uiAssets.forward, &QAction::triggered, + this, &PdfViewer::onActionForwardTriggered)); + + m_toolBar->addSeparator(); + m_toolBar->addWidget(m_zoomSelector); + + auto *actionZoomIn = m_toolBar->addAction(tr("Zoom in")); + actionZoomIn->setToolTip(tr("Increase zoom level")); + actionZoomIn->setIcon(QIcon(":/demos/documentviewer/images/zoom-in.png"_L1)); + m_toolBar->addAction(actionZoomIn); + connect(actionZoomIn, &QAction::triggered, this, &PdfViewer::onActionZoomInTriggered); + + auto *actionZoomOut = m_toolBar->addAction(tr("Zoom out")); + actionZoomOut->setToolTip(tr("Decrease zoom level")); + actionZoomOut->setIcon(QIcon(":/demos/documentviewer/images/zoom-out.png"_L1)); + m_toolBar->addAction(actionZoomOut); + connect(actionZoomOut, &QAction::triggered, this, &PdfViewer::onActionZoomOutTriggered); + + connect(nav, &QPdfPageNavigator::backAvailableChanged, m_actionBack, &QAction::setEnabled); + connect(nav, &QPdfPageNavigator::forwardAvailableChanged, m_actionForward, &QAction::setEnabled); + + connect(m_zoomSelector, &ZoomSelector::zoomModeChanged, m_pdfView, &QPdfView::setZoomMode); + connect(m_zoomSelector, &ZoomSelector::zoomFactorChanged, m_pdfView, &QPdfView::setZoomFactor); + m_zoomSelector->reset(); + + QPdfBookmarkModel *bookmarkModel = new QPdfBookmarkModel(this); + bookmarkModel->setDocument(m_document); + m_uiAssets.tabs->clear(); + m_bookmarks = new QTreeView(m_uiAssets.tabs); + connect(m_bookmarks, &QAbstractItemView::activated, this, &PdfViewer::bookmarkSelected); + m_bookmarks->setModel(bookmarkModel); + m_pdfView->setDocument(m_document); + m_pdfView->setPageMode(QPdfView::PageMode::MultiPage); + + openPdfFile(); + if (!m_document->pageCount()) + return; + + m_pages = new QListView(m_uiAssets.tabs); + m_pages->setModel(m_document->pageModel()); + connect(m_pages->selectionModel(), &QItemSelectionModel::currentRowChanged, m_pages, [&] + (const QModelIndex ¤t, const QModelIndex &previous){ + if (previous == current) + return; + + auto *nav = m_pdfView->pageNavigator(); + const int &row = current.row(); + if (nav->currentPage() == row) + return; + + nav->jump(row, QPointF(), nav->currentZoom()); + }); + + connect(m_pdfView->pageNavigator(), &QPdfPageNavigator::currentPageChanged, m_pages, [&](int page){ + if (m_pages->currentIndex().row() == page) + return; + + m_pages->setCurrentIndex(m_pages->model()->index(page, 0)); + }); + + m_uiAssets.tabs->addTab(m_pages, tr("Pages")); + m_uiAssets.tabs->addTab(m_bookmarks, tr("Bookmarks")); + QScroller::grabGesture(m_pdfView->viewport(), QScroller::ScrollerGestureType::LeftMouseButtonGesture); + HoverWatcher::watcher(m_pdfView->viewport()); +} + +void PdfViewer::openPdfFile() +{ + disablePrinting(); + + if (m_file->open(QIODevice::ReadOnly)) + m_document->load(m_file.get()); + + const auto documentTitle = m_document->metaData(QPdfDocument::MetaDataField::Title).toString(); + statusMessage(documentTitle.isEmpty() ? "PDF Viewer"_L1 : documentTitle); + pageSelected(0); + + statusMessage(tr("Opened PDF file %1") + .arg(QDir::toNativeSeparators(m_file->fileName()))); + qCDebug(lcExample) << "Opened file" << m_file->fileName(); + + maybeEnablePrinting(); +} + +bool PdfViewer::hasContent() const +{ + return m_document ? m_document->pageCount() > 0 : false; +} + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +void PdfViewer::printDocument(QPrinter *printer) const +{ + if (!hasContent()) + return; + + QPainter painter; + painter.begin(printer); + const QRect pageRect = printer->pageRect(QPrinter::Unit::DevicePixel).toRect(); + const QSize pageSize = pageRect.size(); + for (int i = 0; i < m_document->pageCount(); ++i) { + if (i > 0) + printer->newPage(); + const QImage &page = m_document->render(i, pageSize); + painter.drawImage(pageRect, page); + } + painter.end(); +} +#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT + +void PdfViewer::bookmarkSelected(const QModelIndex &index) +{ + if (!index.isValid()) + return; + + const int page = index.data(int(QPdfBookmarkModel::Role::Page)).toInt(); + const qreal zoomLevel = index.data(int(QPdfBookmarkModel::Role::Level)).toReal(); + m_pdfView->pageNavigator()->jump(page, {}, zoomLevel); +} + +void PdfViewer::pageSelected(int page) +{ + auto nav = m_pdfView->pageNavigator(); + nav->jump(page, {}, nav->currentZoom()); +} + +void PdfViewer::onActionZoomInTriggered() +{ + m_pdfView->setZoomFactor(m_pdfView->zoomFactor() * zoomMultiplier); +} + +void PdfViewer::onActionZoomOutTriggered() +{ + m_pdfView->setZoomFactor(m_pdfView->zoomFactor() / zoomMultiplier); +} + +void PdfViewer::onActionPreviousPageTriggered() +{ + auto nav = m_pdfView->pageNavigator(); + nav->jump(nav->currentPage() - 1, {}, nav->currentZoom()); +} + +void PdfViewer::onActionNextPageTriggered() +{ + auto nav = m_pdfView->pageNavigator(); + nav->jump(nav->currentPage() + 1, {}, nav->currentZoom()); +} + +void PdfViewer::onActionBackTriggered() +{ + m_pdfView->pageNavigator()->back(); +} + +void PdfViewer::onActionForwardTriggered() +{ + m_pdfView->pageNavigator()->forward(); +} diff --git a/plugins/pdfviewer/pdfviewer.h b/plugins/pdfviewer/pdfviewer.h new file mode 100644 index 0000000..744f4a9 --- /dev/null +++ b/plugins/pdfviewer/pdfviewer.h @@ -0,0 +1,76 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef PDFVIEWER_H +#define PDFVIEWER_H + +#include "viewerinterfaces.h" +#include + +Q_DECLARE_LOGGING_CATEGORY(lcExample) + +QT_BEGIN_NAMESPACE +class QMainWindow; +class QPdfDocument; +class QPdfView; +class QPdfPageSelector; +class QListView; +class QTabWidget; +class QTreeView; +QT_END_NAMESPACE + +class ZoomSelector; +class PdfViewer : public ViewerInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.DocumentViewer.ViewerInterface" FILE "pdfviewer.json") + Q_INTERFACES(ViewerInterface) +public: + PdfViewer(); + ~PdfViewer() override; + void init(QFile *file, QWidget *parent, QMainWindow *mainWindow) override; + void cleanup() override; + QString viewerName() const override { return QLatin1StringView(staticMetaObject.className()); }; + QStringList supportedMimeTypes() const override; + bool supportsOverview() const override { return true; } + bool hasContent() const override; + QByteArray saveState() const override { return QByteArray(); } + bool restoreState(QByteArray &) override { return true; } + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +protected: + void printDocument(QPrinter *printer) const override; +#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT + +public slots: + void openPdfFile(); + +private slots: + void initPdfViewer(); + void bookmarkSelected(const QModelIndex &index); + void pageSelected(int page); + + // action handlers + void onActionZoomInTriggered(); + void onActionZoomOutTriggered(); + void onActionPreviousPageTriggered(); + void onActionNextPageTriggered(); + void onActionBackTriggered(); + void onActionForwardTriggered(); + +private: + void populateQuestions(); + + const qreal zoomMultiplier = qSqrt(2.0); + QToolBar *m_toolBar = nullptr; + ZoomSelector *m_zoomSelector = nullptr; + QPdfPageSelector *m_pageSelector = nullptr; + QPdfDocument *m_document = nullptr; + QPdfView *m_pdfView = nullptr; + QAction *m_actionForward = nullptr; + QAction *m_actionBack = nullptr; + QTreeView *m_bookmarks = nullptr; + QListView *m_pages = nullptr; +}; + +#endif //PDFVIEWER_H diff --git a/plugins/pdfviewer/pdfviewer.json b/plugins/pdfviewer/pdfviewer.json new file mode 100644 index 0000000..e237d2f --- /dev/null +++ b/plugins/pdfviewer/pdfviewer.json @@ -0,0 +1 @@ +{ "Keys": [ "pdfviewer" ] } diff --git a/plugins/pdfviewer/zoomselector.cpp b/plugins/pdfviewer/zoomselector.cpp new file mode 100644 index 0000000..4832d50 --- /dev/null +++ b/plugins/pdfviewer/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 + +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/plugins/pdfviewer/zoomselector.h b/plugins/pdfviewer/zoomselector.h new file mode 100644 index 0000000..a7ed7b6 --- /dev/null +++ b/plugins/pdfviewer/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 +#include + +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 diff --git a/plugins/txtviewer/CMakeLists.txt b/plugins/txtviewer/CMakeLists.txt new file mode 100644 index 0000000..a07b5d3 --- /dev/null +++ b/plugins/txtviewer/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets + OPTIONAL_COMPONENTS PrintSupport) + +qt_add_plugin(txtviewer + CLASS_NAME TxtViewer + txtviewer.cpp txtviewer.h +) + +set_target_properties(txtviewer PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/app" +) + +target_include_directories(txtviewer PRIVATE + ../../app +) + +target_link_libraries(txtviewer PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + abstractviewer +) + +if(TARGET Qt6::PrintSupport) + target_link_libraries(txtviewer PRIVATE Qt6::PrintSupport) +endif() + +install(TARGETS jsonviewer + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}/plugins" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}/plugins" +) + diff --git a/plugins/txtviewer/txtviewer.cpp b/plugins/txtviewer/txtviewer.cpp new file mode 100644 index 0000000..a9b4c5f --- /dev/null +++ b/plugins/txtviewer/txtviewer.cpp @@ -0,0 +1,196 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +//! [init] +#include "txtviewer.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +#include +#include +#endif + +using namespace Qt::StringLiterals; + +TxtViewer::TxtViewer() +{ + connect(this, &AbstractViewer::uiInitialized, this, &TxtViewer::setupTxtUi); +} + +TxtViewer::~TxtViewer() = default; + +void TxtViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow) +{ + AbstractViewer::init(file, new QPlainTextEdit(parent), mainWindow); + m_textEdit = qobject_cast(widget()); +} + +QStringList TxtViewer::supportedMimeTypes() const +{ + return {"text/plain"_L1}; +} + +void TxtViewer::setupTxtUi() +{ + QMenu *editMenu = addMenu(tr("&Edit")); + QToolBar *editToolBar = addToolBar(tr("Edit")); +#ifndef QT_NO_CLIPBOARD + const QIcon cutIcon = QIcon::fromTheme("edit-cut"_L1, + QIcon(":/demos/documentviewer/images/cut.png"_L1)); + QAction *cutAct = new QAction(cutIcon, tr("Cu&t"), this); + cutAct->setShortcuts(QKeySequence::Cut); + cutAct->setStatusTip(tr("Cut the current selection's contents to the " + "clipboard")); + connect(cutAct, &QAction::triggered, m_textEdit, &QPlainTextEdit::cut); + editMenu->addAction(cutAct); + editToolBar->addAction(cutAct); + + const QIcon copyIcon = QIcon::fromTheme("edit-copy"_L1, + QIcon(":/demos/documentviewer/images/copy.png"_L1)); + QAction *copyAct = new QAction(copyIcon, tr("&Copy"), this); + copyAct->setShortcuts(QKeySequence::Copy); + copyAct->setStatusTip(tr("Copy the current selection's contents to the " + "clipboard")); + connect(copyAct, &QAction::triggered, m_textEdit, &QPlainTextEdit::copy); + editMenu->addAction(copyAct); + editToolBar->addAction(copyAct); + + const QIcon pasteIcon = QIcon::fromTheme("edit-paste"_L1, + QIcon(":/demos/documentviewer/images/paste.png"_L1)); + QAction *pasteAct = new QAction(pasteIcon, tr("&Paste"), this); + pasteAct->setShortcuts(QKeySequence::Paste); + pasteAct->setStatusTip(tr("Paste the clipboard's contents into the current " + "selection")); + connect(pasteAct, &QAction::triggered, m_textEdit, &QPlainTextEdit::paste); + editMenu->addAction(pasteAct); + editToolBar->addAction(pasteAct); + + menuBar()->addSeparator(); + + cutAct->setEnabled(false); + copyAct->setEnabled(false); + connect(m_textEdit, &QPlainTextEdit::copyAvailable, cutAct, &QAction::setEnabled); + connect(m_textEdit, &QPlainTextEdit::copyAvailable, copyAct, &QAction::setEnabled); +#endif // !QT_NO_CLIPBOARD + + openFile(); + + connect(m_textEdit, &QPlainTextEdit::textChanged, this, [&](){ + maybeSetPrintingEnabled(hasContent()); + }); + + connect(m_uiAssets.back, &QAction::triggered, m_textEdit, [&](){ + auto *bar = m_textEdit->verticalScrollBar(); + if (bar->value() > bar->minimum()) + bar->setValue(bar->value() - 1); + }); + + connect(m_uiAssets.forward, &QAction::triggered, m_textEdit, [&](){ + auto *bar = m_textEdit->verticalScrollBar(); + if (bar->value() < bar->maximum()) + bar->setValue(bar->value() + 1); + }); +} +//! [init] + +//! [open] +void TxtViewer::openFile() +{ + const QString type = tr("open"); + if (!m_file->open(QFile::ReadOnly | QFile::Text)) { + statusMessage(tr("Cannot read file %1:\n%2.") + .arg(QDir::toNativeSeparators(m_file->fileName()), + m_file->errorString()), type); + return; + } + + QTextStream in(m_file.get()); +#ifndef QT_NO_CURSOR + QGuiApplication::setOverrideCursor(Qt::WaitCursor); +#endif + if (!m_textEdit->toPlainText().isEmpty()) { + m_textEdit->clear(); + disablePrinting(); + } + m_textEdit->setPlainText(in.readAll()); +#ifndef QT_NO_CURSOR + QGuiApplication::restoreOverrideCursor(); +#endif + + statusMessage(tr("File %1 loaded.") + .arg(QDir::toNativeSeparators(m_file->fileName())), type); + maybeEnablePrinting(); +} +//! [open] + +//! [infoPrintAndSave] +bool TxtViewer::hasContent() const +{ + return (!m_textEdit->toPlainText().isEmpty()); +} + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +void TxtViewer::printDocument(QPrinter *printer) const +{ + if (!hasContent()) + return; + + m_textEdit->print(printer); +} +#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT + +bool TxtViewer::saveFile(QFile *file) +{ + QString errorMessage; + + QGuiApplication::setOverrideCursor(Qt::WaitCursor); + if (file->open(QFile::WriteOnly | QFile::Text)) { + QTextStream out(file); + out << m_textEdit->toPlainText(); + } else { + errorMessage = tr("Cannot open file %1 for writing:\n%2.") + .arg(QDir::toNativeSeparators(file->fileName())), + file->errorString(); + } + QGuiApplication::restoreOverrideCursor(); + + if (!errorMessage.isEmpty()) { + statusMessage(errorMessage); + return false; + } + + statusMessage(tr("File %1 saved") + .arg(QDir::toNativeSeparators(file->fileName()))); + return true; +} + +bool TxtViewer::saveDocumentAs() +{ + QFileDialog dialog(mainWindow()); + dialog.setWindowModality(Qt::WindowModal); + dialog.setAcceptMode(QFileDialog::AcceptSave); + if (dialog.exec() != QDialog::Accepted) + return false; + + const QStringList &files = dialog.selectedFiles(); + if (files.isEmpty()) + return false; + + //newFile(); + m_file->setFileName(files.first()); + return saveDocument(); +} +//! [infoPrintAndSave] diff --git a/plugins/txtviewer/txtviewer.h b/plugins/txtviewer/txtviewer.h new file mode 100644 index 0000000..aa8cb37 --- /dev/null +++ b/plugins/txtviewer/txtviewer.h @@ -0,0 +1,50 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef TXTVIEWER_H +#define TXTVIEWER_H + +#include "viewerinterfaces.h" + +QT_BEGIN_NAMESPACE +class QPlainTextEdit; +QT_END_NAMESPACE + +//! [interfacing] +class TxtViewer : public ViewerInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.DocumentViewer.ViewerInterface" FILE "txtviewer.json") + Q_INTERFACES(ViewerInterface) +//! [interfacing] +//! [classDefinition] +public: + TxtViewer(); + ~TxtViewer() override; + void init(QFile *file, QWidget *parent, QMainWindow *mainWindow) override; + QString viewerName() const override { return QLatin1StringView(staticMetaObject.className()); }; + QStringList supportedMimeTypes() const override; + bool saveDocument() override { return saveFile(m_file.get()); }; + bool saveDocumentAs() override; + bool hasContent() const override; + QByteArray saveState() const override { return {}; } + bool restoreState(QByteArray &) override { return true; } + bool supportsOverview() const override { return false; } + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +protected: + void printDocument(QPrinter *printer) const override; +#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT + +private slots: + void setupTxtUi(); + +private: + void openFile(); + bool saveFile (QFile *file); + + QPlainTextEdit *m_textEdit; +}; +//! [classDefinition] + +#endif //TXTVIEWER_H diff --git a/plugins/txtviewer/txtviewer.json b/plugins/txtviewer/txtviewer.json new file mode 100644 index 0000000..30d13e0 --- /dev/null +++ b/plugins/txtviewer/txtviewer.json @@ -0,0 +1 @@ +{ "Keys": [ "txtviewer" ] } diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..cd758da --- /dev/null +++ b/run.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +rm -rf build/ +mkdir -p build/ +cd build + +# Build the project +cmake -G "Unix Makefiles" -DCMAKE_PREFIX_PATH=$HOME/Qt/6.6.1/gcc_64/ \ +-DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .. + +make + +cp ./compile_commands.json ../