From 45c681e7bee443e08f848bf2f6ae57f9d9741960 Mon Sep 17 00:00:00 2001 From: talksik Date: Wed, 7 Feb 2024 13:12:51 -0800 Subject: [PATCH] making clangd happy and making script to compile/build with run.sh --- .gitignore | 1 + CMakeLists.txt | 25 ++ CMakeLists.txt.user | 417 +++++++++++++++++++ app/CMakeLists.txt | 68 +++ app/abstractviewer.cpp | 281 +++++++++++++ app/abstractviewer.h | 101 +++++ app/documentviewer.qrc | 42 ++ app/images/copy.png | Bin 0 -> 1338 bytes app/images/copy@2x.png | Bin 0 -> 2219 bytes app/images/cut.png | Bin 0 -> 1323 bytes app/images/cut@2x.png | Bin 0 -> 2888 bytes app/images/document-open-recent.svgz | Bin 0 -> 4656 bytes app/images/go-next-view-page.png | Bin 0 -> 1125 bytes app/images/go-next-view-page@2x.png | Bin 0 -> 2312 bytes app/images/go-next-view.png | Bin 0 -> 990 bytes app/images/go-next-view@2x.png | Bin 0 -> 1830 bytes app/images/go-next.svgz | Bin 0 -> 5473 bytes app/images/go-previous-view-page.png | Bin 0 -> 1119 bytes app/images/go-previous-view-page@2x.png | Bin 0 -> 2067 bytes app/images/go-previous-view.png | Bin 0 -> 1010 bytes app/images/go-previous-view@2x.png | Bin 0 -> 1875 bytes app/images/go-previous.svgz | Bin 0 -> 5690 bytes app/images/help-about.svgz | Bin 0 -> 7056 bytes app/images/magnifier.png | Bin 0 -> 1006 bytes app/images/magnifier@2x.png | Bin 0 -> 2249 bytes app/images/open.png | Bin 0 -> 2073 bytes app/images/open@2x.png | Bin 0 -> 1017 bytes app/images/paste.png | Bin 0 -> 1645 bytes app/images/paste@2x.png | Bin 0 -> 2782 bytes app/images/print.png | Bin 0 -> 678 bytes app/images/print2x.png | Bin 0 -> 931 bytes app/images/qt-logo.png | Bin 0 -> 1483 bytes app/images/qt-logo@2x.png | Bin 0 -> 1933 bytes app/images/zoom-fit-best.png | Bin 0 -> 1067 bytes app/images/zoom-fit-best@2x.png | Bin 0 -> 1692 bytes app/images/zoom-fit-width.png | Bin 0 -> 905 bytes app/images/zoom-fit-width@2x.png | Bin 0 -> 1724 bytes app/images/zoom-in.png | Bin 0 -> 952 bytes app/images/zoom-in@2x.png | Bin 0 -> 2100 bytes app/images/zoom-original.png | Bin 0 -> 946 bytes app/images/zoom-original@2x.png | Bin 0 -> 2005 bytes app/images/zoom-out.png | Bin 0 -> 940 bytes app/images/zoom-out@2x.png | Bin 0 -> 1971 bytes app/images/zoom-previous.png | Bin 0 -> 946 bytes app/images/zoom-previous@2x.png | Bin 0 -> 2049 bytes app/main.cpp | 36 ++ app/mainwindow.cpp | 190 +++++++++ app/mainwindow.h | 56 +++ app/mainwindow.ui | 272 ++++++++++++ app/recentfilemenu.cpp | 36 ++ app/recentfilemenu.h | 28 ++ app/recentfiles.cpp | 128 ++++++ app/recentfiles.h | 71 ++++ app/viewerfactory.cpp | 189 +++++++++ app/viewerfactory.h | 62 +++ app/viewerinterfaces.h | 35 ++ compile_commands.json | 67 +++ doc/images/documentviewer_open.png | Bin 0 -> 43176 bytes doc/images/txtviewer_screenshot.png | Bin 0 -> 22921 bytes doc/src/documentviewer.qdoc | 288 +++++++++++++ doc/src/txtviewer.qdoc | 111 +++++ plugins/CMakeLists.txt | 9 + plugins/jsonviewer/CMakeLists.txt | 35 ++ plugins/jsonviewer/jsonviewer.cpp | 525 ++++++++++++++++++++++++ plugins/jsonviewer/jsonviewer.h | 109 +++++ plugins/jsonviewer/jsonviewer.json | 1 + plugins/pdfviewer/CMakeLists.txt | 39 ++ plugins/pdfviewer/hoverwatcher.cpp | 212 ++++++++++ plugins/pdfviewer/hoverwatcher.h | 69 ++++ plugins/pdfviewer/pdfviewer.cpp | 266 ++++++++++++ plugins/pdfviewer/pdfviewer.h | 76 ++++ plugins/pdfviewer/pdfviewer.json | 1 + plugins/pdfviewer/zoomselector.cpp | 64 +++ plugins/pdfviewer/zoomselector.h | 30 ++ plugins/txtviewer/CMakeLists.txt | 35 ++ plugins/txtviewer/txtviewer.cpp | 196 +++++++++ plugins/txtviewer/txtviewer.h | 50 +++ plugins/txtviewer/txtviewer.json | 1 + run.sh | 13 + 79 files changed, 4235 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CMakeLists.txt.user create mode 100644 app/CMakeLists.txt create mode 100644 app/abstractviewer.cpp create mode 100644 app/abstractviewer.h create mode 100644 app/documentviewer.qrc create mode 100644 app/images/copy.png create mode 100644 app/images/copy@2x.png create mode 100644 app/images/cut.png create mode 100644 app/images/cut@2x.png create mode 100644 app/images/document-open-recent.svgz create mode 100644 app/images/go-next-view-page.png create mode 100644 app/images/go-next-view-page@2x.png create mode 100644 app/images/go-next-view.png create mode 100644 app/images/go-next-view@2x.png create mode 100644 app/images/go-next.svgz create mode 100644 app/images/go-previous-view-page.png create mode 100644 app/images/go-previous-view-page@2x.png create mode 100644 app/images/go-previous-view.png create mode 100644 app/images/go-previous-view@2x.png create mode 100644 app/images/go-previous.svgz create mode 100644 app/images/help-about.svgz create mode 100644 app/images/magnifier.png create mode 100644 app/images/magnifier@2x.png create mode 100644 app/images/open.png create mode 100644 app/images/open@2x.png create mode 100644 app/images/paste.png create mode 100644 app/images/paste@2x.png create mode 100644 app/images/print.png create mode 100644 app/images/print2x.png create mode 100644 app/images/qt-logo.png create mode 100644 app/images/qt-logo@2x.png create mode 100644 app/images/zoom-fit-best.png create mode 100644 app/images/zoom-fit-best@2x.png create mode 100644 app/images/zoom-fit-width.png create mode 100644 app/images/zoom-fit-width@2x.png create mode 100644 app/images/zoom-in.png create mode 100644 app/images/zoom-in@2x.png create mode 100644 app/images/zoom-original.png create mode 100644 app/images/zoom-original@2x.png create mode 100644 app/images/zoom-out.png create mode 100644 app/images/zoom-out@2x.png create mode 100644 app/images/zoom-previous.png create mode 100644 app/images/zoom-previous@2x.png create mode 100644 app/main.cpp create mode 100644 app/mainwindow.cpp create mode 100644 app/mainwindow.h create mode 100644 app/mainwindow.ui create mode 100644 app/recentfilemenu.cpp create mode 100644 app/recentfilemenu.h create mode 100644 app/recentfiles.cpp create mode 100644 app/recentfiles.h create mode 100644 app/viewerfactory.cpp create mode 100644 app/viewerfactory.h create mode 100644 app/viewerinterfaces.h create mode 100644 compile_commands.json create mode 100644 doc/images/documentviewer_open.png create mode 100644 doc/images/txtviewer_screenshot.png create mode 100644 doc/src/documentviewer.qdoc create mode 100644 doc/src/txtviewer.qdoc create mode 100644 plugins/CMakeLists.txt create mode 100644 plugins/jsonviewer/CMakeLists.txt create mode 100644 plugins/jsonviewer/jsonviewer.cpp create mode 100644 plugins/jsonviewer/jsonviewer.h create mode 100644 plugins/jsonviewer/jsonviewer.json create mode 100644 plugins/pdfviewer/CMakeLists.txt create mode 100644 plugins/pdfviewer/hoverwatcher.cpp create mode 100644 plugins/pdfviewer/hoverwatcher.h create mode 100644 plugins/pdfviewer/pdfviewer.cpp create mode 100644 plugins/pdfviewer/pdfviewer.h create mode 100644 plugins/pdfviewer/pdfviewer.json create mode 100644 plugins/pdfviewer/zoomselector.cpp create mode 100644 plugins/pdfviewer/zoomselector.h create mode 100644 plugins/txtviewer/CMakeLists.txt create mode 100644 plugins/txtviewer/txtviewer.cpp create mode 100644 plugins/txtviewer/txtviewer.h create mode 100644 plugins/txtviewer/txtviewer.json create mode 100755 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 0000000000000000000000000000000000000000..2aeb28288f58ddffdd1d75115f170c5bf2773814 GIT binary patch literal 1338 zcmV-A1;zS_P)3P|jKV4ArrNQZsr&q&3Fam_48Lh`MiYI|sB6GiaYuZZ? zasnKC=d9Ws*vOa?K!llv;~9}LDH=;*C6q)snnH7j#=aB8{{oN_lX)cZ$XJrkAB0_u z!sQ7f5=*1>!|zQrby)$h>)1Bcke)jH%(>@ZE)hRgo<&7f4Jw)5udTzKv5Ch3thOcm zx#)*$6jZ zxjE=2CQwjNfFaFqeBMc>`320FTpYmRPeJsB@I$Z8%>Zil0#HL{*yW1HLMx&9BQ>hew>g`42C!b-4K{%G& zPNKpdi`TSeSQ5jt zz}D6l-wA<00HP?u@AqSEZ4IXsmC*dI=c&nj!3&r@K%3kE+d(!rH~Ds?RumQ0==)XR zsU3kw0Ovao0D#OTfFv_dBoZhqui^lIxB!co0iZTo)dw&I|ClF}JUvZG$*D>XptrLF zi9N;m7cj8^d~x*v0Ho^4m=ue}P^PGYdPu;S8Mtc&=A4=P=T~F|kkV6X0*FSV+&mNt zp}4dH*9HaLW{2dU0UTt+1SmUX21q@ea}t@;xj-PrWmUM?EueooW3GLWY2c%FnE?J> zJ*6hCuC8*3w@NB-xmUnF8@PU+0q)ELXD;m%K&qWYi;j}haZKLWGrnrG9>`fBl6@D%o`^aXzvlAyk=W}Pc`B^ahku5Y4`j}CIOJCBx$u% zLNX5qgK)WA+>l;M=(ES;f!poI@e{8lpM9a6Ogos0nT|fmct-%0O2q-B6)w#;8B$U@ z_bIi*;ou_U#l=Mq_2TiDT=X8flj*JiEiEl>>0qJM(w7a5mzS5}^Z8)2+4y~FX^BG+ z0F4U^3(c|s(4vx3lM#*SPTzP`J2hanT6y)<3jNMtFeLKw^6s-|9N8B@Q&W>$nrBK+ zA`_~b#ukg2H$b09M@J)VZEfEC{CpGp+d-zXy_)|gKy!05p8`rp5L7=^KR-Xu>(*#A zk-EA%=fkvJOdl{E&Bk~~0Hsptb~>HBVoFMHK9sb+zTTOWlOvfHXH#`n0F8}}Zi~f& wfq{WYZEdYHH#b*S)7=30e@0BgL78sX-$OZ87=>`i1ONa407*qoM6N<$g6NH5LI3~& literal 0 HcmV?d00001 diff --git a/app/images/copy@2x.png b/app/images/copy@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f4ebabba10053c30ac43e334af24c4f3422ea3d0 GIT binary patch literal 2219 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 literal 0 HcmV?d00001 diff --git a/app/images/cut@2x.png b/app/images/cut@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5a5da4fd30f82a92e4f788ee0716a6754f1d283d GIT binary patch literal 2888 zcmZ{m2Tar77RP_IAjnkqP=@R>#j?tfJz8ejLnyQbSprh_A1NyOXUX0m$`IMIWDi9_ zKtTLk_K+=m6xq;+dC5!OOWrx>o^NtaJ~_F$xygw#G18@@=A;GyfKFde3wfy`e-71^ z%N%>r=yfT)Xg#DM0E7qu0RAZeoLrXhzW~5r8UWT%007GX05+eTW>e+M#uaCTt`I0n|g)Y|Inz)E1C=I&M6h$!9PFmW@Rt<#F>j5kACvp<6a# zm$Qdkn0tG2WFUgyTN1DSF}E$5EMs- z<9YW57rxK`tE^KE4&{q`E#o`?M^WlgL-J+Wc-D`Wf&gFwW` zHh_8b_F?dWvO{FM-oTjJ(~k3itO!3ojQ&Ry(pkDpPnc-%;t)*6>n{bctY>83%Ijyk zx|EUUfY~{lq;)*wPoKSs4VH}S&Cf3#2I>IqL(qc?JiHU0cw1dEo8_OVR(P6oC(wI@ zpuN|aZ=VxaLayGq^#kBmBX*EOLAfbuULQSc-8Ka|lL@8ZjZX5C!r24_<=1fxeZ9^H z_=(L>-_&hbAo8|P!jQ_shgE_NEi5t*364+bje_+kQS%5ViT5 z*P=|4LxE++O+k0>Q&zJ#`GXacV>X-a6N@z&23eVjuNBU>JLvbxhW&lm@j^d+U(tW7 zPP+QtBMQZ=(7#`QH(5Wmm~rkI#=^_c%Oktwi)RIebcR76MpCK(DGJDX7&{Uisv2C- zx~|g)Dh%&a*l`l8k8{{L*%gTMQVxJZ18XO<^b7A_3`fu0oiF+*e+(yNjF}KT{nFhi zc*XiZozM;fYe9 zLKdXe9q%JIG~V0v@z}%6wv%>U%wHbb`9qe;FL^rR4L!}3Wa=U~psrTNG~PVfUmh-I zw%~+v{>{VxuC~0~b7=YW{pMe1t5x#cQ@(lX4ma&%yV>nuz-NQE6iFYKXo?s7{!!^X z4W!B7&EqOcAc+Y(hCaJT3HK<4$SUPna__Mc3)&CzKjXe~7cq(S4L@}d^8d(m<&QE0mcP9p|~^1jP|#VOZNCLiE6ZNC2QL7B(6wfU;(KMDpq z*HOyX5~b*9yD7ukqb?#od?0S;tnqW)1;v7QxdykcBv^pM3>=jv66t~ueqqpoQRBe7 z7XJcC6UR~Wa#lO{_4Rv`%`IMsqZV!$G7QJ#>c$&zfg(SCG{XCaO29dM`}3WMx%T8v zLDZb3ZXivNW)JE3)X~3=#m?T2)14MzFi(lDK0mc1R=>@ka%PU%bV05{0ULc68RpcP z4KAaztuk3N<1R~8z+n7@X#ZWtY59t%W zHcGKFJQs@St=Cxys2?Lwc{O5*s)5dp??_ih8(#|ESyMCpO;dr3u4@ayD$S1NNH(0#WAPCaD2GRQN=p4cEl!T`u^tp5)aZC$+qbRINTB}v zp?YKZicvwY!$zhJeNY%UOwlqVLuqX}zY+|=t8}Wv0$^@ib*9sp!cP^NNf>5ovJzg8 zMA2c7r@c6`Y~Bw|4QWWyS*W9X$UK3tMEr&5Z{~uvyuW))s0wbL4q&0m5NCZz6={}E z$v7J}?{}Bt;sbo<^xMHt9dh~C1fEa#F0k*nr+d+kZZ7Eb+?u{y(XsFZioZ1d3}O#< zE~0J|QlyHj9^e*P@)HvIm|_eU!8F24O}**XB_LJxl4b`z;bI}(XK140l+F`%!M+-e z!4>j`uF47GY7YcG{1&PzXKPIn8_tfBwVmdN25uBow8WfPGO*MOL1BX92C;gm6ze1Q zh)64i!x7LN-o|QUzcI`1W&}t>j?+P0U_p0j>DX!~o~{ZO!y;2+qRgI=m)rPArEJ8O z@%1aARH^$~6_!|}n0CSNegEbum*iLoat9C7K$vOEo7>fI@ zE^3LZL2h?{pSrxFu;R&R2l0T*^*VE0*789B8WEb?zd2&`Y{6IbUC?9Po>c4Ee6O9S ztM%$uH(OQQQjf6E(;TFPNL>h4F7-Qmy?fE*x<55_wM;5pktwQ?IbQ2bEiK#D^7@e+ zWyi3-rd`Q>>C))M$0>?oi|np1&T_0sJ06w$?S=x@z{|oSqN$zhp-w0wyNS2|Yg`Kql=*EU8)=N-)fCp4pc2SD=HmQ$K zlRTgDgr8oYQCVJ~C!j14YTh!;%JPIj-`!72uB{v%Q9|kcW##Use!ohJH~hvm$~2gt z^w-`Yv&=KmO6ha`HG%bKRr(>P&iE#ogKpU5X68OObix=pJ24PZ4C|Y0P>>&!=+knT z58;L8s)nBkR@#Tq>4En+0dEl+1Bljchzz5xH7$8$kyc-!c`3ArJAOibj3;aNRM?V% z3ocU{+j8OG<3tbyMC51m0lA$tmF zuoO7TA3xnH@A^H_Cw}{TWYqmzm(QW~crNl-LN4CwCPLszIn$Eq)9=XJYlm zMXWj$f@@-om&{AQCipD|6cG0}ul^oRTA3;5C5+DzMwB^sm71cM8n7j2Wvim6)HJ)m z58bC7m-~@bfIw{fn2Fl!|JaId+fWJ8d;0*!%d8|}Sp-VeP$Of;=;_N0V<7!YhmX4I z=k5*@ms@zQZ&^;iJ(NV9EK@ytr!qx)N--AO^_yb<3T6IV^)w`=wzh8ji0|5p-RX7^ zxGBB)w;U5?NCb!4=RT%qpZeOP{Ni8!P#x)M)+d3GX}rpg=g8*E4N(xMZH>b?;jl3D zL+qshGSV`4B&07x(Og;{CM^e(y(1l_Hy3)T&&i!<>FNz zgzWc<#bi01EiPX5fBW_Q(D!@m&3G{#&zFnhRe!PU|Mm5Ym;W^!_I@Z9#cI4+u8wHRXlz8LL10~ z2!tHh^ji^2yvCQs z`f5BW)}xBf?dz5XkuaOS>i;ypD^?%N{LO5-`9#Ia$LyzKcJXPG@}FnL&D-VetG?{X z9ua$0!IC(i&zCo^`k!a(+1b48u2_sIIXoLre!o~PuNTzWqPXd`8naZ#JX#XyYE`U@ z)#tK#1;Fvcu%$p1iz=z=McVvjT6`|%&9>9!gwPA z#%roS`fW|TN7M1=*>rSTtbgAuuSRQ{if7B)<`|aOo2%>1$KrNVEXu)<4w6D*aoN=a zvB{u_Gjw=9n-?_JJ$;WJ1R*SyxLRCnWp+EgB0Ez`n61TksrdEFX>q=;tbx-YnM%f6 zj8{Lb#?u+~#^q&=CLPf^s=THV#wv{E=59_xFljhh&dEc4hlZc;BWX#ho^9@q;ro8o zVtIbPF38R#Q>u<417aWgz0uwdWc=*y0Je1CA$Blo%C%koJN2p`*kwv}&*_#Z-q$I2 zK>bKvl>@i9&s^Ly`)x7Xkc+t{Eq`Fc|J#BT-p@k}*nj=vPbCJ8u>)y$J0RZsH$Ijo zbV<$(8<%Bs;@|nuWFw?e#%N>n{6dc3o40RH-gNj`hgn_05f!+6PRsryrnw&LX>*#($M_LquaY0>7Wys^a*aDjfc0k+AY zAM2`;ulaQ~UaV4~gWSZB^P)qze&*uR8+jT}oxI;fTpJtn4|9V4kX4#v4?Dwm1yS8YZ z-xRA@IjzgYcV0}W(Rh9{zFXHIDXTkn($u#2VSK$_&&G?l^K0g@n@f^ldRly*jW=<7 zqJ;O-_~05Q)@1DvLOJ}h|JN@k^V!ueIb!j&h<62Rfo{h9|Ji*ro+geziVp5fOSXTb7``bhqkN60C+sS*n^Z( zKTx;#+8QH^*lo%Xm6#Z?rUbl#?6Iy>t6(#;go?I!w%vYhGCn`g`L%8bC}qZVWjj#X zcD90#^lSTVJ)XIaKdlNnBYw99r#y&GJ;7J8lSAbND6M^cYY*3QYCT{@?B8OeG4Jl}tF2YR;^o||I&To2BP@%&(R z$v7nto6LPs^08Xzx}vTlI*%2sP&!y!(bzSi@I*m2(&YE{nIZ@s6rv0RDU@=`yD%6^ zfyBxvJ5a(HkKQBME21!fNT#b4Fx{x5Kd|weV0^~-O|W}hu-1O^eBlXc$eQ%3zTz1tOu~@LzG~Wlx7PE^jz4wJqg2L@GP`0-CR=Q} zHDU6cX~sNptDojc$@lR`-~avjryoy0s*g|FaAa{$I!Ve(ayAgiALrk_k+C#kqS|mJ zf%9xmPTFjALg&Daq(m`%%o>wY0iFnP&;m8qaR#Y~q+JbbZ-qf)6X4v;ptU6Gp~M2Y zQ!ad}T&5uswxsdaG^Jt6A*F$x(wGBE<3vO!31gUWNLkcQS=>`)u?<;t{=IT~_k2KU zb_)}}nTH|~(mwj@MDQQ8yJy~$9%cg}g7Z$H9!P#f@Ia3mfItgQYC|coNRK|MZN)*V z+9n)836#3F#Rd#mU`#idJLSB!g)~U`8!Kg8V8$jn~OLrkaQG^xbb-vdE#7SQkSJ2>kJNIh3yn0LlwhANwOu z0EZV$N4WEXng7W9z30zZspX+&tQ0|#INrmqDJ!+APuXXCnsihqbCAL~4FOGrKgF0R z0otQ?P@&eTn*vZ$<-@tK02rJ%J6)Kqa5D~g{b_l_aPfivx_Cfu%}6XRs2_cG+os+% zsm0~heE|uXn6h%#iESE9EE%3rglkz!OHL{yex(&|?l|uG_KqpFdB$0#W#te$<4m-> zI;}azXwqF{rkJ+NVl$qOH{(i~RlW?%Kfo4>7Q7qdmF3F@`#W_h)l zUCzG5)%-)T*${F`)XkW}*EbcJ`86plOF0aVbbe0`ZA?S?<-dPWy38<|_q=M<$cSbp zX^PBsa#u5+a)VIt7GgYKmo#dzDNf2sSPxTNEPqZVsXiGXv6zo1iAx*>qud+^k(rnT zeUAKKgLP<>fj=S;OwL*fl~pt4C0)=s{-4wL6c08z{>O6ldpRAMIzC%o6BrzoJ~=)w z`oU*($@f1#@aFGs0>>##@Q<`sigQC_ClcK?L_UCy|{=);D>y?$iA<+Q&a2c;f{ zHmYX%>lZJ&ThQnS(hJx63((H%&(_P&7kk!!2L1`(-AQTCoi@L8tA}Q{Vy(eIjuUu` zJ~TP`JSP$+$BBwg-uMP5CG(tu)j3W+ym^0uO-?Gy3DCG4r?+@=a@rb`&U12@<#ejw zsdu5tDUZ04A!I|vPu{Edrpd|YIcYT6uU-kJ_f0E6ndjt`+8^4TIH~*C?&dVuH!=Lv zCPp~)%rIz-aZO2FiOF`I16m8(nC0ZP4*4q;9h{8GaDproplORuCN68(-E$!~N-+of4{_~~f=)m| zJDi{uOt2NECuq&8ry|7D#bA?(Zd~hlEJ7fk1d*wsL;bfS6Pj@eUk^+;05A@o2Minn zm=xrYLL9yt7H6Ld3kH0PzRAQt4;J+#SS%+~#6&w@q8Tlb2PS`Ut|61pJ1r@l+JAR0 zaWZ*M*5UqJbBUANu?f;{|DCzSX~!}FVD{daOPm5^IiUo#|Gr$}lt)~HQu}YqB~E$7 zjdgqO$~8IV5Qitsu@ExTtYRM`KVgn!%3kDN0HTL%Xc19gSFP($6ZJ?4m1viK?LIwk&-A- zfkCZGCg}~%a$4|VkNn8w{=?u`NO!LVb%SM&k_I3;6G;Gj0wCoGRLVQY2vzwL&O7;# zcb-v=IS7n1Q5g7&QYi&WrPv3hK&cdauN3OrFyAU!DWyFo&$_7_6Y4`lqdGUH5>mbl zmNEliDA?36XdO5U{sA{Bw3Z%ilNJN3qBMyLPRK|-X;F=pVCo%!ulFALI4gVrryb`@ z&43PIfb*Qw609xx`CtMv&3JTDN16kL$m5ZL(x{Mu*B%t{Vww+DS*EGrNEVQsmf9$X zWljqkjx;7}O4ejN;e5g&ax)eT5-nz=0_8dLQv=hsa!p9Ot1G#DS2|uzwoOymF-fJc zUTTLJDDHW+{JkK1Srqr_@(PdMR_a9YNVAn>kZI9!he5e!uxtuMU)XN#abDGhw zV_ngvV|v@aT8-}-y)rdAT&gL+$odCuRcoi4;rZO06-t@ryhz{q)rskHV&h_>@-5mO z)6zU(<&j$7^ys(pJ<=&XPIYKi#V=VO2zZ?uUiqR3WKda&Duynk~tu|^g+O$_wpfvGkKO zF=b;JNolyn{lbw}jc+TZX-nzleq9=50&2%ndPy>wD5YUZSniU-KRFXqHkPrJ+#>2S z`scvxo-O4L$y{49kWdq?-Q`J}5Tr1J=%0ImwZm121rY;PYz?pjRGUC;RJ3UC@3CMn zZ;9CIx0S6VA6?LKDqW@SUZc)jq3&Lv&R?D0533JgyPJS?b#Uu->25D+EbS}OjqA~9 zJW#ND#zhXL@WyLGRSJbt#w}t>reZC<&;+R&pdp&n+8sS(_2h7HV>D`Cn@`=4qBcB# zsO1zLww6m(!FRf+rnV%j>Bl zNQ^Q{C)>zup>n$S_=nMbQi1n`vXJO#FUuJ%%hN|nprS;u)$eBYD)Z&RYF5RB19RQJ z!wG94`MdbHCx)z)SN#Zn{clVh?e!4DtuA7lhp>E7Fg_f$e35p}W*k=XW?fg;b%)7S zb{$OVS)$@HmK0_0MmZlft;-5$06lp36ieAZ{~dxm+OdRWSDgIHlvc3(tYc*Idl(Hn z&ZR+#XyR_*1!%9NQ@r*{VU0!wHL3#v4ue($umVi*fIaJl_dX~bC@Z|OT5)TKLTfZ$ z4-f=3MQ}Y&{dmJKX^^l$XAn}~iFUt*2%uG+3Lu3sR)-o#026okrqMTm;tc58C_$n? zZJbHcY?b><%cQ|NXAf|Ue6q{?cY3K2uF0~N_L+#a$to?>((C6+FHQ9`3Bzp|L)Sc$B)V8M`!&SVsLrC z=6x@M4XT8`hgjYgWA7FOO7z+>(R(93@?pkm0ANyJ6$Mo#sYS3NNGDS%J1HHEjC_?s m7c9|?&|G-0`s#OcASu^=9f?6kY&fXr^}hfeBu$70R{#K`5iaEb literal 0 HcmV?d00001 diff --git a/app/images/go-next-view-page.png b/app/images/go-next-view-page.png new file mode 100644 index 0000000000000000000000000000000000000000..bd2a33836e1437961d4b636216d567ca7090f8a9 GIT binary patch literal 1125 zcmZ`$drXs86u)4Ap+msIK&D|+BsjxTKtQ@N(NKXX$jgnng$WeKV+<%A41tdFkcaY! zqzhxjNkkC>lCdd+(7Ax6@@h*Tw6y)`lTk`}TE4F>uib9Rmi@8cJ?H-Jx##}w$;qjW zjta9o=ynhQU>6Y{8VlLU?5`bw`_RWL=OMkF5*`}~z)ep8$|?ac!K*S20AJ$((B1$5 z-vWS3Zrf1I2LMU15 zf*^Xm9wyAe=H})rGxS?#peTkyV3Y!BU-6kCWm4@w$=2!rkh)P=eOeS(JtMJ&S-$kxV^o-Wk72NslB|?C;Xy{ zUsLXnwJvYyc6N4916JCXPh&(sU|(xz#a9eQR6Wg;E=n;J+uhxTWk?;LQ|U)&(?^>p z-EKa0$!ZUqLZ%IvVQ+7bbpLuY%eGs2h&p6n^yo9bLNGr+Z#0=Oqbcu)kD6Xu7x%fB zw-+ufEyDJas^b!>aNWZ_a}uQC$GctqJv{!bNGzF`$>ef5V{DAg=CIl98Sab}K^Tm& zg$4EOtUxA{aX2$vE|({mRVZQi>fvEJi^UR&gebbj=L@E$r(cebBgnjIdmH9Q$CyHq zP_9s@)T%l0oJ=lTSXfXhRYI{uEJcRtbb)|B!{alUOdgL13(tuq6D-b-iO>coZIOIA zEY!-7v&4jfZEpA{`HoG;== zLL98+D)kFX*C>x$UT*cC=wVdkUl3fp=KNFg=zD15w|#|oSO!W-T44cd^*h)&n<_L^2LB#YgOOi>M zZjL7jJ6XiEg0Co4N{26TvQkr?m6uUO@O(j^h)#L@jj&|-tpvt!=^G(byA$4-lLPe8 z%XHxAmlc%MMLFSLL&I2;~_yVvrt>OX?)9AZXV(fLh#*9TKDl_K{7*UuYGwcc literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5ddcbbcc98331691dcd45b2f1764050dc6ba273e GIT binary patch literal 2312 zcmZ`)c{J2-7ysHvp-B=NYlWdQgvO*|vW`X?lF(RYY%w*q{A8O*cF`!Q328J@wya}` ztR)NyVF+dC{o2>?o8NiQdEfK?@t)`2=kuJ;eLm-&d!KWj8)IQ^EWA%@9{>QtrX~hB z4l4iS5Pr@czgt1#fE>XDhXsIpiU2@)003JYiZTTN{s;h=aRdO=GXOY3rd3|mdZ(@H5343QY0{@{7V7*5ukbOt-0lzLiZw zP1+SN*mdcai(vcQoNPhOU0UO9(em5UZZ+mK@$zhW9pH)bp2%T{yV_wx?wDNIXdRcl zi2c2>&-Z!8a5=dY1>U@*4PUs`&Y{wrZ@uQT{Ev}g)x)d1i+{Ne%Zkk21QP@1um+o^ zuhRmDLzdG(a61T`FNTEI_1@Ad>HSk%?|;+C*w`E9w#VWyr}5P$id22fW=w-eUQAYW zQ}6$y7A zSP2uZsH}{|sdPp3+QzG=V6sp1Q8OO{Rrt46d9Nu+5P3webCI+j`~;OsZEJ5&W=$3e zQox(l(YV~}`jvm=A7>IU>pUswrLL_g`~3F{BSlegqLKPthVla_1Rk&zQ%n2h_b?OB zw|-SkOr zcNCkxJQ9&9hOhn`R9L8Ieoj-fQacI674UOb=ituf5)6t|&j_=${Azj;O;{+&#%GT$qXITy%I(% z)zWKTkGxYiM#4ze%hf$@Oulge^w5pZglj6-mVh|kNdc_&83cM zO9*iQcXknXR9BPgbiB4+ZEqK*R=uW>9J7`` zCh|iDKd4$WL^86Qd2yYIvr~N0k+iA{YvG%P!p)$!%DAy8l4;e`vls8h6|nbCRMYy- zTvU34%t2GF;-jp^oiv$Vdae7(xO2nbo1N({H525~hzMQsoY2L;=5Fj`*5~iu#rm&s zF%KQa67@Wnbo3REnK1Zp;k||ZCDqK|OS|7fbiLQMbyt~6k{!PTuu)Z(Ff(wzt_dvF zhU9%A@?N?W9&A2>BCqC-UnwA3w6hk9@9ajiPSY;Q75*@n-LYi11Vq%G5Z1De=;1e} zq>BFjwlHD0P!AeElL<_j{J4E6Hh|?EKW%lpc52ZP$Bv0LlDtgp9KgMkz;C@AnJ))_ zO}C_v-fu8Wl&*LOI%^fhu4CpiGka~{mzv0TC3BN1J~(gu`s%UNaU*oG?NQ)Vqr#El zKmh_NYilT^a_&$>c-i$&ww{G~H-5K<>86KWXzAZ<*&L;^eAXMqLZMj+l2gXLr|SmO z;FW{%>JkJC%)y89ws523W3aaL5xa~%l*8`I5mU)jyK#-sw`6;PvrnX^ZUm_yA?|ie z zrVq7v*8@-Ovr$c543UK&PoGrR*2ee~c(S#W%uY17wAl4j?d+_@mE8j3ha_*Yk&SZA z^VS75mYrVOIaaFnwcZhErw2g{(1niRwz0Oh?zHv26`EdDq@IFv&pT&))Ed$?Fq9-R z&B=Gml@P7oCN5&MQM5ME9^9oB=xSZZ1{g;kMqPeogu8MK$)`voQ z8`mLk|75vwDIYO)%$yBK8Kyt_%by$|G~@J>D=FoIjRa7mB%i z#8k|X&K468iC-KwPAz;YQ=uHY5R#FQpwf58-dMH0v5}WRc>Q}h@OIwN*i5NsTL)}1 zJ?LN4b0tGX;X^96gU_)CU@%7I)u!afk(T>^FK?P3G`MQ;x{Gc*lxaKUIQrex)$P%* z?F*{11?2ewt=M9vXV0I{^z+YE*+i>8{jQj8U=rhJ?-Qwxu8m1uQWlRB5*GF!8Emj9 z?x$N9yiRc@5+AfnIIc8x~AQ$dZdh# zjeN#eoUV74o8{YI-ScBV*lZ#)qs1&JI5;q}r{~(l1O|5gUP(cMJ!_c^iy9vvZ{+G< z4>|4dv2A5VGk@^ZJx^)3_by)U(P?ShpNSeT)$^*7vPiO0d1*A-$kvR?+1qTo)$_^E z$;V=}efCFkGp=Nqvl)lboVSY3ipJ#_sAxx(v2v_byjlIWaa!Uep<|KU;$H^_q_UZ! zoKX?9#eVWZOM3qBw4W4f88umPc74ffHpv8TGWn2Pt@#nOJlWyBvEZ~8cmLfFiAzvk zX~QuK@19rVmf4#3aqcY*D7b-y#Yu5V*^9_dUjh6{QQs%gRXW5hC%c%A* z-`I6zV2=;7In|AQk#K-B@lW^}+V~Nj{9I539~TY)8VHRurw|-NTB)B!X>d$i6@fq@ z5b4jdBmYN0x1rH({ts{cF5c_4#} PqXA3}%?;M1%-+lUV_vy#)KYoAx{(Jr5yGst- zpS5d&;M6%V=Ml*R$=+^%r$Bb~PMicCXWHXgiDI&o{(gkxF#N7H)_CARI2tlt(`w#F@MzE$i*gW#5gs)gsSKl$|e zYvt4(B{RO>_45Wc60EjEak7aXC5R0IS5z8HPt9B5vNWyufO?b4O*x&mR|ASXo+I zTwRU^upU)aQC+$uH6@Vc=@U_rsZ&Bju3iyrlA0BD%W7BEFSa(fV;qjcnZd@z+}_&R z($UsC7VK?aJb82X>e;*7mrpkjxp4gYd3yx|4GR?$9UCPfH3?B!VQKOC6J|`AGl}c+ zylL|$&J>+Jck=A%@eJEF)^1p_W672^dlqftdbs4(tXsQ&wH;fQmU4XimTgbtZAHB oQ;SOya|;l9EJF;;tc(n-ObsBG+9#9X1r>mdKI;Vst03yNaga7~l literal 0 HcmV?d00001 diff --git a/app/images/go-next-view@2x.png b/app/images/go-next-view@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..919406437ce9f3671040c8088d787a3d2adbd199 GIT binary patch literal 1830 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 Xw6srdKjg2a<&XadOC(EQW`h6#G`fNk literal 0 HcmV?d00001 diff --git a/app/images/go-previous-view-page.png b/app/images/go-previous-view-page.png new file mode 100644 index 0000000000000000000000000000000000000000..ecd3768ef5a49efe78ed3d3f8e39bf85460c8269 GIT binary patch literal 1119 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 literal 0 HcmV?d00001 diff --git a/app/images/go-previous-view.png b/app/images/go-previous-view.png new file mode 100644 index 0000000000000000000000000000000000000000..086bd9a1c262e0556397df322066b3f815468907 GIT binary patch literal 1010 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa3dtTpz6=aiY77hwEes65fI;M1%fByP&@9D?4AHVcjV5z59Ch#;fK!2&-YfI=Txni-@bSwJfB*eEaQ;c@)cp+$4oq5qa`ML0 zQ#PNSy5-!|E$3@z?|uB@!~4%aXKlQg*0HgA=Ju9F``VWs?p}4GZ|&*+^=JFnpR1g{ z>+y^CSMR?mowO^uW>IeU%F@Z3s%PzLTyU^$*@>=IXL{D2?^%1HV%o09&)*%r_#~us zQDFY0=!zN1jf-=-*OpA#Q8VXI^O92?E6;bYxzw}vO6BxD7jC|Yty*LjTI(3!8eB9p zv2jIi-}Z`Ghnp6k=~{icf5VN5n{L(4+W+|Z+b1tSRP?XY@u@NhYIaGR5mvb-t!r=T zj8jcZu5_=x)3@P4)r-VZ1r#&+!*(Fa4C|i@zwx?j~srtp&`!+tFvi()f zoFk8)y}oqwQBK=ZpW-En?RyKSo@!VElzlRF$E)hOM;<+U{qDo($xC;JRxeHOKU_KY zQuDG~ZL98;%-sLz*{i>Q|L!?@A*p$BLeHMkS?9}UU&x-kCv;7-@pG@v|&$R_3VWHHP2qXxpn{X`8$t(|M}y8&F&sB5illsySp%Su*!M>Ih+L^ zk;On7m>_`oL$=d-AcMWc)7O>#IWs4lh>`qBzvV!oYo0ESArhC96BJk^X3mJba5UoP z4NHrQM|SL}toZpOu!V)CwZ+xt=#hy>RaBNPNli(8@{zm8jm*iJqEkb!nrcZI>mf7EA&IHV}&bK=!!N9Qm zhS98xs(I-^AF7tPMwFx^mZVxG7o`Fz1|tI_V_gFvvIsFWw=x1EGi?I{D+7ahGnTfZ zXvob^$xN%nt>OJqSxcY>4Y&;@nYpROC5gEO2tAe|24+@923Dp95KHd9*aV6!22WQ% Jmvv4FO#pv#{pSDx literal 0 HcmV?d00001 diff --git a/app/images/go-previous-view@2x.png b/app/images/go-previous-view@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..900860ce0be38a34d751115a5a42fbf42b4fee0e GIT binary patch literal 1875 zcmZ{ldpy$%AIE>v!Q73aL++i1vl_~n$#rgHN?Qzx#*n)$hK!n(%iK;Xv0N(;Ij&D7 zZK5qj$LMh_aw{f>STx7w*x7kKuh;XuoYNUSt_Ke_~*0Jx;1& zj58-L?MAwDq4+`D5Hv%^{Ov+k{vwb^uqgJE0~4!S89Fr^V=Fd%8$PQkojK$U=EtCo zb~yZnUN#)gXIEUQ&{-2uJu91!=~x-P)|iM`=(Lty8KT@8$vqyZ5z=38q5bhm(dZGV$1wD9UDlfq*T?+gy5PsW z1fSa^wQSb zSW}$nM#th(1Gv`czU~_a#+&6|HZ!RYDX|(@El^w^CQ1HWXIyq_H^x2G5t^g2Uqzzm zP7jvJYR@l`Vl`;duC z;OD}_{C_Q4Zf^W@$WNi}^+6m8dq!YYX{<}n@&OGE&&wVoXBm{Wh~~#A+g-MczWX%C za3F?3zBdw8d=(WK)!GYzOkgc6Ky$bY_K(84nYj)sB|vc2T-1-KpIg z{bku|nyiD#zAWOX(;8JGCxk_OPny}Es-!qLs@77^neleGQ zl(s@A6@mG$sk{urP1EU*X_@964jI~iTIo9?%eboMk5BS2T}7@zsd>E*(`Q)D<9WcZ z#MSkE;N4wEEh+cSZ?cYI!B_Mh-pEKgD&Mw~ca;ihn@O-cd2M@=t&T)dP`Omg^C$2{ zQtQ~}2S%U$3nt%|^Na_OBA+Q%oMOdr-IH_Sk&!izAG@Pb;kN+-atd9Qv#^3v!_m$- zDr@W`z?|Pz*Q2inVHcES2G2p;(tba`(Qux}s}*qt-Deq$QH{d|b=ZgDb=ZGkA7LNi zO8voperA4tmCxKDcjyUtiL=WPS;(Plq;g9^h+NA^wv5h4ipkT@mPFdCV9Q^y^!(B8Hqr10=3_TS_4$~aYVaHP zK6k_(EbTjTs_nZ3_1k!Dc+-W-rd1e$nlqf-;F(20t9hAds2&9!$}8}fOZ?sv|K|!) zz+Q>MPkV}rBw2}D@r!WqX`O+Ol?{Xw762-%pt@coEu8&2~k`tEs*|$X7R&p6-iD^HR5+ z5*+E=o?*-DBp1C8m*6hWR+Rg9)&rjrMr6NEG%KErZ5pVS;EnC2XH(fioy)`H$7#aRZ1M9r zLPq(LLxRbn2tq=rSO7DanK=|Dc1x@&9APGQ3lkU&0fUv7JfQy%5FHy5K}`Pt!0M~R wR$>71p9|!O=ui?lI6CgXj1?AUfiSg1oG|_O5gVXFLd*beD2!91L(sLq0FMb*D*ylh literal 0 HcmV?d00001 diff --git a/app/images/go-previous.svgz b/app/images/go-previous.svgz new file mode 100644 index 0000000000000000000000000000000000000000..971b1d98975babba9d6a4ec572fd81c4021cdb56 GIT binary patch literal 5690 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 DJC_X` literal 0 HcmV?d00001 diff --git a/app/images/open.png b/app/images/open.png new file mode 100644 index 0000000000000000000000000000000000000000..45fa2883a71fcb891f1ef7c0c217d71eeae284bc GIT binary patch literal 2073 zcmV+!2RP)CWtKz1rEEwa&Ubd$6s{da=&VIP1;2`BM0`Wb;D z{M-!)vd0-ix#k=pmKgFxQgYk(eOi$0QlJD2NbOI5d5567avIIj< zv@<}9dj`!tE;Q@xs8{wNhf{?zUNu9!u=J3D!C+h_ze{}K1V|htI6?j0>T@UYxhDij zhnKMl|M#(rT!AcZ}eF)wgP_d zAX10jekLr}U#(CrCA<(opAbNs2#S{v>vbjZBPMG2>Dlh2!oC#Mk@9m9Zc z2%^^3CBjkLC=!RpAFqNhVw6pZB#Q!^oqj*T-F(Vw$mLX*nO_ zNX-3DeeTpEomGUYnz~IsN92z9JUkMF)7=lf!HB}*Vw`?29z6pA^e+N~guKQ7E$%u) zC{dC3_66}y+`oN81yr%;K@{IH8|1O-3y*riL6}qGd@SEo}8X#Q_kq zZ|eG3QMpHgF(8Pc07FLLD$kpMaaUZZl&Qk=tr>Kq76&g@g}Kbh`}mD}bLg zSob6#9DIaI(-=7VVTPPm{Z+FbtPVT4Iv?2~9(pg}6TG@z0w%t}gOqRBIG>z~1AjdP zW3QR%ZYsA%*Mp?X$w>b?i;UGgQ?+sSsoXX8X`%zES@sOGBngFV;lVB@d3_10@@V;lg>dY2BN>v^RZ2^?j@k8NdQ$X;_ z_a$H}ZfeJ=gha#~If~qTHdcoJ4Lhqwh-3dbkKgY5JsSA}oWGEOlkpdDqpb%S#7{~T>2A7M zsP8QLl$cN`w?OF*Jo#7bDJpj@!Dj^fwz=d)jlRsXTKwbW85}(HH?VT^u<$rb~rBzaiB|lB=W)A?(%51dO$!?mU^ ze8O#IHd*n50bG}w!BKl4^90wps7K`#)>4RXJ_(`usyx1v$_9j`-;Pq{6K%jV&lNRO%>FPb&x$A5bBEmwG~ zsNAo;tlZJ!2;Bu-bsyf#uSL0b05WG_^}?r@soeJn(y81#_yhC8@e_i|9PXV^3ptdy z4Z_G4qZfz6FCzC2|JrSe-C>#48@AYMyIwNBFNyyFX1QPd5r93<00000NkvXXu0mjf DP*&js literal 0 HcmV?d00001 diff --git a/app/images/open@2x.png b/app/images/open@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..12c2c3c1d8048bd33e8097d7f88e43e176649fad GIT binary patch literal 1017 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU0wmSG7d!(}3dtTpz6=aiY77hwEes65fIh$n-s_6CPdGypbNyS<$&D@<4ViMGN!hLG1 zOUR0=+k%#r&Ffyd?ejhDnbuY3vd%sE_pb8$x!U)nKFw27d}gt(Y>&YMk2P_M;xU17OQ&Wk-}@SSD`w*LV74zaI&wpe{KIvxURoCN z%jejR=Qhc;ubxi&yrD_2`P)pdG7EuyKce=k23?Ae&0EpD>PYGP-CE0E|7V(M>i@^* zuXsVA%b)NGdrvm-%6*)$Ep(r5_IAFXOSg&nN`3mAHT%+w$mCAry3ab&TaEV~o)f-j z8Snm?f>*aNoXy*QeD=!+pV_wjI9YAyXP?m7vBhfD{p8FCcjK-;nk3G{ntO>y>{40f z`n6)bY(=w69;XQ9eA&_?Un6wnl ztQbSF8~4lRYRu|y-v07vxEk|8yKG-cMZR|%SY6GVc9eelvF+1=Rcyc46rRmUsE^%c zkYdKSZu)V}=S&CpM4y#dR};Z$mmuAChP5#J?a|2PlN0Xg*8NXkHf!bxQ+Ls`$_Y!UXSkNJJ#pfu>EY9i`7D+lU1KU$ zvo1sNYwQ{;rO4W%{?M;#-DqPyT#Vubp*TQ~u@j?&Nyr6}}3WT&p#N zfyrF8#5JNMC9x#cD!C{XNHG{07#Zst0FgzAp}Ccjp_PfHwt<0_fx+U1Yu2D>$jwj5 zOsmAL;nE)w0iXsAxD6$lxv9k^iMa&`J(eK`W>$t4RwjlJOKchDGXnK6c)I$ztaD0e F0s!_Ru|EI+ literal 0 HcmV?d00001 diff --git a/app/images/paste.png b/app/images/paste.png new file mode 100644 index 0000000000000000000000000000000000000000..c14425cad1ff1b2c5628be5769c9e9e52b78635f GIT binary patch literal 1645 zcmV-z29o)SP)3Gx zefpoBhy8x@pP6$JN-2UQgm?vbp2urQDfM^OC?_DeL%RWJmr}mQV`N#@2O9RfK(_`t z{}ccw{I~7(;$U4wL~hQBiYr&HP+e1vKmedn2xTZiDQ44YUa1!^ytGbiJMka6M}vG@ zLrgqP2loG9i`^_G_)0Q7JWOR}B`e8wT^_)0sjwm&qAGGfr|N;V0|+ezNTCh%UA#yx zm%}d>QA&}|=UH4_q);eO)MK@^wUhx^fcS#;fB^tDLmDAYR7=GY}hKA6kOPAcP zuC561)~X!<6GT-k!gJLz@}5tjD3MBG2|?Jgh=y$Das?8ijz6FOD-BH>0V2R=mW1#7 zc%Dafb+w*QOZ4j1tA~UTD5c(75x}xR0JbfV+O`xp2&58N!i*b_Ou6iP^*huzHW7R; z!Gvcr8OqDc&1OD!?ARu~e-17|ECJBj*{Kd4I;3nN2w4!eEgZ+jLQu5L8XEw|vWPgA zt^#0Lco4wc+#EGEH3lFWjdJA3kD9q>HlpG95}EZ=m1y-ptN)rjt$|Ez*CB`q|1$rgVVB? zrhTuowc{78+xt2VFSK*}&LsW){Unn~lv3vU`0?ZDx~wzj&Yk1v(W4wbe3;(eUJ{9f z1T3Ky+yU+X&gS^%luv8N&-msq-zN6;H<^5HH8r836d;2i zMi$(072p5qZ>Vh8NaE}}Opji}iIlOmc?X+Yw)07OC1VMf&t~%6oz60$?T8IbC+FC= zE6VyM05f|%YwGIi0L*nNl`;TID-fW$T5#c=V<;@j=Em6&k5ZJ9d&7MsWd+;Xe@so= z%X~bXX3}54&3G)#YcF^p0JU^FLZOfeE#2xLjsYt8K48JdxS&uRBOHrUx22Yncy@gS z>8!_S|DTwyyiCjXgSZP}yn>`ylz7@9$3iWtfa7VPmemyj0M^GtgvulQziMM4o~aCT z;opPo+4&R7qYk+;M*}DWVQT;uOChBkB`_)#q+A6+Sfwh=^&|RlY=-e!kMI5ZkKD@C z;6z-a5gX5w6g>yi0i}>q@=$770agVN2uuwG7@cUt@bD?; zK3j0A)Rg|7%OH8oYxFCwW^r%olu$Hy-NXMuhl?5?;8OW$_d zPbP%{KnOr#TKwMNV7j}zJEh->%p}lW?Q_jx?V5KpZV_N`@cndOUtjvnnKP!vrO=Of r0*SSrV$g&__6vQz4|I&^4Jxf00000NkvXXu0mjfT=)$w literal 0 HcmV?d00001 diff --git a/app/images/paste@2x.png b/app/images/paste@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..360b0f6c750b1c963289f74a20776ff995d37810 GIT binary patch literal 2782 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 U6IkXkj==yHP-~MqBlm=V0nVE>SpWb4 literal 0 HcmV?d00001 diff --git a/app/images/print.png b/app/images/print.png new file mode 100644 index 0000000000000000000000000000000000000000..4ace261407961890d60a745b7a91477271830a53 GIT binary patch literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc0wmQNuC@Uwg=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G&@^*J&_}|`tWO>_%)qcw3xpXHm+QO- z3bL1Y`ns||XXa!RF_J&&x1522vDDMWF+}5h>*bBv!GR)dAMV%IF3dEVn#Aj2l=@+Kv>nj9rOV04VZfa7Jb1ucK)a00> ziD!DyUdbeXEopYcRbeTwyt48-yH9!eddeQ0nK$9a)hS(fOV*jsvKZ=#t-96`2W5U6m z>U_sOrM$TD`4;RHTsrXz^ZD*;`AgjTWc_^&#gsQKdAplqrF>rZvbw!_ceei%`8X;4zxlg4>p$=p zYn1#=QHgf~#))c)YeY#(Vo9o1a#1RfVlXl=GS)Q!B8w11b1Nf5D-%m?0|P4qgT)Ki ztU=L`o1c=IR*74~r9UD9Kn)sj8%i>BQ;SOya|;l9EJF;;tc(n-ObsBG+9#9X1 Mr>mdKI;Vst0PuhcAOHXW literal 0 HcmV?d00001 diff --git a/app/images/print2x.png b/app/images/print2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1c3655be6a157026165fa837dc155b2c9bc5b252 GIT binary patch literal 931 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU0wmSG7d!(}3dtTpz6=aiY77hwEes65fI|HFI6 z-G|BjB}ryX9+T61<<75|`Lg(?TY-aL=ETPg2e-Z47M`K7A}e61)+u=3}GsGDBJ+_F$=Gt`(JP9Ic(@I)hegSwnegq@5SW>$EtTd2zurtExPP&kI_2UiCmtU zAv2PMY-6u2v{Tpe5p?mFttwu8NFv2VkP z6UG~vV)+(3N~zcQw61Qu$)@vf!#Q=?hraJ(3i`KyY02NX!?95${zhO`#fjQ?g+9IV zdpiBOSeBmfn5NdmyLKHfi|?k18-t)D6m3WllF=B^Zeqc$NiKdq)e zH6eIvf^3af&EzQz4Z>xfuIqLl)vG_eZwrfs!*nTmCD&K6Kh!K9TFyL?D^qkL^G5#X zDPem&diA!QU%xW7x$ymzWuH9QwqzV-SMBfAbp7Q#@tKz7!wk(Ol_%fc_)#u*DQr2v z~Jp2r)FbGBUI>vD7v&ure@M zyl~AL6b-rgDVb@NxHVk*BO(CQpaHj`Br`X)xFj*R0HMb+#K6qT(89{Z5Mqff!+b`d O9tKZWKbLh*2~7Z1-*c1z literal 0 HcmV?d00001 diff --git a/app/images/qt-logo.png b/app/images/qt-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c9171422c2c068c7148d20645de5551437a6b2fa GIT binary patch literal 1483 zcmbW1X;70#7{_0(LPSPHL;@%g1n~|*MKT=WPC|(!B#E#fW83$ z+oJi;i1q-Wg7W#1><(>kmXQ8;IJ6O*+I+s_(AY&na*_CABt$nUC2jEP@6#`8XI@?z zF7TB4dr19R18D@=N!9>+XleBNXi3&^zG7)8eK0$q>vZqJ!1=cv&vvSC^5K)|I{P+n z!Gwtau3}+jQ9j?BtmKwYJaBCDacn!DJ6il|P8{45-M^?vR9wQf)2ctz-kYp~4pT68 ztKnk{xibXpiIdHDQ{~~r4o1Vr*JTs;2_5vFh5n059=4rUH`^qe@9JLYA6Xu!cQak3 zdMsw&w+CP(r0D)+KRLL*zK*%Oe=p=3oJAt| zLN4mE1?3e%#YKrf-}oAt2_s(R@pxBGC)T|B=DkX=m!})}o05AQnrO0c-tL+cYOo*l zQKoMQLI6;uB$25s5+fcF9UB!EaSlYV5~D#x0y7!_j$JFDcvX)fwGbRpi>1|Kln$zRO$&bbRf?|k&q`!CR zUrv%M``Yu&>y0}`Qs&T`c1(@DDTKEkeYrEoINz&)9%~jKN##D3N%7m)_OD@r6^Ij0 zqrouFYaP<)G|DK!R;1nJvuR|?SmaYW@9LSLeki}!#jH#yvwBr}+24Dv-mnpF;548U zP;n>uzH4O2dA%A~sNR^FMqNIm z_m!``n~El_{uKwzo{GTtvCqwD2^)peDs1|5M%B>crBJ?iR&PHqmj$j~7^Cwthws=Z z_%Y3{VpxUY(#^sL^^TfH81k*N%JGIAUk4296Kb&r`_nLMs$RF%^UF!1R$ZD{-{Xu? z-k-T{G&~GR}i|k*g9rmnrg%`<0*2!gjIk{r#-aG$1udZrUW8>rt#BpIjz~UYA zi=%3#c3Ws&o|KHh3KM<({HojE-()eYOieR6e9A0Uli%9n zT;)18L3cVUsbv-3U5>ENIk={$+i9}q5mzisJv$P)FmF1+hTdEY&dr%@C=Urjs33y@ zuHhCv0H$hfqrK1Aq$-ZBQDkRfQ6T^YY=KB1*tLzCdxAtF(4C1mnma|uN~~ruUXV}f zm34LYbacyY*|5)8JlqHeFR>~vE;@m2&;YOPuM{!Q%}j4M*}NCL$*(*as8mQCM4LH1 z#kc5#jxSQyRMN0}mFXN;m!?K@vvR?%#8OUPb#{mKqZq$WFkW8riqCEw-V%qi6t?2U z_|Gscj>e@7cMOKS5zF<`a#_uWWAjx24LcKsTI8N5(3LD$?o?JNg9V~PVnHYYC?v|} z01}F0WNTYA${KyxZXXhfMk32gMWTNJ%&5?au*ClcI#={{A;9Xp0xNAZkn^Cn;|6;zTZ-aEi zJv!zd=S@Ggqk4~VScd(M5Hh@Fwd3;!6;4CfE;ceaEa(8zoD6rP2PCjEZ77~DG&Fl} zHgzJ$s2_?Rj#xk3*gf9Ej>X~_c=q1h`_&J3M(%GPaW;>(9H@x&iM(iL%FN!}KKEex zV0CtHzI(mTp6Y{R5El2BJ?Md!6gSc)t#GCcLUut6-DU24N&fQ4X#f?&Om(GWZj)X6 zHpqtk4z^S;9FyQdzeCy_u%~*DaF|g{0%kZObMi^(Sp4ud<32Nmwl(ZKgf%BQI}IQk zms=eNe61+%ludd8`!|OH#NE*c%LglSdkY`F_O!2cF*sk2PmZ^awo@i@UN1FI?y^6w z6Yn#V*AF)jxks(59mt{3&ab_5`-^|AeRLc^SW`Sv!(kDO#BfI9{QeSVB+`ro8`>WI zW93f^vWp1`7CWBaw%XYv%iiFHKoJLXa0UQbXV(h~Z@3)e#vPdkqunj=QMNdHW$9;i z4bevPGDY4XpP#KIMB_@!j0mq0##!522+s?Mo|01U+5wSxsd9;hyW?P}Xi!*0Xh6_i z3<&oi3;f0tH*8Ij@nz{qLD{obJq;=$dM=H{Y{!)1w(gC%>dYFnxRTsc zN7}?h677c*kD+e8Z(Oe}m)Aa%IZw3l9Bf)7m)yC`Y?e^&eU@dL6`n`-XvLrwT(Q=# z>e7Eo!xpq>OiQHnzk_tAL=)p1i5kdor}rs~W@Edtbk->4u}X*hz*OQoSn`{tslGzl z-Wz`@<;dsT79b*&qwl$AyrYtB6uIAXP5j0|Uhl#%0m+{vVACtwaxr?f;$2Y5sU`VZ zXxWyzWsKkt`6Fh^<(1T{AIl!P*{^q8>Xel+W=ME8J?xR`B|TqHt(O`$;+xECP%JEEfOP9Sl33iv-qugKS3ecUMI! zJ1bZC@Q5~4{Y2vqy4XHzWGpimhdGX8BQnz`OLfMJ!@JFiq7|2RN(yT_jD&wg(J!pA zWD=xTQGN7!#Zdid zFtCVPs})~fOovzORL#0QwjF%KFE1Y-=QLC4>B%WoNthNW(?9PqXz7ybU7;W?oqs|3 zgZEWYKT$Pm?wWzCn@4g^;Ebf|BrA!apzEugBwXCvV_I*{o<|smkXmqsFL#3sa;|fw z&e9aPw|Z{L#^g$O4)*m45cuzl!*4Xz-l)fZW$?Qi9R#i zN7v=Uq14|iLITkN7_a%6R{yu2Mie!r%V;yxvSx%+LrawPx%(8n-S@_#{a8W z_B_eAo@y31;FtTH79ZbMh=&NJX-X4`MZFhiuMZ)b75ekM_{vi)AX*oaOIjm_;F5wD ze?LXJ~r=}`j_Y6oi<(HaF;_(YW4*=Fx3CQ8^98tWj zp^CF`$DvU;4A?&c!xKPTQ(NbnCa-S7we-N+T3}s$4NXn3rsj*X+S>mDu%YOnfCv8@ zpdCq{W2YYV^M(^7zTxY5Z;I3{h0t@ZQ%g5O$R16yyQ$?JfWsz6XGVqlOg#>JR{2 zr2q(90oYp8-uu}n0IVCxKVFy}4u?ab5RT&*hA|AI*XwO|yWQb<-5id^wOCqOoSvSh zY1(KqSuC?da`}i#<#esOJzh8F^>}?AFOK>A%c$Ss^5cHev=(NKRy(X5e%|*|Ye6UH ziLqJYPZJ?G5dBj`x0aCoMf}?5|Ed!owGd{r$ueu{=~pO}BQDhK@%b?yf#D>EQ;R4y z=cI6wHk-{xqY?9BGY%+(<50)%CrFZ{D2k?8mf={IgT=BwpHFWiV7xixI4%$fOw1EY zD7;JZJWmk*aVyC{7ROmE7Lp`XX3~Mu!C=s2HY<&!9c2O>OViY#fwG|tAK*gaFvHL< zrl^HgmgfUAMq~E`HRoc3e8A)JSgmt2Mw4b@f@2w(hO#})8=!*^@76UtYwoKEMIeu|>my5ET&9TN`m1A~KV^_amh9SCp(2F8X4!lB^G z%F2*jp;oKCUawB4v%R)QZWhJVZ1Y)k3weDFm@(aqS51Dy-J-1hFCl&C}1ZTGI1*WGH3E$ObTltgy_@y$lb z)7k#u&BGb7NlAC-<5&Vc+oO$;+$lEf7ep4Ilt-F4pS-2zgocsf>Q(c{sSTeVzx(-S zMb`styE_IX6kU7oOh(7SmgHj*%aym&;dky7XP**ZI43R_mR%`_00f9&?`{Ohgt)6<7bCfGLyK6jJ_C5MQh+uN9xGs`;0bUWlX#1&A;e Vc_T8r(q)Alkee;adYoC>_&2=Qb1(n^ literal 0 HcmV?d00001 diff --git a/app/images/zoom-fit-best@2x.png b/app/images/zoom-fit-best@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..904b41c88e0d3ed4f54eff59097964706408b6de GIT binary patch literal 1692 zcmZ{kc~sJA7sr1F0=Z)DCZ(37HX4#IA#UKo1-FcxNf|9%5XnRgtZ2#vS4OQ=OnY6z z+_G}dB^}X9X{%EO(ZMT>RZ7e$}{_GN1Rg3BE8A}0B{-!0LiHU@L4%Z{sRCKQ2_8iDgfY$0Kkw{ z(s{sHxqw9YdJ%z*Zz1Zg$yZJ`v%EvtN>$!e*85oZ$I1rEA^Cei<)F=wE!Ou|%mx8K zl};iOf`ac27RksmPt%N^FcRiO_GTV6f4JKGa?!zfr&3I*qV4fWu`6}E=w!EVOnmZ1 z5UB{0fC5QiKR$W4;trKHTgo{9jiyJ{MKM-oFE$#xwn&Wb(L>)Vasp~n<~ldj{ceX= zlWU_uN9(ws71n9)JaVmKpf?rXQJ*E}IwyTGvVMIEAE_Z;Xld>CZwe%C&C%PGiZ{ap z5~4I29u8eElN{Eb+6uL@EG*S}T;{T4K4Ayaz+EJ~+ueCp)_qmF1T$9ckH{t0re+eJ z$Q_~}je4`#NIh#XWv;R`w6}x6Ag!#doLXMAEbvKVwm*Ebh<0=3^7Smvw= z(IxQEWcHWU`%U4igazbUvcLCpwUmS?n+myS;%BWs~L zF`}!n|6%Rv{J~^04Ex6D{NtIkKf+^p`LmT3$p_$O;7hcoo^!n!7i@8c@<)eko*z-< zk~J^yTWNpGexaD`&8nU)+?Ch0PnE5eTseAq%diG^`S&q{WvAFL^Ov}R?~dP~c*2Zb z%&*FIwX>%6bCmbBE=r57!{I9 z;A(w8=^L;3o7dq|K+m1(2YUs@J=hOo86y!Y2oSb@Pqb#<9lg2QUxgMv+fq;mf#0q5 zRtuJiZ2}Ls3!jcQR`6zw9-T9GOt?uo`1~O>8+W196>M7Z)aV+Ap8vEs2u;rmcCoBNEdffJVu`EYciWpX-N+A>JL>F=qdQvS(u+Ebfe(s2`(zP zsey{l_X~W9vl#+of60fhJLon9SHsRb>yPICv~DKQFoBYcCi9<}9J*k*?L#gkh_cu2 zWz?Qe(4@1pyJmvkmg#}(v55NgTVqyL!<35FfUY^BkM2lb_Qeo{`w?QPHBw4)hveS-EmzR{>3J$Al>26Jf^G8Pw)}U)o zV{aT|GS8=hn%fn8miAgXViQIN*-v94((yU=0w7aT9M2&$mqKBGuoDwK!d|cI##M3pj zUZ6?!y>m~0z(dN4C!yu3TNg-G3n{KH4H?q8gtZm))Zs$IjEtwTi6%KY0@m$h{(Gkf zLx_Qs+TV$9yP19s86BNDRxb2ZRjUc4mQ!B$&ziSVNGXZ*g(4bw;u&iTQfF;c8)V`>=zF@)x$`cm#8z!Z|ct@pv99o%}-6#|eg(@qmO8XZO7azrl=Kg1g!pc;4tE7Y)*I_>mSB32!+Af ZV{s1lf0?)dS5%Y?K=KG6*1J>re*$=j^#T9@ literal 0 HcmV?d00001 diff --git a/app/images/zoom-fit-width.png b/app/images/zoom-fit-width.png new file mode 100644 index 0000000000000000000000000000000000000000..d51fbac69d0ed504e84cb35654d0ed3e4235e3bf GIT binary patch literal 905 zcmZ{gYiv_x7{}i_VPk?R%K`@6MwE+);#nss6|=c?S?gpJDAolguuB(47Un1{P&I=L zGKqvl5*;8QALayaa@ls6`E!+%1<$+cSSgGZ_44go064Z9fR0W8(y-ML2H-FP zKy)tv(ry5jwtjne+hzbnmC6rP6@nmSG8u+pNRmt@lWw;=5C{aJp3Diuun=?z^LRWw z&wIVz=u3(KyV{sJq;RN9i5#AsbiP1R8L~+t$u^Nm< z|KlJN=V>-EMevWK2{z87C`wW!P17vPa`8DO5{XnQl}@K9*yOl$Iz2r-O*2e_=VxYS z;K$h57{_t5v$H-F%?QG%*UKl91VKzpOpbbekDShEG-@9j8Z;OzR%?tP7?yQ7oJNx= z5{V4Ehm)yPzg};%*+)h^;c$51{-DifqbSPda=AxF%~m^BrTGNzT2xn6DFa>e3gqyy zpjBRT2qxS+Eb2h{BY4TyDz;Q)Ph~BbpD!u0RQEz=zC!k%>IK(MhkC~|#bP}@RJ*e9 zeY|#CAAhRgxxYmF5S7_v*Gf0 zc}I8YDx1{pTMzCL^nM2=|LVYt1ywx{)~xRRzDY9`TDoNMH(#qi9PyMazqkv1=bo-c zrW5p5toH1VSD&9(cT?GS`gG?T&n+xetXrGgaCKm}6TAJ|TGdYRkEuL&{x$L0+O0(M z%M!y+51cuo^6G_c&0luj5B7F7H7n2maB0o@^`D(lJ-ci+?LHg|EfL#}OY_FZOW!PP zDf{$KRP4F3=VNDw+Ogu8PDskK(#Gzfx#$*IRFYlRe5U{Usn116u5I4^?qcCMT+%9S z#YftPecDE8eOn_8AVDN0uOrZJtBT)}N}xBcLl7x~T<-2U`5&O=V8edR(f$3m= literal 0 HcmV?d00001 diff --git a/app/images/zoom-fit-width@2x.png b/app/images/zoom-fit-width@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4d1fd0b4d8276b6b2d2d4f1f4677c32968236248 GIT binary patch literal 1724 zcmZ`%c~p~E7XK;~SKL~yMNz6!;ndbh9Sv#SC>SEBq@pbd#l0@2(1Ps%)~XflP`5f| zg25;VwAheDY63AE1OfpeD z;jzjf<5U|;7TrSr%I%*m{BopjV}O(ho|m> zVT;8APK=sItyZ9x0j>49&gO6c+MP~7hs))lQmI5Dv8JYmKp=SBZUlkdZa~E2Ml>Uc z-i&xX2#?28YAm(wwlnpP1Gil_na+njPP*9jSO#bovx&}>oA9MZ*IhnbBY+=t zA+%mML*i!1-IcX8g`!U&5Y$m0_i6&|&wOl!k1p{x^myuoo^v!e?gny*1QU6P&f?{9 zxi*{CZnqiDc8S{YR13=KgkeAzg18zQ$oHzMC=?2t&G!3!Ua!aJ1M~t{rT0EHc%6vX z?+5gQirO6xr^`94)hd-rl}a@-G6H@e;P*KIoKBzL9~c`81Oj8@<4&h@Yz*l5%a<=X z9Ij!+#O82JCX-&T*J`yooeq4F>baW5Voyv2pA8JiWb&S#-mb3hUXfVduX@zf+~40n zG&I!G%4ak+)9H*>K3^mjH#Ij4ySnHMMl+MyF6dCJ2RU4}yiYC`OCHe~y&{p(XyCPQ z6$*t)^|ZB>|KNABT0LYi===K>27{5#XlkHPhlhu$R2sjnO{dqjwD6QlkVqpCbZ9i1 zPGJ{c(8*%4#)Cnf!T3bl*V)-A=;&x^Yk$mbp)@{H^!Mw@cRN5^zj8PQn+W~*YGg8) z%*;>DC;$pt9GLTTf&@6z3vs)WrjLZo3|ko6?u`&~wc$mvU?Zd6LPzu8+lS8?Ho+QS*UZ*$Jh&A?X(B{A~PX-IA6!OC$J=YfrrP z%ZeSPZK1ac!pg`Gs61ZjL{r05|Gcx@mTvnzt%g zoiAOz%Q%ahx;-P1J&=tr+Vl1NV*Cv1L-F{$nlf$~-(x#s~{XzA@oX`ph zYhBJyWgkWE5Y)z!$>j2#(ZN<)p62^taFG$!yKKesq)^|UKzR0olO=-@T+r)KbYaq` zg+~t+9>W~@`WO%>4i)$QyC^_(`ua_nIKYkXpimeT>TX3{>Hh@e<{dqGyy*W2I65NZ xK|suZG8CT7JyuY7C^!GVG4biBjhOXl%!c*a7nupsR;)!{s{yw@xlNA literal 0 HcmV?d00001 diff --git a/app/images/zoom-in.png b/app/images/zoom-in.png new file mode 100644 index 0000000000000000000000000000000000000000..5ae1046c0f9c4a8d9c5380d2710a146bfedbd55d GIT binary patch literal 952 zcmZ{hUrbwd6vuxA`LpTb%M3V0DPaV+vDKPQSIvwQM$u6j>p*=lATYxmN?iBg^Z^Ld zF*Y|v79yhqEG+}tVxhpGqowPme`(i$&_ZbeZ`<3Jw$P4STKdPkm}q?Pta^arc`*s5W_7suwOEKO1G*T0>JX!8}7n95?QfV;&S8@PoXa*pRjT#UDF5>}k zRRBQj03faY?&O(w07zh!oTe1S+*M4A<@GH|KfUJ z)fveU?jR3iBBrb9kkvjfj}s<%^Bg{?*0QZI*_d$e>evF&u>kg)>cEIkQfr*>FT-D-QUGvb3!o9V-~ArdpqcKB6hni5b)b~!TX_kkwCv2evkMK+ zUbs-}MqJL9()YdCDNHJ!Q|29F4b@%rtY^Gj*+fxE?WEtUFFa{JAkn`CmE1LcSej^iT literal 0 HcmV?d00001 diff --git a/app/images/zoom-in@2x.png b/app/images/zoom-in@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..863ef4aca78d6ce7e741f61c65fec2529286e4ae GIT binary patch literal 2100 zcmZ{ldpy(oAIHCQ$yzC?V&nL~EWGN4U zLI41eCt8`2C93(igLg>o%+|9 zx?0S?K#&x6c-dH*0ph<;X=^20lG%CH$|X#ql9v)+4lo^*1ab_bow?ks^v)fMS{6IU8GKlvkHVw+R({hh8sHXK z!*g%mzYqJDQ2to=__(qhm823zbVg?5^a|$*;F~4YNz@=YadpKfAM33P;y9x%;XZ4v zu4veXU_EC{T&6I+z_ivP)Flh~cvi-Y!8%9IRN+;)6i9jmhuJ+N-pg=JO-z*TyP=xz zH0C@x?tA?@VlphJkc2;=ej}YNoJZZV%C=I+*nQEE?ji=*Yjr(;ZY&V^R3Ki~Ke~v@ zv9ezn7&<6aFBOin2Hr#@caiPKou5G$8KZ+em2+^K;cyc^Y-zB=Sd#mMS z`fSRaP}t=Fxh+G6;Owxm*Hb1e!(J$$Wgbyew0zYwf*_WX&AS)Duuq&vuobAQEyeq%oM%Q!3XwYn)pK`PmMZusc$ zEI22aUjOi6T=h=`MVdfgrKqTuF1Q;VY7%X)c9guIR%kMEyR4qg%(&TmC__0Vg_p={ z;^q{#VdX_#yIxa35o-}EgwKmWKm(NWQCluf*-z$zDH50FNh_#e`T0AiAWQr8FR}|# zQv9rK9gfgs!MC9KYQ&1%*Aykz$Q`#cc+Bz(Xzl3g_(TA50RN-0eYLIqN=#tp+Ycx5 zeGVV3HP)e;<-%}#)&-Nn`*vUYjernDtb2SlNHaTSx@!uO@p1HuH-E&eOf`59;U41E z1Eb8+bN z?v`=6%=tJx74>N1U6HhFPx;61b9@$$D8)A!lkIg(H>;F?jHo0s7~!5j7M<>kD9x-azv397p`vn@#m=7+F7;bznZ)PiKK%gW z2iBog(dx~-j8N6w5;esYO?Wo3ht`}P+8}G~IaWh!udJyNcD5aH3L$WiT%y8gP&){0 z#Uw-Awet4Dmd;LpvuXIO`PML<9DiqsG5+37?be*ZC_Pjit(ZoBF+2BJKj&$pDE3|c z^tXWvu>1tn(Lz+t^uflGi&tUhJ^}3aSy4Q9-ID6Zr?Fjp9@|QNqUyv0(WX|o7-obu zQ@;mR4&q2zr0Bl-`KfH*5$MjcJsM6RR?WR9_v$xk#gVC9sb?OCc)fdQ67P-e6^Ra0 zY#bI~>vPPEB-^y1sw3gabutqSsVIB^gr@rP!J?-A&w=nW#a_`L;gd02->L*WDwAxT zl3FK|Rp&*dp>JbpcEUmh5 zY;|R5z#WYxBq&V3POWwTxm&bc{cQ!P0x4XF(`Zzl) zD;(D2x!#99cka-n(Xvk@$MXKgGPk;!C^$2ogomjqrXiC&{GxW+`8!5v+mcAgq(6!# zw>Tc?$Y~5K$oW@@-N-LpUf%UCkYuS_wtI(YUzaZrd(r8vYpopU*BwUnf+XOrAGgvVN;c(_LaRd%1~%K&JI2}I8w4yVXqla&dd_C z*%#j!%KphGRXKY3h2&f-Bb~UZ!PQ-!bN|)Z&BZtxX=2RPVE3X84Q*=mEF>{BEmWBL zImC5sfeDn8)2z!)TP{~rR<@SD85_G5wLY~4bbDb^cBB@>;xhaSz3Wd8SK+8s>TCaK z$ppcEoG5CnQfa2-ht4W-#)yzRCMHK<6C`1NCjn$rVGiVj~4) z2}uu)ZhQL{3hF=}fD8#Jo$CmV)>HmUasGJLxfXKwTC4f7=!TsK^>lsUTOcRb24cV>8dGJFiEp*|7;uxPB_akRwnj+j#hSc&QDpwR|s zG^e0E`Tu~RU~fO)sDB1Ncd4G10Gj{p!0-$531fH$UHu<(+7Yd9fWaH!F#l;{1aLtT O1|XV~%y_0A34Z}u$F*Sq literal 0 HcmV?d00001 diff --git a/app/images/zoom-original.png b/app/images/zoom-original.png new file mode 100644 index 0000000000000000000000000000000000000000..8aa9bb49c5057a707f43e60956cacbaa9977b414 GIT binary patch literal 946 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 Y24(bb6HB0co0tKd?A>t;JKv;#0VukeQvd(} literal 0 HcmV?d00001 diff --git a/app/images/zoom-out.png b/app/images/zoom-out.png new file mode 100644 index 0000000000000000000000000000000000000000..081b6d981028da6fe3a3ba2b0604c64add0036db GIT binary patch literal 940 zcmZ{hdrVt(6vuzacx<@%htuJ7x)SJ~Y1^K){7Ll(_nb{>4ZW znL3ymn`DkrHehVfmX+5;q}cTWy|=XMi>}ZX#`?IWFWS;ed+F`<5)+MocyiA9o^z5< zPEO8+Vs63aP1`mB05)?9^GaylvL5NlbRH@7y+zB8io%j20KQ`aP$vL@phtBW0N+3W zER_MkZUJCx?e&r4Zvv3iUi>~UpQ5NlB7x($#kPPYUP;#e*_AJc;9xC68xVhN{hOM}TmL;_h%93&t(Z70GXtV}CwaTQ@=mtA0lp ztR4ajlm2KT5Tm@S2}h9BKMr+EWf+F}{l0dIVpQi>TRpJWjLuk8CJQn%H=&y~J3`_y zgGOtx+n==bC`OE7kxV6M?raiDo7$v}ZBjvVhr+PfHEI$`hv)-bF6Z@L`5&gxoOflx z7jg!1R}i1~E}OlpZG-9_ao-rMk(&YzoQzNuK~ZsvvIN$C7D|N<3W-l zDQbl`+SzC^g_5gk#);3>jTU>k`i^UYM;%aOA zRbh8)m#DR)`({Vy@ckj%ye%4Cb-OXA(-97b91go)ufN-K$LI5@R4N!&3=Iz%jYgeL zD-!pbXU&mFM5$CFDAGUBk06MoPi&kq5ClPzr1z^yWpChSW$O5mK%D#vqIxS2`CCqFrXo@bmYoqyQC zcx15X#s!R1JnL*dW0!tL*LY{(?!8;jzH_NP!(IX&=I&`ao%st)pO(YV|Bzo<&aYxu zd|gEgU_q=`b0OLf^9~+jv(|kt1hFCLa6o+UKS0e_mDQhr`~Lt(`us&2*!9l_esxXN lIevLf?Z3<$Jm_^c3u3@v*Y{zW`5F2DJbH literal 0 HcmV?d00001 diff --git a/app/images/zoom-out@2x.png b/app/images/zoom-out@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..34c8e1748c5d07bf1bdfd8a6fb7741bf57913ba9 GIT binary patch literal 1971 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8a05c2fc4121d676362813dca39260d29e040897 GIT binary patch literal 43176 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 literal 0 HcmV?d00001 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 ../