File indexing completed on 2024-04-28 15:51:54

0001 /*
0002     SPDX-FileCopyrightText: 2002 Wilco Greven <greven@kde.org>
0003     SPDX-FileCopyrightText: 2002 Chris Cheney <ccheney@cheney.cx>
0004     SPDX-FileCopyrightText: 2002 Malcolm Hunter <malcolm.hunter@gmx.co.uk>
0005     SPDX-FileCopyrightText: 2003-2004 Christophe Devriese <Christophe.Devriese@student.kuleuven.ac.be>
0006     SPDX-FileCopyrightText: 2003 Daniel Molkentin <molkentin@kde.org>
0007     SPDX-FileCopyrightText: 2003 Andy Goossens <andygoossens@telenet.be>
0008     SPDX-FileCopyrightText: 2003 Dirk Mueller <mueller@kde.org>
0009     SPDX-FileCopyrightText: 2003 Laurent Montel <montel@kde.org>
0010     SPDX-FileCopyrightText: 2004 Dominique Devriese <devriese@kde.org>
0011     SPDX-FileCopyrightText: 2004 Christoph Cullmann <crossfire@babylon2k.de>
0012     SPDX-FileCopyrightText: 2004 Henrique Pinto <stampede@coltec.ufmg.br>
0013     SPDX-FileCopyrightText: 2004 Waldo Bastian <bastian@kde.org>
0014     SPDX-FileCopyrightText: 2004-2008 Albert Astals Cid <aacid@kde.org>
0015     SPDX-FileCopyrightText: 2004 Antti Markus <antti.markus@starman.ee>
0016 
0017     Work sponsored by the LiMux project of the city of Munich:
0018     SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
0019 
0020     SPDX-License-Identifier: GPL-2.0-or-later
0021 */
0022 
0023 #include "part.h"
0024 
0025 #include "config-okular.h"
0026 
0027 // qt/kde includes
0028 #include <QApplication>
0029 #include <QContextMenuEvent>
0030 #if HAVE_DBUS
0031 #include <QDBusConnection>
0032 #endif // HAVE_DBUS
0033 #include <QDialog>
0034 #include <QDialogButtonBox>
0035 #include <QFile>
0036 #include <QFileDialog>
0037 #include <QInputDialog>
0038 #include <QJsonArray>
0039 #include <QLabel>
0040 #include <QLayout>
0041 #include <QMenu>
0042 #include <QMenuBar>
0043 #include <QMimeDatabase>
0044 #include <QPrintDialog>
0045 #include <QPrintPreviewDialog>
0046 #include <QPrinter>
0047 #include <QScopedValueRollback>
0048 #include <QScrollBar>
0049 #include <QSlider>
0050 #include <QSpinBox>
0051 #include <QStandardPaths>
0052 #include <QTemporaryFile>
0053 #include <QTimer>
0054 #include <QWidgetAction>
0055 
0056 #include <KAboutPluginDialog>
0057 #include <KActionCollection>
0058 #include <KActionMenu>
0059 #include <KBookmarkAction>
0060 #include <KColorSchemeManager>
0061 #include <KColorSchemeMenu>
0062 #include <KCompressionDevice>
0063 #include <KDirWatch>
0064 #include <KFilterBase>
0065 #include <KHamburgerMenu>
0066 #include <KIO/FileCopyJob>
0067 #include <KIO/Global>
0068 #include <KIO/OpenFileManagerWindowJob>
0069 #include <KIO/StatJob>
0070 #include <KJobWidgets>
0071 #include <KMainWindow>
0072 #include <KMessageBox>
0073 #include <KParts/GUIActivateEvent>
0074 #include <KPasswordDialog>
0075 #include <KPluginMetaData>
0076 #include <KSharedDataCache>
0077 #include <KStandardShortcut>
0078 #include <KToggleAction>
0079 #include <KToggleFullScreenAction>
0080 #include <KToolBar>
0081 #if HAVE_KWALLET
0082 #include <KWallet>
0083 #endif
0084 #include <KXMLGUIClient>
0085 #include <KXMLGUIFactory>
0086 
0087 #if HAVE_PURPOSE
0088 #include <Purpose/AlternativesModel>
0089 #include <Purpose/Menu>
0090 #include <purpose_version.h>
0091 #endif
0092 
0093 // local includes
0094 #include "aboutdata.h"
0095 #include "bookmarklist.h"
0096 #include "core/action.h"
0097 #include "core/annotations.h"
0098 #include "core/bookmarkmanager.h"
0099 #include "core/document_p.h"
0100 #include "core/form.h"
0101 #include "core/generator.h"
0102 #include "core/page.h"
0103 #include "core/printoptionswidget.h"
0104 #include "drawingtoolactions.h"
0105 #include "embeddedfilesdialog.h"
0106 #include "extensions.h"
0107 #include "fileprinterpreview.h"
0108 #include "findbar.h"
0109 #include "gui/signatureguiutils.h"
0110 #include "layers.h"
0111 #include "minibar.h"
0112 #include "okmenutitle.h"
0113 #include "pagesizelabel.h"
0114 #include "pageview.h"
0115 #include "preferencesdialog.h"
0116 #include "presentationwidget.h"
0117 #include "propertiesdialog.h"
0118 #include "searchwidget.h"
0119 #include "settings.h"
0120 #include "side_reviews.h"
0121 #include "sidebar.h"
0122 #include "signaturepanel.h"
0123 #include "thumbnaillist.h"
0124 #include "toc.h"
0125 
0126 #include <memory>
0127 #include <type_traits>
0128 
0129 #ifdef OKULAR_KEEP_FILE_OPEN
0130 class FileKeeper
0131 {
0132 public:
0133     FileKeeper()
0134         : m_handle(nullptr)
0135     {
0136     }
0137 
0138     ~FileKeeper()
0139     {
0140     }
0141 
0142     void open(const QString &path)
0143     {
0144         if (!m_handle)
0145             m_handle = std::fopen(QFile::encodeName(path).constData(), "r");
0146     }
0147 
0148     void close()
0149     {
0150         if (m_handle) {
0151             int ret = std::fclose(m_handle);
0152             Q_UNUSED(ret)
0153             m_handle = nullptr;
0154         }
0155     }
0156 
0157     QTemporaryFile *copyToTemporary() const
0158     {
0159         if (!m_handle)
0160             return nullptr;
0161 
0162         QTemporaryFile *retFile = new QTemporaryFile;
0163         retFile->open();
0164 
0165         std::rewind(m_handle);
0166         int c = -1;
0167         do {
0168             c = std::fgetc(m_handle);
0169             if (c == EOF)
0170                 break;
0171             if (!retFile->putChar((char)c))
0172                 break;
0173         } while (!feof(m_handle));
0174 
0175         retFile->flush();
0176 
0177         return retFile;
0178     }
0179 
0180 private:
0181     std::FILE *m_handle;
0182 };
0183 #endif
0184 
0185 K_PLUGIN_FACTORY_WITH_JSON(OkularPartFactory, "okular_part.json", registerPlugin<Okular::Part>();)
0186 
0187 static QAction *actionForExportFormat(const Okular::ExportFormat &format, QObject *parent = Q_NULLPTR)
0188 {
0189     QAction *act = new QAction(format.description(), parent);
0190     if (!format.icon().isNull()) {
0191         act->setIcon(format.icon());
0192     }
0193     return act;
0194 }
0195 
0196 static KCompressionDevice::CompressionType compressionTypeFor(const QString &mime_to_check)
0197 {
0198     // The compressedMimeMap is here in case you have a very old shared mime database
0199     // that doesn't have inheritance info for things like gzeps, etc
0200     // Otherwise the "is()" calls below are just good enough
0201     static QHash<QString, KCompressionDevice::CompressionType> compressedMimeMap;
0202     static bool supportBzip = false;
0203     static bool supportXz = false;
0204     const QString app_gzip(QStringLiteral("application/x-gzip"));
0205     const QString app_bzip(QStringLiteral("application/x-bzip"));
0206     const QString app_xz(QStringLiteral("application/x-xz"));
0207     if (compressedMimeMap.isEmpty()) {
0208         std::unique_ptr<KFilterBase> f;
0209         compressedMimeMap[QStringLiteral("image/x-gzeps")] = KCompressionDevice::GZip;
0210         // check we can read bzip2-compressed files
0211         f.reset(KCompressionDevice::filterForCompressionType(KCompressionDevice::BZip2));
0212         if (f.get()) {
0213             supportBzip = true;
0214             compressedMimeMap[QStringLiteral("application/x-bzpdf")] = KCompressionDevice::BZip2;
0215             compressedMimeMap[QStringLiteral("application/x-bzpostscript")] = KCompressionDevice::BZip2;
0216             compressedMimeMap[QStringLiteral("application/x-bzdvi")] = KCompressionDevice::BZip2;
0217             compressedMimeMap[QStringLiteral("image/x-bzeps")] = KCompressionDevice::BZip2;
0218         }
0219         // check if we can read XZ-compressed files
0220         f.reset(KCompressionDevice::filterForCompressionType(KCompressionDevice::Xz));
0221         if (f.get()) {
0222             supportXz = true;
0223         }
0224     }
0225     QHash<QString, KCompressionDevice::CompressionType>::const_iterator it = compressedMimeMap.constFind(mime_to_check);
0226     if (it != compressedMimeMap.constEnd()) {
0227         return it.value();
0228     }
0229 
0230     QMimeDatabase db;
0231     QMimeType mime = db.mimeTypeForName(mime_to_check);
0232     if (mime.isValid()) {
0233         if (mime.inherits(app_gzip)) {
0234             return KCompressionDevice::GZip;
0235         } else if (supportBzip && mime.inherits(app_bzip)) {
0236             return KCompressionDevice::BZip2;
0237         } else if (supportXz && mime.inherits(app_xz)) {
0238             return KCompressionDevice::Xz;
0239         }
0240     }
0241 
0242     return KCompressionDevice::None;
0243 }
0244 
0245 static Okular::EmbedMode detectEmbedMode(QWidget *parentWidget, QObject *parent, const QVariantList &args)
0246 {
0247     Q_UNUSED(parentWidget);
0248 
0249     if (parent && (parent->objectName().startsWith(QLatin1String("okular::Shell")) || parent->objectName().startsWith(QLatin1String("okular/okular__Shell")))) {
0250         return Okular::NativeShellMode;
0251     }
0252 
0253     for (const QVariant &arg : args) {
0254         if (arg.metaType().id() == QMetaType::QString) {
0255             if (arg.toString() == QLatin1String("Print/Preview")) {
0256                 return Okular::PrintPreviewMode;
0257             } else if (arg.toString() == QLatin1String("ViewerWidget")) {
0258                 return Okular::ViewerWidgetMode;
0259             }
0260         }
0261     }
0262 
0263     return Okular::UnknownEmbedMode;
0264 }
0265 
0266 static QString detectConfigFileName(const QVariantList &args)
0267 {
0268     for (const QVariant &arg : args) {
0269         if (arg.metaType().id() == QMetaType::QString) {
0270             QString argString = arg.toString();
0271             int separatorIndex = argString.indexOf(QStringLiteral("="));
0272             if (separatorIndex >= 0 && argString.left(separatorIndex) == QLatin1String("ConfigFileName")) {
0273                 return argString.mid(separatorIndex + 1);
0274             }
0275         }
0276     }
0277 
0278     return QString();
0279 }
0280 
0281 #undef OKULAR_KEEP_FILE_OPEN
0282 
0283 #ifdef OKULAR_KEEP_FILE_OPEN
0284 static bool keepFileOpen()
0285 {
0286     static bool keep_file_open = !qgetenv("OKULAR_NO_KEEP_FILE_OPEN").toInt();
0287     return keep_file_open;
0288 }
0289 #endif
0290 
0291 int Okular::Part::numberOfParts = 0;
0292 
0293 namespace Okular
0294 {
0295 Part::Part(QObject *parent, const QVariantList &args)
0296     : KParts::ReadWritePart(parent)
0297     , m_tempfile(nullptr)
0298     , m_documentOpenWithPassword(false)
0299     , m_swapInsteadOfOpening(false)
0300     , m_tocEnabled(false)
0301     , m_isReloading(false)
0302     , m_fileWasRemoved(false)
0303     , m_showMenuBarAction(nullptr)
0304     , m_showFullScreenAction(nullptr)
0305     , m_cliPresentation(false)
0306     , m_cliPrint(false)
0307     , m_cliPrintAndExit(false)
0308     , m_embedMode(detectEmbedMode(widget(), parent, args))
0309     , m_generatorGuiClient(nullptr)
0310     , m_keeper(nullptr)
0311 {
0312     // make sure that the component name is okular otherwise the XMLGUI .rc files are not found
0313     // when this part is used in an application other than okular (e.g. unit tests)
0314     setComponentName(QStringLiteral("okular"), QString());
0315 
0316     setupConfigSkeleton(args);
0317 
0318 #if HAVE_DBUS
0319     numberOfParts++;
0320     if (numberOfParts == 1) {
0321         m_registerDbusName = QStringLiteral("/okular");
0322     } else {
0323         m_registerDbusName = QStringLiteral("/okular%1").arg(numberOfParts);
0324     }
0325     QDBusConnection::sessionBus().registerObject(m_registerDbusName, this, QDBusConnection::ExportScriptableSlots);
0326 #endif // HAVE_DBUS
0327 
0328     // connect the started signal to tell the job the mimetypes we like,
0329     // and get some more information from it
0330     connect(this, &KParts::ReadOnlyPart::started, this, &Part::slotJobStarted);
0331 
0332     // connect the completed signal so we can put the window caption when loading remote files
0333     connect(this, QOverload<>::of(&Part::completed), this, &Part::setWindowTitleFromDocument);
0334     connect(this, &KParts::ReadOnlyPart::canceled, this, &Part::loadCancelled);
0335 
0336     // create browser extension (for printing when embedded into browser)
0337     m_bExtension = new BrowserExtension(this);
0338 
0339     const QStringList iconDirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics"), QStandardPaths::LocateDirectory);
0340     QIcon::setFallbackSearchPaths(QIcon::fallbackSearchPaths() << iconDirs);
0341 
0342     m_sidebar = new Sidebar(widget());
0343     setWidget(m_sidebar);
0344     connect(m_sidebar, &Sidebar::urlsDropped, this, &Part::handleDroppedUrls);
0345 
0346     // build the document
0347     m_document = new Okular::Document(widget());
0348     connect(m_document, &Document::linkFind, this, &Part::slotFind);
0349     connect(m_document, &Document::linkGoToPage, this, &Part::slotGoToPage);
0350     connect(m_document, &Document::linkPresentation, this, &Part::slotShowPresentation);
0351     connect(m_document, &Document::linkEndPresentation, this, &Part::slotHidePresentation);
0352     connect(m_document, &Document::openUrl, this, &Part::openUrlFromDocument);
0353     connect(m_document->bookmarkManager(), &BookmarkManager::openUrl, this, &Part::openUrlFromBookmarks);
0354     connect(m_document, &Document::close, this, &Part::close);
0355     connect(m_document, &Document::requestPrint, this, &Part::slotPrint);
0356     connect(m_document, &Document::requestSaveAs, this, [this] { slotSaveFileAs(); });
0357     connect(m_document, &Document::undoHistoryCleanChanged, this, [this](bool clean) {
0358         setModified(!clean);
0359         setWindowTitleFromDocument();
0360     });
0361 
0362     if (parent && parent->metaObject()->indexOfSlot(QMetaObject::normalizedSignature("slotQuit()").constData()) != -1) {
0363         connect(m_document, SIGNAL(quit()), parent, SLOT(slotQuit())); // clazy:exclude=old-style-connect
0364     } else {
0365         connect(m_document, &Document::quit, this, &Part::cannotQuit);
0366     }
0367     // widgets: ^searchbar (toolbar containing label and SearchWidget)
0368     //      m_searchToolBar = new KToolBar( parentWidget, "searchBar" );
0369     //      m_searchToolBar->boxLayout()->setSpacing( KDialog::spacingHint() );
0370     //      QLabel * sLabel = new QLabel( i18n( "&Search:" ), m_searchToolBar, "kde toolbar widget" );
0371     //      m_searchWidget = new SearchWidget( m_searchToolBar, m_document );
0372     //      sLabel->setBuddy( m_searchWidget );
0373     //      m_searchToolBar->setStretchableWidget( m_searchWidget );
0374 
0375     // [left toolbox optional item: Table of Contents] | []
0376     m_toc = new TOC(nullptr, m_document);
0377     connect(m_toc.data(), &TOC::hasTOC, this, &Part::enableTOC);
0378     connect(m_toc.data(), &TOC::rightClick, this, &Part::slotShowTOCMenu);
0379 
0380     // [left toolbox optional item: Layers] | []
0381     m_layers = new Layers(nullptr, m_document);
0382     connect(m_layers.data(), &Layers::hasLayers, this, &Part::enableLayers);
0383 
0384     // [left toolbox: Thumbnails and Bookmarks] | []
0385     QWidget *thumbsBox = new ThumbnailsBox(nullptr);
0386     thumbsBox->layout()->setSpacing(6);
0387     m_searchWidget = new SearchWidget(thumbsBox, m_document);
0388     thumbsBox->layout()->addWidget(m_searchWidget);
0389     m_thumbnailList = new ThumbnailList(thumbsBox, m_document);
0390     thumbsBox->layout()->addWidget(m_thumbnailList);
0391     //  ThumbnailController * m_tc = new ThumbnailController( thumbsBox, m_thumbnailList );
0392     connect(m_thumbnailList.data(), &ThumbnailList::rightClick, this, &Part::slotShowMenu);
0393     m_sidebar->addItem(thumbsBox, QIcon::fromTheme(QStringLiteral("view-preview")), i18n("Thumbnails"));
0394 
0395     m_sidebar->setCurrentItem(thumbsBox);
0396 
0397     // [left toolbox: Reviews] | []
0398     m_reviewsWidget = new Reviews(nullptr, m_document);
0399     m_sidebar->addItem(m_reviewsWidget, QIcon::fromTheme(QStringLiteral("draw-freehand")), i18n("Annotations"));
0400 
0401     // [left toolbox: Bookmarks] | []
0402     m_bookmarkList = new BookmarkList(m_document, nullptr);
0403     m_sidebar->addItem(m_bookmarkList, QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Bookmarks"));
0404 
0405     // [left toolbox optional item: Signature Panel] | []
0406     m_signaturePanel = new SignaturePanel(m_document, nullptr);
0407     connect(m_signaturePanel.data(), &SignaturePanel::documentHasSignatures, this, &Part::enableSidebarSignaturesItem);
0408 
0409     // widgets: [../miniBarContainer] | []
0410 #ifdef OKULAR_ENABLE_MINIBAR
0411     QWidget *miniBarContainer = new QWidget(0);
0412     m_sidebar->setBottomWidget(miniBarContainer);
0413     QVBoxLayout *miniBarLayout = new QVBoxLayout(miniBarContainer);
0414     miniBarLayout->setContentsMargins(0, 0, 0, 0);
0415     // widgets: [../[spacer/..]] | []
0416     miniBarLayout->addItem(new QSpacerItem(6, 6, QSizePolicy::Fixed, QSizePolicy::Fixed));
0417     // widgets: [../[../MiniBar]] | []
0418     QFrame *bevelContainer = new QFrame(miniBarContainer);
0419     bevelContainer->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
0420     QVBoxLayout *bevelContainerLayout = new QVBoxLayout(bevelContainer);
0421     bevelContainerLayout->setContentsMargins(4, 4, 4, 4);
0422     m_progressWidget = new ProgressWidget(bevelContainer, m_document);
0423     bevelContainerLayout->addWidget(m_progressWidget);
0424     miniBarLayout->addWidget(bevelContainer);
0425     miniBarLayout->addItem(new QSpacerItem(6, 6, QSizePolicy::Fixed, QSizePolicy::Fixed));
0426 #endif
0427 
0428     // widgets: [] | [right 'pageView']
0429     QWidget *rightContainer = new QWidget(nullptr);
0430     m_sidebar->setMainWidget(rightContainer);
0431     QVBoxLayout *rightLayout = new QVBoxLayout(rightContainer);
0432     rightLayout->setContentsMargins(0, 0, 0, 0);
0433     rightLayout->setSpacing(0);
0434     //  KToolBar * rtb = new KToolBar( rightContainer, "mainToolBarSS" );
0435     //  rightLayout->addWidget( rtb );
0436     m_migrationMessage = new KMessageWidget(rightContainer);
0437     m_migrationMessage->setVisible(false);
0438     m_migrationMessage->setWordWrap(true);
0439     m_migrationMessage->setMessageType(KMessageWidget::Warning);
0440     m_migrationMessage->setText(
0441         i18n("This document contains annotations or form data that were saved internally by a previous Okular version. Internal storage is <b>no longer supported</b>.<br/>Please save to a file in order to move them if you want to continue "
0442              "to edit the document."));
0443     rightLayout->addWidget(m_migrationMessage);
0444     m_topMessage = new KMessageWidget(rightContainer);
0445     m_topMessage->setVisible(false);
0446     m_topMessage->setWordWrap(true);
0447     m_topMessage->setMessageType(KMessageWidget::Information);
0448     m_topMessage->setText(i18n("This document has embedded files. <a href=\"okular:/embeddedfiles\">Click here to see them</a> or go to File -> Embedded Files."));
0449     m_topMessage->setIcon(QIcon::fromTheme(QStringLiteral("mail-attachment")));
0450     connect(m_topMessage, &KMessageWidget::linkActivated, this, &Part::slotShowEmbeddedFiles);
0451     rightLayout->addWidget(m_topMessage);
0452     m_formsMessage = new KMessageWidget(rightContainer);
0453     m_formsMessage->setVisible(false);
0454     m_formsMessage->setWordWrap(true);
0455     m_formsMessage->setMessageType(KMessageWidget::Information);
0456     rightLayout->addWidget(m_formsMessage);
0457     m_infoMessage = new KMessageWidget(rightContainer);
0458     m_infoMessage->setVisible(false);
0459     m_infoMessage->setWordWrap(true);
0460     m_infoMessage->setMessageType(KMessageWidget::Information);
0461     rightLayout->addWidget(m_infoMessage);
0462     m_infoTimer = new QTimer();
0463     m_infoTimer->setSingleShot(true);
0464     connect(m_infoTimer, &QTimer::timeout, m_infoMessage, &KMessageWidget::animatedHide);
0465     m_signatureMessage = new KMessageWidget(rightContainer);
0466     m_signatureMessage->setVisible(false);
0467     m_signatureMessage->setWordWrap(true);
0468     rightLayout->addWidget(m_signatureMessage);
0469     m_pageView = new PageView(rightContainer, m_document);
0470     rightContainer->setFocusProxy(m_pageView);
0471     QMetaObject::invokeMethod(m_pageView, "setFocus", Qt::QueuedConnection); // usability setting
0472     //    m_splitter->setFocusProxy(m_pageView);
0473     connect(m_pageView.data(), &PageView::rightClick, this, &Part::slotShowMenu);
0474     connect(m_pageView, &PageView::triggerSearch, this, [this](const QString &searchText) {
0475         m_findBar->startSearch(searchText);
0476         slotShowFindBar();
0477     });
0478     connect(m_document, &Document::error, this, &Part::errorMessage);
0479     connect(m_document, &Document::warning, this, &Part::warningMessage);
0480     connect(m_document, &Document::notice, this, &Part::noticeMessage);
0481     connect(m_document, &Document::sourceReferenceActivated, this, &Part::slotHandleActivatedSourceReference);
0482     connect(m_pageView.data(), &PageView::fitWindowToPage, this, &Part::fitWindowToPage);
0483     rightLayout->addWidget(m_pageView);
0484     m_layers->setPageView(m_pageView);
0485     m_signaturePanel->setPageView(m_pageView);
0486     m_findBar = new FindBar(m_document, rightContainer);
0487     rightLayout->addWidget(m_findBar);
0488     m_bottomBar = new QWidget(rightContainer);
0489     QHBoxLayout *bottomBarLayout = new QHBoxLayout(m_bottomBar);
0490     m_pageSizeLabel = new PageSizeLabel(m_bottomBar, m_document);
0491     bottomBarLayout->setContentsMargins(0, 0, 0, 0);
0492     bottomBarLayout->setSpacing(0);
0493     bottomBarLayout->addItem(new QSpacerItem(5, 5, QSizePolicy::Expanding, QSizePolicy::Minimum));
0494     m_miniBarLogic = new MiniBarLogic(this, m_document);
0495     m_miniBar = new MiniBar(m_bottomBar, m_miniBarLogic);
0496     bottomBarLayout->addWidget(m_miniBar);
0497     bottomBarLayout->addWidget(m_pageSizeLabel);
0498     rightLayout->addWidget(m_bottomBar);
0499 
0500     m_pageNumberTool = new MiniBar(nullptr, m_miniBarLogic);
0501 
0502     connect(m_findBar, &FindBar::forwardKeyPressEvent, m_pageView, &PageView::externalKeyPressEvent);
0503     connect(m_findBar, &FindBar::onCloseButtonPressed, m_pageView, QOverload<>::of(&PageView::setFocus));
0504     connect(m_miniBar, &MiniBar::forwardKeyPressEvent, m_pageView, &PageView::externalKeyPressEvent);
0505     connect(m_pageView.data(), &PageView::escPressed, m_findBar, &FindBar::resetSearch);
0506     connect(m_pageNumberTool, &MiniBar::forwardKeyPressEvent, m_pageView, &PageView::externalKeyPressEvent);
0507     connect(m_pageView.data(), &PageView::requestOpenFile, this, [this](const QString &filePath, int pageNumber) {
0508         // We cheat a bit here reusing the urlsDropped signal, but at the end the output is the same, we want to open some files
0509         // urlsDropped should have just had a different name
0510         QUrl u = QUrl::fromLocalFile(filePath);
0511         u.setFragment(QStringLiteral("page=%1").arg(pageNumber));
0512         Q_EMIT urlsDropped({u});
0513     });
0514 
0515     connect(m_reviewsWidget.data(), &Reviews::openAnnotationWindow, m_pageView.data(), &PageView::openAnnotationWindow);
0516 
0517     // add document observers
0518     m_document->addObserver(this);
0519     m_document->addObserver(m_thumbnailList);
0520     m_document->addObserver(m_pageView);
0521     m_document->registerView(m_pageView);
0522     m_document->addObserver(m_toc);
0523     m_document->addObserver(m_miniBarLogic);
0524 #ifdef OKULAR_ENABLE_MINIBAR
0525     m_document->addObserver(m_progressWidget);
0526 #endif
0527     m_document->addObserver(m_reviewsWidget);
0528     m_document->addObserver(m_pageSizeLabel);
0529     m_document->addObserver(m_bookmarkList);
0530     m_document->addObserver(m_signaturePanel);
0531 
0532     connect(m_document->bookmarkManager(), &BookmarkManager::saved, this, &Part::slotRebuildBookmarkMenu);
0533 
0534     setupViewerActions();
0535 
0536     if (m_embedMode != ViewerWidgetMode) {
0537         setupActions();
0538     } else {
0539         setViewerShortcuts();
0540     }
0541 
0542     // document watcher and reloader
0543     m_watcher = new KDirWatch(this);
0544     connect(m_watcher, &KDirWatch::dirty, this, &Part::slotFileDirty);
0545     connect(m_watcher, &KDirWatch::created, this, &Part::slotFileDirty);
0546     connect(m_watcher, &KDirWatch::deleted, this, &Part::slotFileDirty);
0547     m_dirtyHandler = new QTimer(this);
0548     m_dirtyHandler->setSingleShot(true);
0549     connect(m_dirtyHandler, &QTimer::timeout, this, [this] { slotAttemptReload(); });
0550 
0551     slotNewConfig();
0552 
0553     // keep us informed when the user changes settings
0554     connect(Okular::Settings::self(), &KCoreConfigSkeleton::configChanged, this, &Part::slotNewConfig);
0555 
0556 #if HAVE_SPEECH
0557     // [SPEECH] check for TTS presence and usability
0558     Okular::Settings::setUseTTS(true);
0559     Okular::Settings::self()->save();
0560 #endif
0561 
0562     rebuildBookmarkMenu(false);
0563 
0564     if (m_embedMode == ViewerWidgetMode) {
0565         // set the XML-UI resource file for the viewer mode
0566         setXMLFile(QStringLiteral("part-viewermode.rc"));
0567     } else {
0568         // set our main XML-UI resource file
0569         setXMLFile(QStringLiteral("part.rc"));
0570     }
0571 
0572     m_pageView->setupBaseActions(actionCollection());
0573 
0574     m_sidebar->setSidebarVisibility(false);
0575     if (m_embedMode != PrintPreviewMode) {
0576         // now set up actions that are required for all remaining modes
0577         m_pageView->setupViewerActions(actionCollection());
0578         // and if we are not in viewer mode, we want the full GUI
0579         if (m_embedMode != ViewerWidgetMode) {
0580             unsetDummyMode();
0581         }
0582     }
0583 
0584     // ensure history actions are in the correct state
0585     updateViewActions();
0586 
0587     // also update the state of the actions in the page view
0588     m_pageView->updateActionState(false, false);
0589 
0590     if (m_embedMode == NativeShellMode) {
0591         m_sidebar->setAutoFillBackground(false);
0592     }
0593 
0594 #ifdef OKULAR_KEEP_FILE_OPEN
0595     m_keeper = new FileKeeper();
0596 #endif
0597 }
0598 
0599 void Part::setupConfigSkeleton(const QVariantList &args)
0600 {
0601     const QLatin1String configFileName("okularpartrc");
0602 
0603     // first, we check if a config file name has been specified
0604     QString configFilePath = detectConfigFileName(args);
0605 
0606     if (configFilePath.isEmpty()) {
0607         configFilePath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1Char('/') + configFileName;
0608     }
0609 
0610     KSharedConfigPtr config = KSharedConfig::openConfig(configFilePath);
0611 
0612     // Configuration update: SlidesTransitionsEnabled -> SlidesTransition = NoTransitions.
0613     // See https://invent.kde.org/graphics/okular/-/merge_requests/357
0614     KConfigGroup slidesConfigGroup = config.data()->group(QStringLiteral("Dlg Presentation"));
0615     if (slidesConfigGroup.readEntry<bool>("SlidesTransitionsEnabled", true) == false) {
0616         slidesConfigGroup.writeEntry("SlidesTransition", "NoTransitions");
0617     }
0618     slidesConfigGroup.deleteEntry("SlidesTransitionsEnabled");
0619     config.data()->sync();
0620 
0621     Okular::Settings::instance(config);
0622 }
0623 
0624 void Part::setupViewerActions()
0625 {
0626     // ACTIONS
0627     KActionCollection *ac = actionCollection();
0628 
0629     // Page Traversal actions
0630     m_gotoPage = KStandardAction::gotoPage(this, SLOT(slotGoToPage()), ac);
0631     ac->setDefaultShortcuts(m_gotoPage, KStandardShortcut::gotoLine());
0632     // dirty way to activate gotopage when pressing miniBar's button
0633     connect(m_miniBar.data(), &MiniBar::gotoPage, m_gotoPage, &QAction::trigger);
0634     connect(m_pageNumberTool.data(), &MiniBar::gotoPage, m_gotoPage, &QAction::trigger);
0635 
0636     m_prevPage = KStandardAction::prior(this, SLOT(slotPreviousPage()), ac);
0637     m_prevPage->setIconText(i18nc("Previous page", "Previous"));
0638     m_prevPage->setToolTip(i18n("Go back to the Previous Page"));
0639     m_prevPage->setWhatsThis(i18n("Moves to the previous page of the document"));
0640     ac->setDefaultShortcut(m_prevPage, QKeySequence());
0641     // dirty way to activate prev page when pressing miniBar's button
0642     connect(m_miniBar.data(), &MiniBar::prevPage, m_prevPage, &QAction::trigger);
0643     connect(m_pageNumberTool.data(), &MiniBar::prevPage, m_prevPage, &QAction::trigger);
0644 #ifdef OKULAR_ENABLE_MINIBAR
0645     connect(m_progressWidget, SIGNAL(prevPage()), m_prevPage, SLOT(trigger()));
0646 #endif
0647 
0648     m_nextPage = KStandardAction::next(this, SLOT(slotNextPage()), ac);
0649     m_nextPage->setIconText(i18nc("Next page", "Next"));
0650     m_nextPage->setToolTip(i18n("Advance to the Next Page"));
0651     m_nextPage->setWhatsThis(i18n("Moves to the next page of the document"));
0652     ac->setDefaultShortcut(m_nextPage, QKeySequence());
0653     // dirty way to activate next page when pressing miniBar's button
0654     connect(m_miniBar.data(), &MiniBar::nextPage, m_nextPage, &QAction::trigger);
0655     connect(m_pageNumberTool.data(), &MiniBar::nextPage, m_nextPage, &QAction::trigger);
0656 #ifdef OKULAR_ENABLE_MINIBAR
0657     connect(m_progressWidget, SIGNAL(nextPage()), m_nextPage, SLOT(trigger()));
0658 #endif
0659 
0660     m_beginningOfDocument = KStandardAction::firstPage(this, SLOT(slotGotoFirst()), ac);
0661     ac->addAction(QStringLiteral("first_page"), m_beginningOfDocument);
0662     m_beginningOfDocument->setText(i18n("Beginning of the document"));
0663     m_beginningOfDocument->setWhatsThis(i18n("Moves to the beginning of the document"));
0664 
0665     m_endOfDocument = KStandardAction::lastPage(this, SLOT(slotGotoLast()), ac);
0666     ac->addAction(QStringLiteral("last_page"), m_endOfDocument);
0667     m_endOfDocument->setText(i18n("End of the document"));
0668     m_endOfDocument->setWhatsThis(i18n("Moves to the end of the document"));
0669 
0670     // we do not want back and next in history in the dummy mode
0671     m_historyBack = nullptr;
0672     m_historyNext = nullptr;
0673 
0674     m_addBookmark = KStandardAction::addBookmark(this, SLOT(slotAddBookmark()), ac);
0675     m_addBookmarkText = m_addBookmark->text();
0676     m_addBookmarkIcon = m_addBookmark->icon();
0677     m_bookmarkList->setAddBookmarkAction(m_addBookmark);
0678 
0679     m_renameBookmark = ac->addAction(QStringLiteral("rename_bookmark"));
0680     m_renameBookmark->setText(i18n("Rename Bookmark"));
0681     m_renameBookmark->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename")));
0682     m_renameBookmark->setWhatsThis(i18n("Rename the current bookmark"));
0683     connect(m_renameBookmark, &QAction::triggered, this, &Part::slotRenameCurrentViewportBookmark);
0684 
0685     m_prevBookmark = ac->addAction(QStringLiteral("previous_bookmark"));
0686     m_prevBookmark->setText(i18n("Previous Bookmark"));
0687     m_prevBookmark->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search")));
0688     m_prevBookmark->setWhatsThis(i18n("Go to the previous bookmark"));
0689     connect(m_prevBookmark, &QAction::triggered, this, &Part::slotPreviousBookmark);
0690 
0691     m_nextBookmark = ac->addAction(QStringLiteral("next_bookmark"));
0692     m_nextBookmark->setText(i18n("Next Bookmark"));
0693     m_nextBookmark->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search")));
0694     m_nextBookmark->setWhatsThis(i18n("Go to the next bookmark"));
0695     connect(m_nextBookmark, &QAction::triggered, this, &Part::slotNextBookmark);
0696 
0697     m_copy = nullptr;
0698 
0699     m_selectAll = nullptr;
0700     m_selectCurrentPage = nullptr;
0701 
0702     // Find and other actions
0703     m_find = KStandardAction::find(this, SLOT(slotShowFindBar()), ac);
0704     QList<QKeySequence> s = m_find->shortcuts();
0705     s.append(QKeySequence(Qt::Key_Slash));
0706     ac->setDefaultShortcuts(m_find, s);
0707     m_find->setEnabled(false);
0708 
0709     m_findNext = KStandardAction::findNext(this, SLOT(slotFindNext()), ac);
0710     m_findNext->setEnabled(false);
0711 
0712     m_findPrev = KStandardAction::findPrev(this, SLOT(slotFindPrev()), ac);
0713     m_findPrev->setEnabled(false);
0714 
0715     m_save = nullptr;
0716     m_saveAs = nullptr;
0717     m_openContainingFolder = nullptr;
0718 
0719     m_hamburgerMenuAction = nullptr;
0720 
0721     QAction *prefs = KStandardAction::preferences(this, SLOT(slotPreferences()), ac);
0722     if (m_embedMode == NativeShellMode) {
0723         prefs->setText(i18n("Configure Okular..."));
0724     } else {
0725         // TODO: improve this message
0726         prefs->setText(i18n("Configure Viewer..."));
0727     }
0728 
0729     QAction *genPrefs = new QAction(ac);
0730     ac->addAction(QStringLiteral("options_configure_generators"), genPrefs);
0731     if (m_embedMode == ViewerWidgetMode) {
0732         genPrefs->setText(i18n("Configure Viewer Backends..."));
0733     } else {
0734         genPrefs->setText(i18n("Configure Backends..."));
0735     }
0736     genPrefs->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
0737     genPrefs->setEnabled(m_document->configurableGenerators() > 0);
0738     connect(genPrefs, &QAction::triggered, this, &Part::slotGeneratorPreferences);
0739 
0740     m_printPreview = KStandardAction::printPreview(this, SLOT(slotPrintPreview()), ac);
0741     m_printPreview->setEnabled(false);
0742 
0743     m_showLeftPanel = nullptr;
0744     m_showBottomBar = nullptr;
0745     m_showSignaturePanel = nullptr;
0746 
0747     m_showProperties = ac->addAction(QStringLiteral("properties"));
0748     m_showProperties->setText(i18n("&Properties"));
0749     m_showProperties->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
0750     ac->setDefaultShortcuts(m_showProperties, {QKeySequence(Qt::ALT | Qt::Key_Return)});
0751     connect(m_showProperties, &QAction::triggered, this, &Part::slotShowProperties);
0752     m_showProperties->setEnabled(false);
0753 
0754     m_showEmbeddedFiles = nullptr;
0755     m_showPresentation = nullptr;
0756 
0757     m_exportAs = nullptr;
0758     m_exportAsMenu = nullptr;
0759     m_exportAsText = nullptr;
0760     m_exportAsDocArchive = nullptr;
0761 
0762 #if HAVE_PURPOSE
0763     m_share = nullptr;
0764     m_shareMenu = nullptr;
0765 #endif
0766 
0767     m_presentationDrawingActions = nullptr;
0768 
0769     m_aboutBackend = ac->addAction(QStringLiteral("help_about_backend"));
0770     m_aboutBackend->setText(i18n("About Backend"));
0771     m_aboutBackend->setEnabled(false);
0772     connect(m_aboutBackend, &QAction::triggered, this, &Part::slotAboutBackend);
0773 
0774     QAction *reload = ac->add<QAction>(QStringLiteral("file_reload"));
0775     reload->setText(i18n("Reloa&d"));
0776     reload->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
0777     reload->setWhatsThis(i18n("Reload the current document from disk."));
0778     connect(reload, &QAction::triggered, this, &Part::slotReload);
0779     ac->setDefaultShortcuts(reload, KStandardShortcut::reload());
0780     m_reload = reload;
0781 
0782     m_closeFindBar = ac->addAction(QStringLiteral("close_find_bar"), this, SLOT(slotHideFindBar()));
0783     m_closeFindBar->setText(i18n("Close &Find Bar"));
0784     ac->setDefaultShortcut(m_closeFindBar, QKeySequence(Qt::Key_Escape));
0785     m_closeFindBar->setEnabled(false);
0786 
0787     QWidgetAction *pageno = new QWidgetAction(ac);
0788     pageno->setText(i18n("Page Number"));
0789     pageno->setDefaultWidget(m_pageNumberTool);
0790     ac->addAction(QStringLiteral("page_number"), pageno);
0791 
0792     QAction *configureColorModes = new QAction(i18nc("@action", "Configure Color Modes..."), ac);
0793     ac->addAction(QStringLiteral("options_configure_color_modes"), configureColorModes);
0794     configureColorModes->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
0795     connect(configureColorModes, &QAction::triggered, this, &Part::slotAccessibilityPreferences);
0796 }
0797 
0798 void Part::setViewerShortcuts()
0799 {
0800     KActionCollection *ac = actionCollection();
0801 
0802     ac->setDefaultShortcut(m_gotoPage, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_G));
0803     ac->setDefaultShortcut(m_find, QKeySequence());
0804 
0805     ac->setDefaultShortcut(m_findNext, QKeySequence());
0806     ac->setDefaultShortcut(m_findPrev, QKeySequence());
0807 
0808     ac->setDefaultShortcut(m_addBookmark, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_B));
0809 
0810     ac->setDefaultShortcut(m_beginningOfDocument, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Home));
0811     ac->setDefaultShortcut(m_endOfDocument, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_End));
0812 
0813     QAction *action = static_cast<QAction *>(ac->action(QStringLiteral("file_reload")));
0814     if (action) {
0815         ac->setDefaultShortcut(action, QKeySequence(Qt::ALT | Qt::Key_F5));
0816     }
0817 }
0818 
0819 void Part::setupActions()
0820 {
0821     KActionCollection *ac = actionCollection();
0822 
0823     auto manager = new KColorSchemeManager(this);
0824     KActionMenu *schemeMenu = KColorSchemeMenu::createMenu(manager, this);
0825     ac->addAction(QStringLiteral("colorscheme_menu"), schemeMenu->menu()->menuAction());
0826 
0827     m_copy = KStandardAction::create(KStandardAction::Copy, m_pageView, SLOT(copyTextSelection()), ac);
0828 
0829     m_selectAll = KStandardAction::selectAll(m_pageView, SLOT(selectAll()), ac);
0830 
0831     // Setup select all action for the current page
0832     m_selectCurrentPage = ac->addAction(QStringLiteral("edit_select_all_current_page"));
0833     m_selectCurrentPage->setText(i18n("Select All Text on Current Page"));
0834     connect(m_selectCurrentPage, &QAction::triggered, m_pageView, &PageView::slotSelectPage);
0835     m_selectCurrentPage->setEnabled(false);
0836 
0837     m_save = KStandardAction::save(
0838         this, [this] { saveFile(); }, ac);
0839     m_save->setEnabled(false);
0840 
0841     m_saveAs = KStandardAction::saveAs(this, SLOT(slotSaveFileAs()), ac);
0842     m_saveAs->setEnabled(false);
0843     m_migrationMessage->addAction(m_saveAs);
0844 
0845     m_showLeftPanel = ac->add<KToggleAction>(QStringLiteral("show_leftpanel"));
0846     m_showLeftPanel->setText(i18n("Show S&idebar"));
0847     const QString preferredSidebarIcon = m_sidebar->layoutDirection() == Qt::LeftToRight ? QStringLiteral("sidebar-expand-left") : QStringLiteral("sidebar-expand-right");
0848     m_showLeftPanel->setIcon(QIcon::fromTheme(preferredSidebarIcon, QIcon::fromTheme(QStringLiteral("view-sidetree"))));
0849     connect(m_showLeftPanel, &QAction::toggled, this, &Part::slotShowLeftPanel);
0850     ac->setDefaultShortcut(m_showLeftPanel, QKeySequence(Qt::Key_F7));
0851     m_showLeftPanel->setChecked(Okular::Settings::showLeftPanel());
0852     slotShowLeftPanel();
0853 
0854     m_showBottomBar = ac->add<KToggleAction>(QStringLiteral("show_bottombar"));
0855     m_showBottomBar->setText(i18n("Show &Page Bar"));
0856     connect(m_showBottomBar, &QAction::toggled, this, &Part::slotShowBottomBar);
0857     m_showBottomBar->setChecked(Okular::Settings::showBottomBar());
0858     slotShowBottomBar();
0859 
0860     m_showSignaturePanel = ac->add<QAction>(QStringLiteral("show_signatures"));
0861     m_showSignaturePanel->setText(i18n("Show &Signatures Panel"));
0862     connect(m_showSignaturePanel, &QAction::triggered, this, [this] {
0863         if (m_sidebar->currentItem() != m_signaturePanel) {
0864             m_sidebar->setCurrentItem(m_signaturePanel);
0865         }
0866         if (!m_showLeftPanel->isChecked()) {
0867             m_showLeftPanel->trigger();
0868         }
0869     });
0870 
0871     m_showEmbeddedFiles = ac->addAction(QStringLiteral("embedded_files"));
0872     m_showEmbeddedFiles->setText(i18n("&Embedded Files"));
0873     m_showEmbeddedFiles->setIcon(QIcon::fromTheme(QStringLiteral("mail-attachment")));
0874     connect(m_showEmbeddedFiles, &QAction::triggered, this, &Part::slotShowEmbeddedFiles);
0875     m_showEmbeddedFiles->setEnabled(false);
0876 
0877     m_exportAs = ac->addAction(QStringLiteral("file_export_as"));
0878     m_exportAs->setText(i18n("E&xport As"));
0879     m_exportAs->setIcon(QIcon::fromTheme(QStringLiteral("document-export")));
0880     m_exportAsMenu = new QMenu();
0881     connect(m_exportAsMenu, &QMenu::triggered, this, &Part::slotExportAs);
0882     m_exportAs->setMenu(m_exportAsMenu);
0883     m_exportAsText = actionForExportFormat(Okular::ExportFormat::standardFormat(Okular::ExportFormat::PlainText), m_exportAsMenu);
0884     m_exportAsMenu->addAction(m_exportAsText);
0885     m_exportAs->setEnabled(false);
0886     m_exportAsText->setEnabled(false);
0887 
0888 #if HAVE_PURPOSE
0889     m_share = ac->addAction(QStringLiteral("file_share"));
0890     m_share->setText(i18n("S&hare"));
0891     m_share->setIcon(QIcon::fromTheme(QStringLiteral("document-share")));
0892     m_share->setEnabled(false);
0893     m_shareMenu = new Purpose::Menu();
0894     connect(m_shareMenu, &Purpose::Menu::finished, this, &Part::slotShareActionFinished);
0895     m_share->setMenu(m_shareMenu);
0896 #endif
0897 
0898     m_showPresentation = ac->addAction(QStringLiteral("presentation"));
0899     m_showPresentation->setText(i18n("P&resentation"));
0900     m_showPresentation->setIcon(QIcon::fromTheme(QStringLiteral("view-presentation")));
0901     connect(m_showPresentation, &QAction::triggered, this, &Part::slotShowPresentation);
0902     ac->setDefaultShortcut(m_showPresentation, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_P));
0903     m_showPresentation->setEnabled(false);
0904 
0905     m_openContainingFolder = ac->addAction(QStringLiteral("open_containing_folder"));
0906     m_openContainingFolder->setText(i18n("Open Con&taining Folder"));
0907     m_openContainingFolder->setIcon(QIcon::fromTheme(QStringLiteral("document-open-folder")));
0908     connect(m_openContainingFolder, &QAction::triggered, this, &Part::slotOpenContainingFolder);
0909     m_openContainingFolder->setEnabled(false);
0910 
0911     if (m_embedMode == Okular::NativeShellMode) { // This hamburger menu is designed to be quite Okular-specific.
0912         m_hamburgerMenuAction = KStandardAction::hamburgerMenu(nullptr, nullptr, ac);
0913         if (auto *mainWindow = findMainWindow()) {
0914             m_hamburgerMenuAction->setMenuBar(mainWindow->menuBar());
0915         }
0916         connect(m_hamburgerMenuAction, &KHamburgerMenu::aboutToShowMenu, this, &Part::slotUpdateHamburgerMenu);
0917     }
0918 
0919     QAction *importPS = ac->addAction(QStringLiteral("import_ps"));
0920     importPS->setText(i18n("&Import PostScript as PDF..."));
0921     importPS->setIcon(QIcon::fromTheme(QStringLiteral("document-import")));
0922     connect(importPS, &QAction::triggered, this, &Part::slotImportPSFile);
0923 
0924     KToggleAction *blackscreenAction = new KToggleAction(i18n("Switch Blackscreen Mode"), ac);
0925     ac->addAction(QStringLiteral("switch_blackscreen_mode"), blackscreenAction);
0926     ac->setDefaultShortcut(blackscreenAction, QKeySequence(Qt::Key_B));
0927     blackscreenAction->setIcon(QIcon::fromTheme(QStringLiteral("view-presentation")));
0928     blackscreenAction->setEnabled(false);
0929 
0930     m_presentationDrawingActions = new DrawingToolActions(ac);
0931 
0932     QAction *eraseDrawingAction = new QAction(i18n("Erase Drawing"), ac);
0933     ac->addAction(QStringLiteral("presentation_erase_drawings"), eraseDrawingAction);
0934     eraseDrawingAction->setIcon(QIcon::fromTheme(QStringLiteral("draw-eraser-delete-objects")));
0935     eraseDrawingAction->setEnabled(false);
0936 
0937     QAction *configureAnnotations = new QAction(i18n("Configure Annotations..."), ac);
0938     ac->addAction(QStringLiteral("options_configure_annotations"), configureAnnotations);
0939     configureAnnotations->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
0940     connect(configureAnnotations, &QAction::triggered, this, &Part::slotAnnotationPreferences);
0941 
0942     QAction *playPauseAction = new QAction(i18n("Play/Pause Presentation"), ac);
0943     ac->addAction(QStringLiteral("presentation_play_pause"), playPauseAction);
0944     playPauseAction->setEnabled(false);
0945 }
0946 
0947 Part::~Part()
0948 {
0949 #if HAVE_DBUS
0950     QDBusConnection::sessionBus().unregisterObject(m_registerDbusName);
0951 #endif // HAVE_DBUS
0952 
0953     m_document->removeObserver(this);
0954 
0955     if (m_document->isOpened()) {
0956         Part::closeUrl(false);
0957     }
0958 
0959     delete m_toc;
0960     delete m_layers;
0961     delete m_pageView;
0962     delete m_thumbnailList;
0963     delete m_miniBar;
0964     delete m_pageNumberTool;
0965     delete m_miniBarLogic;
0966     delete m_bottomBar;
0967 #ifdef OKULAR_ENABLE_MINIBAR
0968     delete m_progressWidget;
0969 #endif
0970     delete m_pageSizeLabel;
0971     delete m_reviewsWidget;
0972     delete m_bookmarkList;
0973     delete m_infoTimer;
0974     delete m_signaturePanel;
0975 
0976     delete m_document;
0977 
0978     delete m_tempfile;
0979 
0980     qDeleteAll(m_bookmarkActions);
0981 
0982     delete m_exportAsMenu;
0983 #if HAVE_PURPOSE
0984     delete m_shareMenu;
0985 #endif
0986 
0987 #ifdef OKULAR_KEEP_FILE_OPEN
0988     delete m_keeper;
0989 #endif
0990 }
0991 
0992 bool Part::openDocument(const QUrl &url, uint page)
0993 {
0994     Okular::DocumentViewport vp(page - 1);
0995     vp.rePos.enabled = true;
0996     vp.rePos.normalizedX = 0;
0997     vp.rePos.normalizedY = 0;
0998     vp.rePos.pos = Okular::DocumentViewport::TopLeft;
0999     if (vp.isValid()) {
1000         m_document->setNextDocumentViewport(vp);
1001     }
1002     return openUrl(url);
1003 }
1004 
1005 void Part::startPresentation()
1006 {
1007     m_cliPresentation = true;
1008 }
1009 
1010 QStringList Part::supportedMimeTypes() const
1011 {
1012     return m_document->supportedMimeTypes();
1013 }
1014 
1015 QUrl Part::realUrl() const
1016 {
1017     if (!m_realUrl.isEmpty()) {
1018         return m_realUrl;
1019     }
1020 
1021     return url();
1022 }
1023 
1024 // ViewerInterface
1025 
1026 void Part::showSourceLocation(const QString &fileName, int line, int column, bool showGraphically)
1027 {
1028     Q_UNUSED(column);
1029 
1030     const QString u = QStringLiteral("src:%1 %2").arg(line + 1).arg(fileName);
1031     GotoAction action(QString(), u);
1032     m_document->processAction(&action);
1033     if (showGraphically) {
1034         m_pageView->setLastSourceLocationViewport(m_document->viewport());
1035     }
1036 }
1037 
1038 void Part::clearLastShownSourceLocation()
1039 {
1040     m_pageView->clearLastSourceLocationViewport();
1041 }
1042 
1043 bool Part::isWatchFileModeEnabled() const
1044 {
1045     return !m_watcher->signalsBlocked();
1046 }
1047 
1048 void Part::setWatchFileModeEnabled(bool enabled)
1049 {
1050     // Don't call 'KDirWatch::stopScan()' in here (as of KDE Frameworks 5.51.0, see bug 400541)!
1051     // 'KDirWatch::stopScan' has a bug that may affect other code paths that make use of KDirWatch
1052     // (other loaded KParts, for example).
1053     if (isWatchFileModeEnabled() == enabled) {
1054         return;
1055     }
1056 
1057     m_watcher->blockSignals(!enabled);
1058 
1059     if (!enabled) {
1060         m_dirtyHandler->stop();
1061     }
1062 }
1063 
1064 bool Part::areSourceLocationsShownGraphically() const
1065 {
1066     return m_pageView->areSourceLocationsShownGraphically();
1067 }
1068 
1069 void Part::setShowSourceLocationsGraphically(bool show)
1070 {
1071     m_pageView->setShowSourceLocationsGraphically(show);
1072 }
1073 
1074 bool Part::openNewFilesInTabs() const
1075 {
1076     return Okular::Settings::self()->shellOpenFileInTabs();
1077 }
1078 
1079 QWidget *Part::getSideContainer() const
1080 {
1081     return m_sidebar->getSideContainer();
1082 }
1083 
1084 bool Part::activateTabIfAlreadyOpenFile() const
1085 {
1086     return Okular::Settings::self()->switchToTabIfOpen();
1087 }
1088 
1089 void Part::setModified(bool modified)
1090 {
1091     KParts::ReadWritePart::setModified(modified);
1092 
1093     if (modified && !m_save->isEnabled()) {
1094         if (!m_warnedAboutModifyingUnsaveableDocument) {
1095             m_warnedAboutModifyingUnsaveableDocument = true;
1096             KMessageBox::information(widget(),
1097                                      i18n("You have just modified the open document, but this kind of document can not be saved.\nAny modification will be lost once Okular is closed."),
1098                                      i18n("Document can't be saved"),
1099                                      QStringLiteral("warnAboutUnsaveableDocuments"));
1100         }
1101     }
1102 }
1103 
1104 void Part::slotHandleActivatedSourceReference(const QString &absFileName, int line, int col, bool *handled)
1105 {
1106     Q_EMIT openSourceReference(absFileName, line, col);
1107     if (m_embedMode == Okular::ViewerWidgetMode) {
1108         *handled = true;
1109     }
1110 }
1111 
1112 void Part::openUrlFromDocument(const QUrl &url)
1113 {
1114     if (m_embedMode == PrintPreviewMode) {
1115         return;
1116     }
1117 
1118     if (url.isLocalFile()) {
1119         if (!QFile::exists(url.toLocalFile())) {
1120             KMessageBox::error(widget(), i18n("Could not open '%1'. File does not exist", url.toDisplayString()));
1121             return;
1122         }
1123     } else {
1124         KIO::StatJob *statJob = KIO::stat(url, KIO::StatJob::SourceSide);
1125         KJobWidgets::setWindow(statJob, widget());
1126         if (!statJob->exec() || statJob->error()) {
1127             KMessageBox::error(widget(), i18n("Could not open '%1' (%2) ", url.toDisplayString(), statJob->errorString()));
1128             return;
1129         }
1130     }
1131 
1132     Q_EMIT m_bExtension->openUrlNotify();
1133     Q_EMIT m_bExtension->setLocationBarUrl(url.toDisplayString());
1134     openUrl(url);
1135 }
1136 
1137 void Part::openUrlFromBookmarks(const QUrl &_url)
1138 {
1139     QUrl url = _url;
1140     Okular::DocumentViewport vp(_url.fragment(QUrl::FullyDecoded));
1141     if (vp.isValid()) {
1142         m_document->setNextDocumentViewport(vp);
1143     }
1144     url.setFragment(QString());
1145     if (m_document->currentDocument() == url) {
1146         if (vp.isValid()) {
1147             m_document->setViewport(vp);
1148         }
1149     } else {
1150         openUrl(url);
1151     }
1152 }
1153 
1154 void Part::handleDroppedUrls(const QList<QUrl> &urls)
1155 {
1156     if (urls.isEmpty()) {
1157         return;
1158     }
1159 
1160     if (m_embedMode != NativeShellMode || !openNewFilesInTabs()) {
1161         openUrlFromDocument(urls.first());
1162         return;
1163     }
1164 
1165     Q_EMIT urlsDropped(urls);
1166 }
1167 
1168 void Part::slotJobStarted(KIO::Job *job)
1169 {
1170     if (job) {
1171         QStringList supportedMimeTypes = m_document->supportedMimeTypes();
1172         job->addMetaData(QStringLiteral("accept"), supportedMimeTypes.join(QStringLiteral(", ")) + QStringLiteral(", */*;q=0.5"));
1173 
1174         connect(job, &KJob::result, this, &Part::slotJobFinished);
1175     }
1176 }
1177 
1178 void Part::slotJobFinished(KJob *job)
1179 {
1180     if (job->error() == KIO::ERR_USER_CANCELED) {
1181         m_pageView->displayMessage(i18n("The loading of %1 has been canceled.", realUrl().toDisplayString(QUrl::PreferLocalFile)));
1182     }
1183 }
1184 
1185 void Part::loadCancelled(const QString &reason)
1186 {
1187     Q_EMIT setWindowCaption(QString());
1188     resetStartArguments();
1189 
1190     // when m_viewportDirty.pageNumber != -1 we come from slotAttemptReload
1191     // so we don't want to show an ugly messagebox just because the document is
1192     // taking more than usual to be recreated
1193     if (m_viewportDirty.pageNumber == -1) {
1194         if (m_urlWithFragment.isValid() && !m_urlWithFragment.isLocalFile()) {
1195             tryOpeningUrlWithFragmentAsName();
1196         } else if (!reason.isEmpty()) {
1197             KMessageBox::error(widget(), i18n("Could not open %1. Reason: %2", url().toDisplayString(), reason));
1198         }
1199     }
1200 }
1201 
1202 void Part::setWindowTitleFromDocument()
1203 {
1204     // If 'DocumentTitle' should be used, check if the document has one. If
1205     // either case is false, use the file name.
1206     QString title = Okular::Settings::displayDocumentNameOrPath() == Okular::Settings::EnumDisplayDocumentNameOrPath::Path ? realUrl().toDisplayString(QUrl::PreferLocalFile) : realUrl().fileName();
1207 
1208     if (Okular::Settings::displayDocumentTitle()) {
1209         const QString docTitle = m_document->metaData(QStringLiteral("DocumentTitle")).toString();
1210         if (!docTitle.isEmpty() && !docTitle.trimmed().isEmpty()) {
1211             title = docTitle;
1212         }
1213     }
1214 
1215     Q_EMIT setWindowCaption(title);
1216 }
1217 
1218 KConfigDialog *Part::slotGeneratorPreferences()
1219 {
1220     // Create dialog
1221     KConfigDialog *dialog = new Okular::BackendConfigDialog(m_pageView, QStringLiteral("generator_prefs"), Okular::Settings::self());
1222     dialog->setAttribute(Qt::WA_DeleteOnClose);
1223 
1224     if (m_embedMode == ViewerWidgetMode) {
1225         dialog->setWindowTitle(i18n("Configure Viewer Backends"));
1226     } else {
1227         dialog->setWindowTitle(i18n("Configure Backends"));
1228     }
1229 
1230     m_document->fillConfigDialog(dialog);
1231 
1232     // Show it
1233     dialog->setWindowModality(Qt::ApplicationModal);
1234     dialog->show();
1235 
1236     return dialog;
1237 }
1238 
1239 void Part::notifySetup(const QVector<Okular::Page *> & /*pages*/, int setupFlags)
1240 {
1241     // Hide the migration message if the user has just migrated. Otherwise,
1242     // if m_migrationMessage is already hidden, this does nothing.
1243     if (!m_document->isDocdataMigrationNeeded()) {
1244         m_migrationMessage->animatedHide();
1245     }
1246 
1247     if (!(setupFlags & Okular::DocumentObserver::DocumentChanged)) {
1248         return;
1249     }
1250 
1251     rebuildBookmarkMenu();
1252     updateAboutBackendAction();
1253     m_findBar->resetSearch();
1254     m_searchWidget->setEnabled(m_document->supportsSearching());
1255 }
1256 
1257 void Part::notifyViewportChanged(bool /*smoothMove*/)
1258 {
1259     updateViewActions();
1260 }
1261 
1262 void Part::notifyPageChanged(int page, int flags)
1263 {
1264     if (!(flags & Okular::DocumentObserver::Bookmark)) {
1265         return;
1266     }
1267 
1268     rebuildBookmarkMenu();
1269     if (page == m_document->viewport().pageNumber) {
1270         updateBookmarksActions();
1271     }
1272 }
1273 
1274 void Part::goToPage(uint page)
1275 {
1276     if (page <= m_document->pages()) {
1277         m_document->setViewportPage(page - 1);
1278     }
1279 }
1280 
1281 void Part::openDocument(const QString &doc)
1282 {
1283     openUrl(QUrl::fromUserInput(doc));
1284 }
1285 
1286 uint Part::pages()
1287 {
1288     return m_document->pages();
1289 }
1290 
1291 uint Part::currentPage()
1292 {
1293     return m_document->pages() ? m_document->currentPage() + 1 : 0;
1294 }
1295 
1296 QString Part::currentDocument()
1297 {
1298     return m_document->currentDocument().toDisplayString(QUrl::PreferLocalFile);
1299 }
1300 
1301 QString Part::documentMetaData(const QString &metaData) const
1302 {
1303     const Okular::DocumentInfo info = m_document->documentInfo();
1304     return info.get(metaData);
1305 }
1306 
1307 bool Part::slotImportPSFile()
1308 {
1309     QString app = QStandardPaths::findExecutable(QStringLiteral("ps2pdf"));
1310     if (app.isEmpty()) {
1311         // TODO point the user to their distro packages?
1312         KMessageBox::error(widget(), i18n("The program \"ps2pdf\" was not found, so Okular can not import PS files using it."), i18n("ps2pdf not found"));
1313         return false;
1314     }
1315 
1316     QMimeDatabase mimeDatabase;
1317     QString filter = i18n("PostScript files (%1)", mimeDatabase.mimeTypeForName(QStringLiteral("application/postscript")).globPatterns().join(QLatin1Char(' ')));
1318 
1319     QUrl url = QFileDialog::getOpenFileUrl(widget(), QString(), QUrl(), filter);
1320     if (url.isLocalFile()) {
1321         QTemporaryFile tf(QDir::tempPath() + QLatin1String("/okular_XXXXXX.pdf"));
1322         tf.setAutoRemove(false);
1323         if (!tf.open()) {
1324             return false;
1325         }
1326         m_temporaryLocalFile = tf.fileName();
1327         tf.close();
1328 
1329         setLocalFilePath(url.toLocalFile());
1330         QStringList args;
1331         QProcess *p = new QProcess();
1332         args << url.toLocalFile() << m_temporaryLocalFile;
1333         m_pageView->displayMessage(i18n("Importing PS file as PDF (this may take a while)..."));
1334         connect(p, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &Part::psTransformEnded);
1335         p->start(app, args);
1336         return true;
1337     }
1338 
1339     m_temporaryLocalFile.clear();
1340     return false;
1341 }
1342 
1343 void Part::setFileToWatch(const QString &filePath)
1344 {
1345     if (!m_watchedFilePath.isEmpty()) {
1346         unsetFileToWatch();
1347     }
1348 
1349     const QFileInfo fi(filePath);
1350 
1351     m_watchedFilePath = filePath;
1352     m_watcher->addFile(m_watchedFilePath);
1353 
1354     if (fi.isSymLink()) {
1355         m_watchedFileSymlinkTarget = fi.symLinkTarget();
1356         m_watcher->addFile(m_watchedFileSymlinkTarget);
1357     } else {
1358         m_watchedFileSymlinkTarget.clear();
1359     }
1360 }
1361 
1362 void Part::unsetFileToWatch()
1363 {
1364     if (m_watchedFilePath.isEmpty()) {
1365         return;
1366     }
1367 
1368     m_watcher->removeFile(m_watchedFilePath);
1369 
1370     if (!m_watchedFileSymlinkTarget.isEmpty()) {
1371         m_watcher->removeFile(m_watchedFileSymlinkTarget);
1372     }
1373 
1374     m_watchedFilePath.clear();
1375     m_watchedFileSymlinkTarget.clear();
1376 }
1377 
1378 Document::OpenResult Part::doOpenFile(const QMimeType &mimeA, const QString &fileNameToOpenA, bool *isCompressedFile)
1379 {
1380     QMimeDatabase db;
1381     Document::OpenResult openResult = Document::OpenError;
1382     bool uncompressOk = true;
1383     QMimeType mime = mimeA;
1384     QString fileNameToOpen = fileNameToOpenA;
1385     KCompressionDevice::CompressionType compressionType = compressionTypeFor(mime.name());
1386     if (compressionType != KCompressionDevice::None) {
1387         *isCompressedFile = true;
1388         uncompressOk = handleCompressed(fileNameToOpen, localFilePath(), compressionType);
1389         mime = db.mimeTypeForFile(fileNameToOpen);
1390     } else {
1391         *isCompressedFile = false;
1392     }
1393 
1394     if (m_swapInsteadOfOpening) {
1395         m_swapInsteadOfOpening = false;
1396 
1397         if (!uncompressOk) {
1398             return Document::OpenError;
1399         }
1400 
1401         if (mime.inherits(QStringLiteral("application/vnd.kde.okular-archive"))) {
1402             isDocumentArchive = true;
1403             if (!m_document->swapBackingFileArchive(fileNameToOpen, url())) {
1404                 return Document::OpenError;
1405             }
1406         } else {
1407             isDocumentArchive = false;
1408             if (!m_document->swapBackingFile(fileNameToOpen, url())) {
1409                 return Document::OpenError;
1410             }
1411         }
1412 
1413         m_fileLastModified = QFileInfo(localFilePath()).lastModified();
1414         return Document::OpenSuccess;
1415     }
1416 
1417     isDocumentArchive = false;
1418     if (uncompressOk) {
1419         if (mime.inherits(QStringLiteral("application/vnd.kde.okular-archive"))) {
1420             openResult = m_document->openDocumentArchive(fileNameToOpen, url());
1421             isDocumentArchive = true;
1422         } else {
1423             openResult = m_document->openDocument(fileNameToOpen, url(), mime);
1424         }
1425         m_documentOpenWithPassword = false;
1426 
1427 #if HAVE_KWALLET
1428         // if the file didn't open correctly it might be encrypted, so ask for a pass
1429         QString walletName, walletFolder, walletKey;
1430         m_document->walletDataForFile(fileNameToOpen, &walletName, &walletFolder, &walletKey);
1431         bool firstInput = true;
1432         bool triedWallet = false;
1433         KWallet::Wallet *wallet = nullptr;
1434         bool keep = true;
1435         while (openResult == Document::OpenNeedsPassword) {
1436             QString password;
1437 
1438             // 1.A. try to retrieve the first password from the kde wallet system
1439             if (!triedWallet && !walletKey.isNull()) {
1440                 const WId parentwid = widget()->effectiveWinId();
1441                 wallet = KWallet::Wallet::openWallet(walletName, parentwid);
1442                 if (wallet) {
1443                     // use the KPdf folder (and create if missing)
1444                     if (!wallet->hasFolder(walletFolder)) {
1445                         wallet->createFolder(walletFolder);
1446                     }
1447                     wallet->setFolder(walletFolder);
1448 
1449                     // look for the pass in that folder
1450                     QString retrievedPass;
1451                     if (!wallet->readPassword(walletKey, retrievedPass)) {
1452                         password = retrievedPass;
1453                     }
1454                 }
1455                 triedWallet = true;
1456             }
1457 
1458             // 1.B. if not retrieved, ask the password using the kde password dialog
1459             if (password.isNull()) {
1460                 QString prompt;
1461                 if (firstInput) {
1462                     prompt = i18n("Please enter the password to read the document:");
1463                 } else {
1464                     prompt = i18n("Incorrect password. Try again:");
1465                 }
1466                 firstInput = false;
1467 
1468                 // if the user presses cancel, abort opening
1469                 KPasswordDialog dlg(widget(), wallet ? KPasswordDialog::ShowKeepPassword : KPasswordDialog::KPasswordDialogFlags());
1470                 dlg.setWindowTitle(i18n("Document Password"));
1471                 dlg.setPrompt(prompt);
1472                 if (!dlg.exec()) {
1473                     break;
1474                 }
1475                 password = dlg.password();
1476                 if (wallet) {
1477                     keep = dlg.keepPassword();
1478                 }
1479             }
1480 
1481             // 2. reopen the document using the password
1482             if (mime.inherits(QStringLiteral("application/vnd.kde.okular-archive"))) {
1483                 openResult = m_document->openDocumentArchive(fileNameToOpen, url(), password);
1484                 isDocumentArchive = true;
1485             } else {
1486                 openResult = m_document->openDocument(fileNameToOpen, url(), mime, password);
1487             }
1488 
1489             if (openResult == Document::OpenSuccess) {
1490                 m_documentOpenWithPassword = true;
1491 
1492                 // 3. if the password is correct and the user chose to remember it, store it to the wallet
1493                 if (wallet && /*safety check*/ wallet->isOpen() && keep) {
1494                     wallet->writePassword(walletKey, password);
1495                 }
1496             }
1497         }
1498 #endif
1499     }
1500 
1501     if (openResult == Document::OpenSuccess) {
1502         m_fileLastModified = QFileInfo(localFilePath()).lastModified();
1503         m_warnedAboutModifyingUnsaveableDocument = false;
1504     }
1505     return openResult;
1506 }
1507 
1508 bool Part::openFile()
1509 {
1510     QList<QMimeType> mimes;
1511     QString fileNameToOpen = localFilePath();
1512     const bool isstdin = url().isLocalFile() && url().fileName() == QLatin1String("-");
1513     const QFileInfo fileInfo(fileNameToOpen);
1514     if ((!isstdin) && (!fileInfo.exists())) {
1515         return false;
1516     }
1517     QMimeDatabase db;
1518     QMimeType pathMime = db.mimeTypeForFile(fileNameToOpen);
1519     if (!arguments().mimeType().isEmpty()) {
1520         QMimeType argMime = db.mimeTypeForName(arguments().mimeType());
1521 
1522         // Select the "childmost" mimetype, if none of them
1523         // inherits the other trust more what pathMime says
1524         // but still do a second try if that one fails
1525         if (argMime.inherits(pathMime.name())) {
1526             mimes << argMime;
1527         } else if (pathMime.inherits(argMime.name())) {
1528             mimes << pathMime;
1529         } else {
1530             mimes << pathMime << argMime;
1531         }
1532 
1533         // text is super annoying because it always succeeds when opening so try to make sure
1534         // that we don't set it as first mime unless we're really sure it is that.
1535         // If it could be something else based on the content we try that first but only if that content itself
1536         // is not text or if it's a supported text child like markdown
1537         if (mimes[0].inherits(QStringLiteral("text/plain"))) {
1538             const QMimeType contentMime = db.mimeTypeForFile(fileNameToOpen, QMimeDatabase::MatchContent);
1539             if (!contentMime.inherits(QStringLiteral("text/plain"))) {
1540                 mimes.prepend(contentMime);
1541             } else if (contentMime.name() != QLatin1String("text/plain")) {
1542                 const QStringList supportedMimes = m_document->supportedMimeTypes();
1543                 if (supportedMimes.contains(contentMime.name())) {
1544                     mimes.prepend(contentMime);
1545                 }
1546             }
1547         }
1548     } else {
1549         mimes << pathMime;
1550     }
1551 
1552     QMimeType mime;
1553     Document::OpenResult openResult = Document::OpenError;
1554     bool isCompressedFile = false;
1555     while (!mimes.isEmpty() && openResult == Document::OpenError) {
1556         mime = mimes.takeFirst();
1557         openResult = doOpenFile(mime, fileNameToOpen, &isCompressedFile);
1558     }
1559 
1560     bool canSearch = m_document->supportsSearching();
1561     Q_EMIT mimeTypeChanged(mime);
1562 
1563     // update one-time actions
1564     const bool ok = openResult == Document::OpenSuccess;
1565     Q_EMIT enableCloseAction(ok);
1566     m_find->setEnabled(ok && canSearch);
1567     m_findNext->setEnabled(ok && canSearch);
1568     m_findPrev->setEnabled(ok && canSearch);
1569     if (m_save) {
1570         m_save->setEnabled(ok && !(isstdin || mime.inherits(QStringLiteral("inode/directory"))));
1571     }
1572     if (m_saveAs) {
1573         m_saveAs->setEnabled(ok && !(isstdin || mime.inherits(QStringLiteral("inode/directory"))));
1574     }
1575     Q_EMIT enablePrintAction(ok && m_document->printingSupport() != Okular::Document::NoPrinting);
1576     m_printPreview->setEnabled(ok && m_document->printingSupport() != Okular::Document::NoPrinting);
1577     m_showProperties->setEnabled(ok);
1578     if (m_openContainingFolder) {
1579         m_openContainingFolder->setEnabled(ok);
1580     }
1581     bool hasEmbeddedFiles = ok && m_document->embeddedFiles() && m_document->embeddedFiles()->count() > 0;
1582     if (m_showEmbeddedFiles) {
1583         m_showEmbeddedFiles->setEnabled(hasEmbeddedFiles);
1584     }
1585     m_topMessage->setVisible(hasEmbeddedFiles && Okular::Settings::showEmbeddedContentMessages());
1586     m_migrationMessage->setVisible(m_document->isDocdataMigrationNeeded());
1587 
1588     // Warn the user that XFA forms are not supported yet (NOTE: poppler generator only)
1589     if (ok && Okular::Settings::showEmbeddedContentMessages() && m_document->metaData(QStringLiteral("HasUnsupportedXfaForm")).toBool() == true) {
1590         m_formsMessage->setText(i18n("This document has XFA forms, which are currently <b>unsupported</b>."));
1591         m_formsMessage->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning")));
1592         m_formsMessage->setMessageType(KMessageWidget::Warning);
1593         m_formsMessage->setVisible(true);
1594     }
1595     // m_pageView->toggleFormsAction() may be null on dummy mode
1596     else if (ok && Okular::Settings::showEmbeddedContentMessages() && m_pageView->toggleFormsAction() && m_pageView->toggleFormsAction()->isEnabled()) {
1597         m_formsMessage->setText(i18n("This document has forms. Click on the button to interact with them, or use View -> Show Forms."));
1598         m_formsMessage->setMessageType(KMessageWidget::Information);
1599         m_formsMessage->setVisible(true);
1600     } else {
1601         m_formsMessage->setVisible(false);
1602     }
1603 
1604     if (ok) {
1605         KMessageWidget::MessageType messageType;
1606         QString message;
1607 
1608         std::tie(messageType, message) = SignatureGuiUtils::documentSignatureMessageWidgetText(m_document);
1609 
1610         if (!message.isEmpty()) {
1611             if (m_embedMode == PrintPreviewMode) {
1612                 if (Okular::Settings::showEmbeddedContentMessages()) {
1613                     m_signatureMessage->setText(i18n("All editing and interactive features for this document are disabled. Please save a copy and reopen to edit this document."));
1614                     m_signatureMessage->setVisible(true);
1615                 }
1616             } else {
1617                 if (Okular::Settings::showEmbeddedContentMessages() || messageType > KMessageWidget::Information) {
1618                     m_signatureMessage->setMessageType(messageType);
1619                     m_signatureMessage->setText(message);
1620                     m_signatureMessage->setVisible(true);
1621                 }
1622             }
1623         }
1624     }
1625 
1626     if (m_showPresentation) {
1627         m_showPresentation->setEnabled(ok);
1628     }
1629     if (ok) {
1630         if (m_exportAs) {
1631             m_exportFormats = m_document->exportFormats();
1632             QList<Okular::ExportFormat>::ConstIterator it = m_exportFormats.constBegin();
1633             QList<Okular::ExportFormat>::ConstIterator itEnd = m_exportFormats.constEnd();
1634             QMenu *menu = m_exportAs->menu();
1635             for (; it != itEnd; ++it) {
1636                 menu->addAction(actionForExportFormat(*it));
1637             }
1638         }
1639 #if HAVE_PURPOSE
1640         if (m_share) {
1641             m_shareMenu->model()->setInputData(QJsonObject {{QStringLiteral("mimeType"), mime.name()}, {QStringLiteral("urls"), QJsonArray {url().toString()}}});
1642             m_shareMenu->model()->setPluginType(QStringLiteral("Export"));
1643             m_shareMenu->reload();
1644         }
1645 #endif
1646         if (isCompressedFile) {
1647             m_realUrl = url();
1648         }
1649 #ifdef OKULAR_KEEP_FILE_OPEN
1650         if (keepFileOpen())
1651             m_keeper->open(fileNameToOpen);
1652 #endif
1653 
1654         // Tries to find the text passed from terminal after the file is open
1655         if (!m_textToFindOnOpen.isEmpty()) {
1656             m_findBar->startSearch(m_textToFindOnOpen);
1657             m_textToFindOnOpen = QString();
1658         }
1659     }
1660     if (m_exportAsText) {
1661         m_exportAsText->setEnabled(ok && m_document->canExportToText());
1662     }
1663     if (m_exportAs) {
1664         m_exportAs->setEnabled(ok);
1665     }
1666 #if HAVE_PURPOSE
1667     if (m_share) {
1668         m_share->setEnabled(ok);
1669     }
1670 #endif
1671 
1672     // update viewing actions
1673     updateViewActions();
1674 
1675     m_fileWasRemoved = false;
1676 
1677     if (!ok) {
1678         // if can't open document, update windows so they display blank contents
1679         m_pageView->viewport()->update();
1680         m_thumbnailList->update();
1681         setUrl(QUrl());
1682         return false;
1683     }
1684 
1685     // set the file to the fileWatcher
1686     if (url().isLocalFile()) {
1687         setFileToWatch(localFilePath());
1688     }
1689 
1690     // if the 'OpenTOC' flag is set, open the TOC
1691     if (m_document->metaData(QStringLiteral("OpenTOC")).toBool() && m_tocEnabled && m_sidebar->currentItem() != m_toc) {
1692         m_sidebar->setCurrentItem(m_toc);
1693     }
1694     // if the 'StartFullScreen' flag is set and we're not in viewer widget mode, or the command line flag was
1695     // specified, start presentation
1696     const bool presentationBecauseOfDocumentMetadata = (m_embedMode != ViewerWidgetMode) && m_document->metaData(QStringLiteral("StartFullScreen")).toBool();
1697     if ((presentationBecauseOfDocumentMetadata || m_cliPresentation) && !m_isReloading) {
1698         bool goAheadWithPresentationMode = true;
1699         if (!m_cliPresentation) {
1700             const QString text = i18n(
1701                 "This document wants to be shown full screen.\n"
1702                 "Leave normal mode and enter presentation mode?");
1703             const QString caption = i18n("Request to Change Viewing Mode");
1704             const KGuiItem yesItem = KGuiItem(i18n("Enter Presentation Mode"), QStringLiteral("dialog-ok"));
1705             const KGuiItem noItem = KGuiItem(i18n("Deny Request"), QStringLiteral("dialog-cancel"));
1706             const int result = KMessageBox::questionTwoActions(widget(), text, caption, yesItem, noItem);
1707             if (result == KMessageBox::SecondaryAction) {
1708                 goAheadWithPresentationMode = false;
1709             }
1710         }
1711         m_cliPresentation = false;
1712         if (goAheadWithPresentationMode) {
1713             QMetaObject::invokeMethod(this, "slotShowPresentation", Qt::QueuedConnection);
1714         }
1715     }
1716     m_generatorGuiClient = factory() ? m_document->guiClient() : nullptr;
1717     if (m_generatorGuiClient) {
1718         factory()->addClient(m_generatorGuiClient);
1719     }
1720     if (m_cliPrint) {
1721         m_cliPrint = false;
1722         slotPrint();
1723     } else if (m_cliPrintAndExit) {
1724         slotPrint();
1725     }
1726     return true;
1727 }
1728 
1729 bool Part::openUrl(const QUrl &url)
1730 {
1731     return openUrl(url, false /* swapInsteadOfOpening */);
1732 }
1733 
1734 bool Part::openUrl(const QUrl &_url, bool swapInsteadOfOpening)
1735 {
1736     /* Store swapInsteadOfOpening, so that closeUrl and openFile will be able
1737      * to read it */
1738     m_swapInsteadOfOpening = swapInsteadOfOpening;
1739 
1740     // The subsequent call to closeUrl clears the arguments.
1741     // We want to save them and restore them later.
1742     const KParts::OpenUrlArguments args = arguments();
1743 
1744     // Close current document if any
1745     if (!closeUrl()) {
1746         return false;
1747     }
1748 
1749     setArguments(args);
1750 
1751     QUrl url(_url);
1752     if (url.hasFragment()) {
1753         m_urlWithFragment = _url;
1754         const QString dest = url.fragment(QUrl::FullyDecoded);
1755         bool ok = true;
1756         int page = dest.toInt(&ok);
1757 
1758         if (!ok) {
1759             const QStringList parameters = dest.split(QLatin1Char('&'));
1760             for (const QString &parameter : parameters) {
1761                 if (parameter.startsWith(QStringLiteral("page="), Qt::CaseInsensitive)) {
1762                     page = QStringView {dest}.mid(5).toInt(&ok);
1763                 }
1764             }
1765         }
1766 
1767         if (ok) {
1768             Okular::DocumentViewport vp(page - 1);
1769             vp.rePos.enabled = true;
1770             vp.rePos.normalizedX = 0;
1771             vp.rePos.normalizedY = 0;
1772             vp.rePos.pos = Okular::DocumentViewport::TopLeft;
1773             m_document->setNextDocumentViewport(vp);
1774         } else {
1775             m_document->setNextDocumentDestination(dest);
1776         }
1777         url.setFragment(QString());
1778     } else {
1779         m_urlWithFragment.clear();
1780     }
1781 
1782     // this calls in sequence the 'closeUrl' and 'openFile' methods
1783     bool openOk = KParts::ReadWritePart::openUrl(url);
1784 
1785     if (openOk) {
1786         m_viewportDirty.pageNumber = -1;
1787 
1788         setWindowTitleFromDocument();
1789     } else {
1790         if (m_urlWithFragment.isValid() && m_urlWithFragment.isLocalFile()) {
1791             openOk = tryOpeningUrlWithFragmentAsName();
1792         } else {
1793             resetStartArguments();
1794             /* TRANSLATORS: Adding the reason (%2) why the opening failed (if any). */
1795             QString errorMessage = i18n("Could not open %1. %2", url.toDisplayString(), QStringLiteral("\n%1").arg(m_document->openError()));
1796             KMessageBox::error(widget(), errorMessage);
1797         }
1798     }
1799 
1800     return openOk;
1801 }
1802 
1803 bool Part::tryOpeningUrlWithFragmentAsName()
1804 {
1805     QUrl url = m_urlWithFragment;
1806     url.setPath(url.path() + QLatin1Char('#') + url.fragment());
1807     url.setFragment(QString());
1808     return openUrl(url);
1809 }
1810 
1811 bool Part::queryClose()
1812 {
1813     if (!isReadWrite() || !isModified()) {
1814         return true;
1815     }
1816 
1817     // TODO When we get different saving backends we need to query the backend
1818     // as to if it can save changes even if the open file has been modified,
1819     // since we only have poppler as saving backend for now we're skipping that check
1820     if (m_fileLastModified != QFileInfo(localFilePath()).lastModified()) {
1821         int res;
1822         if (m_isReloading) {
1823             res = KMessageBox::warningContinueCancel(widget(),
1824                                                      xi18nc("@info",
1825                                                             "The file <filename>%1</filename> has unsaved changes but has been modified by another program. Reloading it "
1826                                                             "will replace the unsaved changes with the changes made in the other "
1827                                                             "program.<nl/><nl/>Do you want to continue reloading the file?",
1828                                                             url().fileName()),
1829                                                      i18n("File Changed"),
1830                                                      KGuiItem(i18n("Continue Reloading")), // <- KMessageBox::Continue
1831                                                      KGuiItem(i18n("Abort Reloading")));
1832         } else {
1833             res = KMessageBox::warningContinueCancel(widget(),
1834                                                      xi18nc("@info",
1835                                                             "The file <filename>%1</filename> has unsaved changes but has been modified by another program. Closing it "
1836                                                             "will replace the unsaved changes with the changes made in the other "
1837                                                             "program.<nl/><nl/>Do you want to continue closing the file?",
1838                                                             url().fileName()),
1839                                                      i18n("File Changed"),
1840                                                      KGuiItem(i18n("Continue Closing")), // <- KMessageBox::Continue
1841                                                      KGuiItem(i18n("Abort Closing")));
1842         }
1843         return res == KMessageBox::Continue;
1844     }
1845 
1846     // Not all things are saveable (e.g. files opened from stdin, folders)
1847     if (m_save->isEnabled()) {
1848         const int res = KMessageBox::warningTwoActionsCancel(widget(), i18n("Do you want to save your changes to \"%1\" or discard them?", url().fileName()), i18n("Close Document"), KStandardGuiItem::save(), KStandardGuiItem::discard());
1849 
1850         switch (res) {
1851         case KMessageBox::PrimaryAction: // Save
1852             saveFile();
1853             return !isModified();          // Only allow closing if file was really saved
1854         case KMessageBox::SecondaryAction: // Discard
1855             return true;
1856         default: // Cancel
1857             return false;
1858         }
1859     } else {
1860         return true;
1861     }
1862 }
1863 
1864 bool Part::closeUrl(bool promptToSave)
1865 {
1866     if (promptToSave && !queryClose()) {
1867         return false;
1868     }
1869 
1870     if (m_swapInsteadOfOpening) {
1871         // If we're swapping the backing file, we don't want to close the
1872         // current one when openUrl() calls us internally
1873         return true; // pretend it worked
1874     }
1875 
1876     m_document->setHistoryClean(true);
1877 
1878     if (!m_temporaryLocalFile.isNull() && m_temporaryLocalFile != localFilePath()) {
1879         QFile::remove(m_temporaryLocalFile);
1880         m_temporaryLocalFile.clear();
1881     }
1882 
1883     slotHidePresentation();
1884     Q_EMIT enableCloseAction(false);
1885     m_find->setEnabled(false);
1886     m_findNext->setEnabled(false);
1887     m_findPrev->setEnabled(false);
1888     if (m_save) {
1889         m_save->setEnabled(false);
1890     }
1891     if (m_saveAs) {
1892         m_saveAs->setEnabled(false);
1893     }
1894     m_printPreview->setEnabled(false);
1895     m_showProperties->setEnabled(false);
1896     if (m_showEmbeddedFiles) {
1897         m_showEmbeddedFiles->setEnabled(false);
1898     }
1899     if (m_exportAs) {
1900         m_exportAs->setEnabled(false);
1901     }
1902     if (m_exportAsText) {
1903         m_exportAsText->setEnabled(false);
1904     }
1905     m_exportFormats.clear();
1906     if (m_exportAs) {
1907         QMenu *menu = m_exportAs->menu();
1908         QList<QAction *> acts = menu->actions();
1909         int num = acts.count();
1910         for (int i = 1; i < num; ++i) {
1911             menu->removeAction(acts.at(i));
1912             delete acts.at(i);
1913         }
1914     }
1915 #if HAVE_PURPOSE
1916     if (m_share) {
1917         m_share->setEnabled(false);
1918         m_shareMenu->clear();
1919     }
1920 #endif
1921     if (m_showPresentation) {
1922         m_showPresentation->setEnabled(false);
1923     }
1924     Q_EMIT setWindowCaption(QLatin1String(""));
1925     Q_EMIT enablePrintAction(false);
1926     m_realUrl = QUrl();
1927     if (url().isLocalFile()) {
1928         unsetFileToWatch();
1929     }
1930     m_fileWasRemoved = false;
1931     if (m_generatorGuiClient) {
1932         factory()->removeClient(m_generatorGuiClient);
1933     }
1934     m_generatorGuiClient = nullptr;
1935     m_document->closeDocument();
1936     m_fileLastModified = QDateTime();
1937     updateViewActions();
1938     delete m_tempfile;
1939     m_tempfile = nullptr;
1940     if (widget()) {
1941         m_searchWidget->clearText();
1942         m_migrationMessage->setVisible(false);
1943         m_topMessage->setVisible(false);
1944         m_formsMessage->setVisible(false);
1945         m_signatureMessage->setVisible(false);
1946     }
1947 #ifdef OKULAR_KEEP_FILE_OPEN
1948     m_keeper->close();
1949 #endif
1950     bool r = KParts::ReadWritePart::closeUrl();
1951     setUrl(QUrl());
1952 
1953     return r;
1954 }
1955 
1956 bool Part::closeUrl()
1957 {
1958     return closeUrl(true);
1959 }
1960 
1961 void Part::guiActivateEvent(KParts::GUIActivateEvent *event)
1962 {
1963     updateViewActions();
1964 
1965     KParts::ReadWritePart::guiActivateEvent(event);
1966 
1967     setWindowTitleFromDocument();
1968 
1969     if (event->activated()) {
1970         m_pageView->setupActionsPostGUIActivated();
1971         rebuildBookmarkMenu();
1972     }
1973 }
1974 
1975 void Part::close()
1976 {
1977     if (m_embedMode == NativeShellMode) {
1978         closeUrl();
1979     } else {
1980         KMessageBox::information(widget(), i18n("This link points to a close document action that does not work when using the embedded viewer."), QString(), QStringLiteral("warnNoCloseIfNotInOkular"));
1981     }
1982 }
1983 
1984 void Part::cannotQuit()
1985 {
1986     KMessageBox::information(widget(), i18n("This link points to a quit application action that does not work when using the embedded viewer."), QString(), QStringLiteral("warnNoQuitIfNotInOkular"));
1987 }
1988 
1989 void Part::slotShowLeftPanel()
1990 {
1991     bool showLeft = m_showLeftPanel->isChecked();
1992     Okular::Settings::setShowLeftPanel(showLeft);
1993     Okular::Settings::self()->save();
1994     // show/hide left panel
1995     m_sidebar->setSidebarVisibility(showLeft);
1996 }
1997 
1998 void Part::slotShowBottomBar()
1999 {
2000     const bool showBottom = m_showBottomBar->isChecked();
2001     Okular::Settings::setShowBottomBar(showBottom);
2002     Okular::Settings::self()->save();
2003     // show/hide bottom bar
2004     m_bottomBar->setVisible(showBottom);
2005 }
2006 
2007 void Part::slotFileDirty(const QString &path)
2008 {
2009     // The beauty of this is that each start cancels the previous one.
2010     // This means that timeout() is only fired when there have
2011     // no changes to the file for the last 750 millisecs.
2012     // This ensures that we don't update on every other byte that gets
2013     // written to the file.
2014     if (path == localFilePath()) {
2015         // Only start watching the file in case if it wasn't removed
2016         if (QFile::exists(localFilePath())) {
2017             m_dirtyHandler->start(750);
2018         } else {
2019             m_fileWasRemoved = true;
2020         }
2021     } else {
2022         const QFileInfo fi(localFilePath());
2023         if (fi.absolutePath() == path) {
2024             // Our parent has been dirtified
2025             if (!QFile::exists(localFilePath())) {
2026                 m_fileWasRemoved = true;
2027             } else if (m_fileWasRemoved && QFile::exists(localFilePath())) {
2028                 // we need to watch the new file
2029                 unsetFileToWatch();
2030                 setFileToWatch(localFilePath());
2031                 m_dirtyHandler->start(750);
2032             }
2033         } else if (fi.isSymLink() && fi.symLinkTarget() == path) {
2034             if (QFile::exists(fi.symLinkTarget())) {
2035                 m_dirtyHandler->start(750);
2036             } else {
2037                 m_fileWasRemoved = true;
2038             }
2039         }
2040     }
2041 }
2042 
2043 // Attempt to reload the document, one or more times, optionally from a different URL
2044 bool Part::slotAttemptReload(bool oneShot, const QUrl &newUrl)
2045 {
2046     // Skip reload when another reload is already in progress
2047     if (m_isReloading) {
2048         return false;
2049     }
2050     QScopedValueRollback<bool> rollback(m_isReloading, true);
2051 
2052     bool tocReloadPrepared = false;
2053 
2054     // do the following the first time the file is reloaded
2055     if (m_viewportDirty.pageNumber == -1) {
2056         // store the url of the current document
2057         m_oldUrl = newUrl.isEmpty() ? url() : newUrl;
2058 
2059         // store the current viewport
2060         m_viewportDirty = m_document->viewport();
2061 
2062         // store the current toolbox pane
2063         m_dirtyToolboxItem = m_sidebar->currentItem();
2064         m_wasSidebarVisible = m_sidebar->isSidebarVisible();
2065 
2066         // store if presentation view was open
2067         m_wasPresentationOpen = (m_presentationWidget != nullptr);
2068 
2069         // preserves the toc state after reload
2070         m_toc->prepareForReload();
2071         tocReloadPrepared = true;
2072 
2073         // store the page rotation
2074         m_dirtyPageRotation = m_document->rotation();
2075 
2076         // inform the user about the operation in progress
2077         // TODO: Remove this line and integrate reload info in queryClose
2078         m_pageView->displayMessage(i18n("Reloading the document..."));
2079     }
2080 
2081     // close and (try to) reopen the document
2082     if (!closeUrl()) {
2083         m_viewportDirty.pageNumber = -1;
2084 
2085         if (tocReloadPrepared) {
2086             m_toc->rollbackReload();
2087         }
2088         return false;
2089     }
2090 
2091     if (tocReloadPrepared) {
2092         m_toc->finishReload();
2093     }
2094 
2095     // inform the user about the operation in progress
2096     m_pageView->displayMessage(i18n("Reloading the document..."));
2097 
2098     bool reloadSucceeded = false;
2099 
2100     if (KParts::ReadWritePart::openUrl(m_oldUrl)) {
2101         // on successful opening, restore the previous viewport
2102         if (m_viewportDirty.pageNumber >= (int)m_document->pages()) {
2103             m_viewportDirty.pageNumber = (int)m_document->pages() - 1;
2104         }
2105         m_document->setViewport(m_viewportDirty);
2106         m_oldUrl = QUrl();
2107         m_viewportDirty.pageNumber = -1;
2108         m_document->setRotation(m_dirtyPageRotation);
2109         if (m_sidebar->currentItem() != m_dirtyToolboxItem) {
2110             m_sidebar->setCurrentItem(m_dirtyToolboxItem);
2111         }
2112         if (m_sidebar->isSidebarVisible() != m_wasSidebarVisible) {
2113             m_sidebar->setSidebarVisibility(m_wasSidebarVisible);
2114         }
2115         if (m_wasPresentationOpen) {
2116             slotShowPresentation();
2117         }
2118         Q_EMIT enablePrintAction(true && m_document->printingSupport() != Okular::Document::NoPrinting);
2119 
2120         reloadSucceeded = true;
2121     } else if (!oneShot) {
2122         // start watching the file again (since we dropped it on close)
2123         setFileToWatch(localFilePath());
2124         m_dirtyHandler->start(750);
2125     }
2126 
2127     return reloadSucceeded;
2128 }
2129 
2130 void Part::updateViewActions()
2131 {
2132     bool opened = m_document->pages() > 0;
2133     if (opened) {
2134         m_gotoPage->setEnabled(m_document->pages() > 1);
2135 
2136         // Check if you are at the beginning or not
2137         if (m_document->currentPage() != 0) {
2138             m_beginningOfDocument->setEnabled(true);
2139             m_prevPage->setEnabled(true);
2140         } else {
2141             if (m_pageView->verticalScrollBar()->value() != 0) {
2142                 // The page isn't at the very beginning
2143                 m_beginningOfDocument->setEnabled(true);
2144             } else {
2145                 // The page is at the very beginning of the document
2146                 m_beginningOfDocument->setEnabled(false);
2147             }
2148             // The document is at the first page, you can go to a page before
2149             m_prevPage->setEnabled(false);
2150         }
2151 
2152         if (m_document->pages() == m_document->currentPage() + 1) {
2153             // If you are at the end, disable go to next page
2154             m_nextPage->setEnabled(false);
2155             if (m_pageView->verticalScrollBar()->value() == m_pageView->verticalScrollBar()->maximum()) {
2156                 // If you are the end of the page of the last document, you can't go to the last page
2157                 m_endOfDocument->setEnabled(false);
2158             } else {
2159                 // Otherwise you can move to the endif
2160                 m_endOfDocument->setEnabled(true);
2161             }
2162         } else {
2163             // If you are not at the end, enable go to next page
2164             m_nextPage->setEnabled(true);
2165             m_endOfDocument->setEnabled(true);
2166         }
2167 
2168         if (m_historyBack) {
2169             m_historyBack->setEnabled(!m_document->historyAtBegin());
2170         }
2171         if (m_historyNext) {
2172             m_historyNext->setEnabled(!m_document->historyAtEnd());
2173         }
2174         m_reload->setEnabled(true);
2175         if (m_copy) {
2176             m_copy->setEnabled(true);
2177         }
2178         if (m_selectAll) {
2179             m_selectAll->setEnabled(true);
2180         }
2181         if (m_selectCurrentPage) {
2182             m_selectCurrentPage->setEnabled(true);
2183         }
2184     } else {
2185         m_gotoPage->setEnabled(false);
2186         m_beginningOfDocument->setEnabled(false);
2187         m_endOfDocument->setEnabled(false);
2188         m_prevPage->setEnabled(false);
2189         m_nextPage->setEnabled(false);
2190         if (m_historyBack) {
2191             m_historyBack->setEnabled(false);
2192         }
2193         if (m_historyNext) {
2194             m_historyNext->setEnabled(false);
2195         }
2196         m_reload->setEnabled(false);
2197         if (m_copy) {
2198             m_copy->setEnabled(false);
2199         }
2200         if (m_selectAll) {
2201             m_selectAll->setEnabled(false);
2202         }
2203         if (m_selectCurrentPage) {
2204             m_selectCurrentPage->setEnabled(false);
2205         }
2206     }
2207 
2208     if (factory()) {
2209         QWidget *menu = factory()->container(QStringLiteral("menu_okular_part_viewer"), this);
2210         if (menu) {
2211             menu->setEnabled(opened);
2212         }
2213 
2214         menu = factory()->container(QStringLiteral("view_orientation"), this);
2215         if (menu) {
2216             menu->setEnabled(opened);
2217         }
2218     }
2219     Q_EMIT viewerMenuStateChange(opened);
2220 
2221     updateBookmarksActions();
2222 }
2223 
2224 void Part::updateBookmarksActions()
2225 {
2226     bool opened = m_document->pages() > 0;
2227     if (opened) {
2228         m_addBookmark->setEnabled(true);
2229         if (m_document->bookmarkManager()->isBookmarked(m_document->viewport())) {
2230             m_addBookmark->setText(i18n("Remove Bookmark"));
2231             m_addBookmark->setIcon(QIcon::fromTheme(QStringLiteral("bookmark-remove"), QIcon::fromTheme(QStringLiteral("edit-delete-bookmark"))));
2232             m_renameBookmark->setEnabled(true);
2233         } else {
2234             m_addBookmark->setText(m_addBookmarkText);
2235             m_addBookmark->setIcon(m_addBookmarkIcon);
2236             m_renameBookmark->setEnabled(false);
2237         }
2238     } else {
2239         m_addBookmark->setEnabled(false);
2240         m_addBookmark->setText(m_addBookmarkText);
2241         m_addBookmark->setIcon(m_addBookmarkIcon);
2242         m_renameBookmark->setEnabled(false);
2243     }
2244 }
2245 
2246 void Part::enableTOC(bool enable)
2247 {
2248     if (!enable) {
2249         m_tocEnabled = false;
2250         return;
2251     }
2252 
2253     m_sidebar->addItem(m_toc, QIcon::fromTheme(QApplication::isLeftToRight() ? QStringLiteral("format-justify-left") : QStringLiteral("format-justify-right")), i18n("Contents"));
2254     m_tocEnabled = true;
2255 
2256     // If present, show the TOC when a document is opened
2257     if (m_sidebar->currentItem() != m_toc) {
2258         m_sidebar->setCurrentItem(m_toc);
2259     }
2260 }
2261 
2262 void Part::slotRebuildBookmarkMenu()
2263 {
2264     rebuildBookmarkMenu();
2265 }
2266 
2267 void Part::enableLayers(bool enable)
2268 {
2269     if (!enable) {
2270         return;
2271     }
2272 
2273     m_sidebar->addItem(m_layers, QIcon::fromTheme(QStringLiteral("format-list-unordered")), i18n("Layers"));
2274 }
2275 
2276 void Part::enableSidebarSignaturesItem(bool enable)
2277 {
2278     if (!enable) {
2279         return;
2280     }
2281 
2282     m_sidebar->addItem(m_signaturePanel, QIcon::fromTheme(QStringLiteral("application-pkcs7-signature")), i18n("Signatures"));
2283 }
2284 
2285 void Part::slotShowFindBar()
2286 {
2287     m_findBar->show();
2288     m_findBar->focusAndSetCursor();
2289     m_closeFindBar->setEnabled(true);
2290 }
2291 
2292 void Part::slotHideFindBar()
2293 {
2294     if (m_findBar->maybeHide()) {
2295         m_pageView->setFocus();
2296         m_closeFindBar->setEnabled(false);
2297     }
2298 }
2299 
2300 // BEGIN go to page dialog
2301 class GotoPageDialog : public QDialog
2302 {
2303     Q_OBJECT
2304 
2305 public:
2306     GotoPageDialog(QWidget *p, int current, int max)
2307         : QDialog(p)
2308     {
2309         setWindowTitle(i18n("Go to Page"));
2310         buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
2311         connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
2312         connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
2313 
2314         QVBoxLayout *topLayout = new QVBoxLayout(this);
2315         topLayout->setContentsMargins(6, 6, 6, 6);
2316         QHBoxLayout *midLayout = new QHBoxLayout();
2317         spinbox = new QSpinBox(this);
2318         spinbox->setRange(1, max);
2319         spinbox->setValue(current);
2320         spinbox->setFocus();
2321 
2322         slider = new QSlider(Qt::Horizontal, this);
2323         slider->setRange(1, max);
2324         slider->setValue(current);
2325         slider->setSingleStep(1);
2326         slider->setTickPosition(QSlider::TicksBelow);
2327         slider->setTickInterval(max / 10);
2328 
2329         connect(slider, &QSlider::valueChanged, spinbox, &QSpinBox::setValue);
2330         connect(spinbox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), slider, &QSlider::setValue);
2331 
2332         QLabel *label = new QLabel(i18n("&Page:"), this);
2333         label->setBuddy(spinbox);
2334         topLayout->addWidget(label);
2335         topLayout->addLayout(midLayout);
2336         midLayout->addWidget(slider);
2337         midLayout->addWidget(spinbox);
2338 
2339         // A little bit extra space
2340         topLayout->addStretch(10);
2341         topLayout->addWidget(buttonBox);
2342         spinbox->setFocus();
2343     }
2344 
2345     int getPage() const
2346     {
2347         return spinbox->value();
2348     }
2349 
2350 protected:
2351     QSpinBox *spinbox;
2352     QSlider *slider;
2353     QDialogButtonBox *buttonBox;
2354 };
2355 // END go to page dialog
2356 
2357 void Part::slotGoToPage()
2358 {
2359     GotoPageDialog pageDialog(m_pageView, m_document->currentPage() + 1, m_document->pages());
2360     if (pageDialog.exec() == QDialog::Accepted) {
2361         m_document->setViewportPage(pageDialog.getPage() - 1, nullptr, true);
2362     }
2363 }
2364 
2365 void Part::slotPreviousPage()
2366 {
2367     if (m_document->isOpened() && !(m_document->currentPage() < 1)) {
2368         m_document->setViewportPage(m_document->currentPage() - 1, nullptr, true);
2369     }
2370 }
2371 
2372 void Part::slotNextPage()
2373 {
2374     if (m_document->isOpened() && m_document->currentPage() < (m_document->pages() - 1)) {
2375         m_document->setViewportPage(m_document->currentPage() + 1, nullptr, true);
2376     }
2377 }
2378 
2379 void Part::slotGotoFirst()
2380 {
2381     if (m_document->isOpened()) {
2382         m_document->setViewportPage(0, nullptr, true);
2383         m_beginningOfDocument->setEnabled(false);
2384     }
2385 }
2386 
2387 void Part::slotGotoLast()
2388 {
2389     if (m_document->isOpened()) {
2390         DocumentViewport endPage(m_document->pages() - 1);
2391         endPage.rePos.enabled = true;
2392         endPage.rePos.normalizedX = 0;
2393         endPage.rePos.normalizedY = 1;
2394         endPage.rePos.pos = Okular::DocumentViewport::TopLeft;
2395         m_document->setViewport(endPage, nullptr, true);
2396         m_endOfDocument->setEnabled(false);
2397     }
2398 }
2399 
2400 void Part::slotHistoryBack()
2401 {
2402     m_document->setPrevViewport();
2403 }
2404 
2405 void Part::slotHistoryNext()
2406 {
2407     m_document->setNextViewport();
2408 }
2409 
2410 void Part::slotAddBookmark()
2411 {
2412     DocumentViewport vp = m_document->viewport();
2413     if (m_document->bookmarkManager()->isBookmarked(vp)) {
2414         m_document->bookmarkManager()->removeBookmark(vp);
2415     } else {
2416         m_document->bookmarkManager()->addBookmark(vp);
2417     }
2418 }
2419 
2420 void Part::slotRenameBookmark(const DocumentViewport &viewport)
2421 {
2422     Q_ASSERT(m_document->bookmarkManager()->isBookmarked(viewport));
2423     if (m_document->bookmarkManager()->isBookmarked(viewport)) {
2424         KBookmark bookmark = m_document->bookmarkManager()->bookmark(viewport);
2425         const QString newName = QInputDialog::getText(widget(), i18n("Rename Bookmark"), i18n("Enter the new name of the bookmark:"), QLineEdit::Normal, bookmark.fullText());
2426         if (!newName.isEmpty()) {
2427             m_document->bookmarkManager()->renameBookmark(&bookmark, newName);
2428         }
2429     }
2430 }
2431 
2432 void Part::slotRenameBookmarkFromMenu()
2433 {
2434     QAction *action = dynamic_cast<QAction *>(sender());
2435     Q_ASSERT(action);
2436     if (action) {
2437         DocumentViewport vp(action->data().toString());
2438         slotRenameBookmark(vp);
2439     }
2440 }
2441 
2442 void Part::slotRemoveBookmarkFromMenu()
2443 {
2444     QAction *action = dynamic_cast<QAction *>(sender());
2445     Q_ASSERT(action);
2446     if (action) {
2447         DocumentViewport vp(action->data().toString());
2448         slotRemoveBookmark(vp);
2449     }
2450 }
2451 
2452 void Part::slotRemoveBookmark(const DocumentViewport &viewport)
2453 {
2454     Q_ASSERT(m_document->bookmarkManager()->isBookmarked(viewport));
2455     if (m_document->bookmarkManager()->isBookmarked(viewport)) {
2456         m_document->bookmarkManager()->removeBookmark(viewport);
2457     }
2458 }
2459 
2460 void Part::slotRenameCurrentViewportBookmark()
2461 {
2462     slotRenameBookmark(m_document->viewport());
2463 }
2464 
2465 bool Part::aboutToShowContextMenu(QMenu * /*menu*/, QAction *action, QMenu *contextMenu)
2466 {
2467     KBookmarkAction *ba = dynamic_cast<KBookmarkAction *>(action);
2468     if (ba != nullptr) {
2469         QAction *separatorAction = contextMenu->addSeparator();
2470         separatorAction->setObjectName(QStringLiteral("OkularPrivateRenameBookmarkActions"));
2471         QAction *renameAction = contextMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename this Bookmark"), this, &Part::slotRenameBookmarkFromMenu);
2472         renameAction->setData(ba->property("htmlRef").toString());
2473         renameAction->setObjectName(QStringLiteral("OkularPrivateRenameBookmarkActions"));
2474         QAction *deleteAction = contextMenu->addAction(QIcon::fromTheme(QStringLiteral("bookmark-remove"), QIcon::fromTheme(QStringLiteral("edit-delete-bookmark"))), i18n("Remove this Bookmark"), this, &Part::slotRemoveBookmarkFromMenu);
2475         deleteAction->setData(ba->property("htmlRef").toString());
2476         deleteAction->setObjectName(QStringLiteral("OkularPrivateRenameBookmarkActions"));
2477     }
2478     return ba;
2479 }
2480 
2481 void Part::slotPreviousBookmark()
2482 {
2483     const KBookmark bookmark = m_document->bookmarkManager()->previousBookmark(m_document->viewport());
2484 
2485     if (!bookmark.isNull()) {
2486         DocumentViewport vp(bookmark.url().fragment(QUrl::FullyDecoded));
2487         m_document->setViewport(vp, nullptr, true);
2488     }
2489 }
2490 
2491 void Part::slotNextBookmark()
2492 {
2493     const KBookmark bookmark = m_document->bookmarkManager()->nextBookmark(m_document->viewport());
2494 
2495     if (!bookmark.isNull()) {
2496         DocumentViewport vp(bookmark.url().fragment(QUrl::FullyDecoded));
2497         m_document->setViewport(vp, nullptr, true);
2498     }
2499 }
2500 
2501 void Part::slotFind()
2502 {
2503     // when in presentation mode, there's already a search bar, taking care of
2504     // the 'find' requests
2505     if (m_presentationWidget != nullptr) {
2506         m_presentationWidget->slotFind();
2507     } else {
2508         slotShowFindBar();
2509     }
2510 }
2511 
2512 void Part::slotFindNext()
2513 {
2514     if (m_findBar->isHidden()) {
2515         slotShowFindBar();
2516     } else {
2517         m_findBar->findNext();
2518     }
2519 }
2520 
2521 void Part::slotFindPrev()
2522 {
2523     if (m_findBar->isHidden()) {
2524         slotShowFindBar();
2525     } else {
2526         m_findBar->findPrev();
2527     }
2528 }
2529 
2530 bool Part::saveFile()
2531 {
2532     if (!isModified()) {
2533         return true;
2534     } else {
2535         return saveAs(url());
2536     }
2537 }
2538 
2539 bool Part::slotSaveFileAs(bool showOkularArchiveAsDefaultFormat)
2540 {
2541     if (m_embedMode == PrintPreviewMode) {
2542         return false;
2543     }
2544 
2545     // Determine the document's mimetype
2546     QMimeDatabase db;
2547     QMimeType originalMimeType;
2548     const QString typeName = m_document->documentInfo().get(DocumentInfo::MimeType);
2549     if (!typeName.isEmpty()) {
2550         originalMimeType = db.mimeTypeForName(typeName);
2551     }
2552 
2553     // What data would we lose if we saved natively?
2554     bool wontSaveForms, wontSaveAnnotations;
2555     checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations);
2556 
2557     const QMimeType okularArchiveMimeType = db.mimeTypeForName(QStringLiteral("application/vnd.kde.okular-archive"));
2558 
2559     // Prepare "Save As" dialog
2560     const QString originalMimeTypeFilter = i18nc("File type name and pattern", "%1 (%2)", originalMimeType.comment(), originalMimeType.globPatterns().join(QLatin1Char(' ')));
2561     const QString okularArchiveMimeTypeFilter = i18nc("File type name and pattern", "%1 (%2)", okularArchiveMimeType.comment(), okularArchiveMimeType.globPatterns().join(QLatin1Char(' ')));
2562 
2563     // What format choice should we show as default?
2564     QString selectedFilter = (isDocumentArchive || showOkularArchiveAsDefaultFormat || wontSaveForms || wontSaveAnnotations) ? okularArchiveMimeTypeFilter : originalMimeTypeFilter;
2565 
2566     QString filter = originalMimeTypeFilter + QStringLiteral(";;") + okularArchiveMimeTypeFilter;
2567 
2568     const QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18n("Save As"), url(), filter, &selectedFilter);
2569 
2570     if (!saveUrl.isValid() || saveUrl.isEmpty()) {
2571         return false;
2572     }
2573 
2574     // Has the user chosen to save in .okular archive format?
2575     const bool saveAsOkularArchive = (selectedFilter == okularArchiveMimeTypeFilter);
2576 
2577     if (saveAsOkularArchive) {
2578         // Non Plasma file dialogs are terrible and it's very easy to select saving a file as okular archive and call it hello.md
2579         // and that's bad because it is *not* an .md file so tell the user to fix it
2580         Q_ASSERT(okularArchiveMimeType.suffixes().count() == 1);
2581         Q_ASSERT(okularArchiveMimeType.suffixes().at(0) == okularArchiveMimeType.preferredSuffix());
2582         const QString wantedExtension = QLatin1Char('.') + okularArchiveMimeType.preferredSuffix();
2583         if (!saveUrl.path().endsWith(wantedExtension)) {
2584             const auto button = KMessageBox::questionTwoActions(widget(),
2585                                                                 i18n("You have chosen to save an Okular Archive without the file name ending with the '%1' extension. That is not allowed, do you want to choose a new name?", wantedExtension),
2586                                                                 i18n("Unsupported extension"),
2587                                                                 KGuiItem(i18nc("@action:button", "Choose New Name"), QStringLiteral("edit-rename")),
2588                                                                 KStandardGuiItem::cancel());
2589 
2590             return button == KMessageBox::PrimaryAction ? slotSaveFileAs(showOkularArchiveAsDefaultFormat) : false;
2591         }
2592     }
2593 
2594     return saveAs(saveUrl, saveAsOkularArchive ? SaveAsOkularArchive : NoSaveAsFlags);
2595 }
2596 
2597 bool Part::saveAs(const QUrl &saveUrl)
2598 {
2599     // Save in the same format (.okular vs native) as the current file
2600     return saveAs(saveUrl, isDocumentArchive ? SaveAsOkularArchive : NoSaveAsFlags);
2601 }
2602 
2603 static QUrl resolveSymlinksIfFileExists(const QUrl &saveUrl)
2604 {
2605     if (saveUrl.isLocalFile()) {
2606         const QFileInfo fi(saveUrl.toLocalFile());
2607         return fi.exists() ? QUrl::fromLocalFile(fi.canonicalFilePath()) : saveUrl;
2608     } else {
2609         return saveUrl;
2610     }
2611 }
2612 
2613 bool Part::saveAs(const QUrl &saveUrl, SaveAsFlags flags)
2614 {
2615     // TODO When we get different saving backends we need to query the backend
2616     // as to if it can save changes even if the open file has been modified,
2617     // since we only have poppler as saving backend for now we're skipping that check
2618 
2619     // Don't warn the user about external changes if what actually happened was that
2620     // the file on disk was deleted for some reason; in this case just go on normally
2621     // to avoid confusion or data loss.
2622     // Also don't warn if the file was modified on disk but the user is doing a Save As
2623     // with a different URL, since the original changed document is safe so there's
2624     // nothing to warn about.
2625     const QFileInfo fi(localFilePath());
2626     if (fi.exists() && m_fileLastModified != fi.lastModified() && saveUrl == realUrl()) {
2627         const int res = KMessageBox::warningTwoActionsCancel(widget(),
2628                                                              xi18nc("@info",
2629                                                                     "The file <filename>%1</filename> has been modified by another program. If you save now, any "
2630                                                                     "changes made in the other program will be lost. Are you sure you want to continue?",
2631                                                                     realUrl().fileName()),
2632                                                              i18n("Save - Warning"),
2633                                                              KStandardGuiItem::cont(),                // <- KMessageBox::PrimaryAction
2634                                                              KGuiItem(i18n("Save a Copy Elsewhere")), // <- KMessageBox::SecondaryAction
2635                                                              KStandardGuiItem::cancel());             // <- KMessageBox::Cancel
2636 
2637         if (res == KMessageBox::SecondaryAction) {
2638             slotSaveFileAs(false);
2639         }
2640         if (res != KMessageBox::PrimaryAction) {
2641             return false;
2642         }
2643     }
2644 
2645     bool hasUserAcceptedReload = false;
2646     if (m_documentOpenWithPassword) {
2647         const int res = KMessageBox::warningContinueCancel(
2648             widget(),
2649             i18n("The current document is protected with a password.<br />In order to save, the file needs to be reloaded. You will be asked for the password again and your undo/redo history will be lost.<br />Do you want to continue?"),
2650             i18n("Save - Warning"),
2651             KStandardGuiItem::cont(),
2652             KStandardGuiItem::cancel());
2653 
2654         switch (res) {
2655         case KMessageBox::Continue:
2656             hasUserAcceptedReload = true;
2657             // do nothing
2658             break;
2659         case KMessageBox::Cancel: // User said no to continue, so return true even if save didn't happen otherwise we will get an error
2660             return true;
2661         }
2662     }
2663 
2664     bool setModifiedAfterSave = false;
2665 
2666     QString tmpFileName;
2667     // don't turn the file_copy that use this to a file_move
2668     // doesn't work on Windows
2669     bool deleteTmpFileName = false;
2670     {
2671         // Own scope for the QTemporaryFile since we only care about the random name
2672         // we need to destroy it so the file gets deleted otherwise windows will complain
2673         // when trying to save over it because the file is still open
2674         QTemporaryFile tf;
2675         if (!tf.open()) {
2676             KMessageBox::information(widget(), i18n("Could not open the temporary file for saving."));
2677             return false;
2678         }
2679         tmpFileName = tf.fileName();
2680     }
2681 
2682     // Figure out the real save url, for symlinks we don't want to copy over the symlink but over the target file
2683     const QUrl realSaveUrl = resolveSymlinksIfFileExists(saveUrl);
2684 
2685     // Due to the way we write we can overwrite readonly files so check if it's one and just bail out early
2686     if (realSaveUrl.isLocalFile()) {
2687         const QFileInfo fi(realSaveUrl.toLocalFile());
2688         if (fi.exists() && !fi.isWritable()) {
2689             KMessageBox::information(widget(), xi18nc("@info", "Could not overwrite <filename>%1</filename> because that file is read-only. Try saving to another location or changing that file's permissions.", realSaveUrl.toLocalFile()));
2690             return false;
2691         }
2692     }
2693 
2694     QScopedPointer<QTemporaryFile> tempFile;
2695     KIO::Job *copyJob = nullptr; // this will be filled with the job that writes to saveUrl
2696 
2697     // Does the user want a .okular archive?
2698     if (flags & SaveAsOkularArchive) {
2699         if (!hasUserAcceptedReload && !m_document->canSwapBackingFile()) {
2700             const int res = KMessageBox::warningContinueCancel(widget(),
2701                                                                i18n("After saving, the current document format requires the file to be reloaded. Your undo/redo history will be lost.<br />Do you want to continue?"),
2702                                                                i18n("Save - Warning"),
2703                                                                KStandardGuiItem::cont(),
2704                                                                KStandardGuiItem::cancel());
2705 
2706             switch (res) {
2707             case KMessageBox::Continue:
2708                 // do nothing
2709                 break;
2710             case KMessageBox::Cancel: // User said no to continue, so return true even if save didn't happen otherwise we will get an error
2711                 return true;
2712             }
2713         }
2714 
2715         if (!m_document->saveDocumentArchive(tmpFileName)) {
2716             KMessageBox::information(widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", tmpFileName));
2717             return false;
2718         }
2719 
2720         copyJob = KIO::file_copy(QUrl::fromLocalFile(tmpFileName), realSaveUrl, -1, KIO::Overwrite);
2721         deleteTmpFileName = true;
2722     } else {
2723         bool wontSaveForms, wontSaveAnnotations;
2724         checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations);
2725 
2726         // If something can't be saved in this format, ask for confirmation
2727         QStringList listOfwontSaves;
2728         if (wontSaveForms) {
2729             listOfwontSaves << i18n("Filled form contents");
2730         }
2731         if (wontSaveAnnotations) {
2732             listOfwontSaves << i18n("User annotations");
2733         }
2734         if (!listOfwontSaves.isEmpty()) {
2735             if (saveUrl == url()) {
2736                 // Save
2737                 const QString warningMessage = i18n("You are about to save changes, but the current file format does not support saving the following elements. Please use the <i>Okular document archive</i> format to preserve them.");
2738                 const int result = KMessageBox::warningContinueCancelList(widget(),
2739                                                                           warningMessage,
2740                                                                           listOfwontSaves,
2741                                                                           i18n("Warning"),
2742                                                                           KGuiItem(i18n("Save as Okular document archive..."), QStringLiteral("document-save-as")), // <- KMessageBox::Continue
2743                                                                           KStandardGuiItem::cancel());
2744 
2745                 switch (result) {
2746                 case KMessageBox::Continue: // -> Save as Okular document archive
2747                     return slotSaveFileAs(true /* showOkularArchiveAsDefaultFormat */);
2748                 default:
2749                     return false;
2750                 }
2751             } else {
2752                 // Save as
2753                 const QString warningMessage = m_document->canSwapBackingFile() ? i18n(
2754                                                                                       "You are about to save changes, but the current file format does not support saving the following elements. Please use the <i>Okular document "
2755                                                                                       "archive</i> format to preserve them. Click <i>Continue</i> to save the document and discard these elements.")
2756                                                                                 : i18n(
2757                                                                                       "You are about to save changes, but the current file format does not support saving the following elements. Please use the <i>Okular document "
2758                                                                                       "archive</i> format to preserve them. Click <i>Continue</i> to save, but you will lose these elements as well as the undo/redo history.");
2759                 const QString continueMessage = m_document->canSwapBackingFile() ? i18n("Continue") : i18n("Continue losing changes");
2760                 const int result = KMessageBox::warningTwoActionsCancelList(widget(),
2761                                                                             warningMessage,
2762                                                                             listOfwontSaves,
2763                                                                             i18n("Warning"),
2764                                                                             KGuiItem(i18n("Save as Okular document archive..."), QStringLiteral("document-save-as")), // <- KMessageBox::PrimaryAction
2765                                                                             KGuiItem(continueMessage, QStringLiteral("arrow-right")));                                // <- KMessageBox::SecondaryAction
2766 
2767                 switch (result) {
2768                 case KMessageBox::PrimaryAction: // -> Save as Okular document archive
2769                     return slotSaveFileAs(true /* showOkularArchiveAsDefaultFormat */);
2770                 case KMessageBox::SecondaryAction: // -> Continue
2771                     setModifiedAfterSave = m_document->canSwapBackingFile();
2772                     break;
2773                 case KMessageBox::Cancel:
2774                     return false;
2775                 }
2776             }
2777         }
2778 
2779         if (m_document->canSaveChanges()) {
2780             // If the generator supports saving changes, save them
2781 
2782             QString errorText;
2783             if (!m_document->saveChanges(tmpFileName, &errorText)) {
2784                 if (errorText.isEmpty()) {
2785                     KMessageBox::information(widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", tmpFileName));
2786                 } else {
2787                     KMessageBox::information(widget(), i18n("File could not be saved in '%1'. %2", tmpFileName, errorText));
2788                 }
2789 
2790                 return false;
2791             }
2792 
2793             copyJob = KIO::file_copy(QUrl::fromLocalFile(tmpFileName), realSaveUrl, -1, KIO::Overwrite);
2794             deleteTmpFileName = true;
2795         } else {
2796             // If the generators doesn't support saving changes, we will
2797             // just copy the original file.
2798 
2799             if (isDocumentArchive) {
2800                 // Special case: if the user is extracting the contents of a
2801                 // .okular archive back to the native format, we can't just copy
2802                 // the open file (which is a .okular). So let's ask to core to
2803                 // extract and give us the real file
2804 
2805                 if (!m_document->extractArchivedFile(tmpFileName)) {
2806                     KMessageBox::information(widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", tmpFileName));
2807                     return false;
2808                 }
2809 
2810                 copyJob = KIO::file_copy(QUrl::fromLocalFile(tmpFileName), realSaveUrl, -1, KIO::Overwrite);
2811                 deleteTmpFileName = true;
2812             } else {
2813                 // Otherwise just copy the open file.
2814                 // make use of the already downloaded (in case of remote URLs) file,
2815                 // no point in downloading that again
2816                 QUrl srcUrl = QUrl::fromLocalFile(localFilePath());
2817                 // duh, our local file disappeared...
2818                 if (!QFile::exists(localFilePath())) {
2819                     if (url().isLocalFile()) {
2820 #ifdef OKULAR_KEEP_FILE_OPEN
2821                         // local file: try to get it back from the open handle on it
2822                         tempFile.reset(m_keeper->copyToTemporary());
2823                         if (tempFile)
2824                             srcUrl = KUrl::fromPath(tempFile->fileName());
2825 #else
2826                         const QString msg = i18n("Okular cannot copy %1 to the specified location.\n\nThe document does not exist anymore.", localFilePath());
2827                         KMessageBox::error(widget(), msg);
2828                         return false;
2829 #endif
2830                     } else {
2831                         // we still have the original remote URL of the document,
2832                         // so copy the document from there
2833                         srcUrl = url();
2834                     }
2835                 }
2836 
2837                 if (srcUrl != saveUrl) {
2838                     copyJob = KIO::file_copy(srcUrl, realSaveUrl, -1, KIO::Overwrite);
2839                 } else {
2840                     // Don't do a real copy in this case, just update the timestamps
2841                     copyJob = KIO::setModificationTime(realSaveUrl, QDateTime::currentDateTime());
2842                 }
2843             }
2844         }
2845     }
2846 
2847     // Stop watching for changes while we write the new file (useful when
2848     // overwriting)
2849     if (url().isLocalFile()) {
2850         unsetFileToWatch();
2851     }
2852 
2853     const auto deleteTmpFileFunction = [deleteTmpFileName, tmpFileName] {
2854         Q_ASSERT(deleteTmpFileName == QFile::exists(tmpFileName));
2855         if (deleteTmpFileName) {
2856             QFile::remove(tmpFileName);
2857         }
2858     };
2859 
2860     KJobWidgets::setWindow(copyJob, widget());
2861     if (!copyJob->exec()) {
2862         KMessageBox::information(widget(), i18n("File could not be saved in '%1'. Error: '%2'. Try to save it to another location.", saveUrl.toDisplayString(), copyJob->errorString()));
2863 
2864         // Restore watcher
2865         if (url().isLocalFile()) {
2866             setFileToWatch(localFilePath());
2867         }
2868 
2869         deleteTmpFileFunction();
2870 
2871         return false;
2872     }
2873     deleteTmpFileFunction();
2874 
2875     m_document->setHistoryClean(true);
2876 
2877     if (m_document->isDocdataMigrationNeeded()) {
2878         m_document->docdataMigrationDone();
2879     }
2880 
2881     bool reloadedCorrectly = true;
2882 
2883     // Make the generator use the new file instead of the old one
2884     if (m_document->canSwapBackingFile() && !m_documentOpenWithPassword) {
2885         QWidget *currentSidebarItem = m_sidebar->currentItem();
2886         // this calls openFile internally, which in turn actually calls
2887         // m_document->swapBackingFile() instead of the regular loadDocument
2888         if (openUrl(saveUrl, true /* swapInsteadOfOpening */)) {
2889             if (setModifiedAfterSave) {
2890                 m_document->setHistoryClean(false);
2891             }
2892         } else {
2893             reloadedCorrectly = false;
2894         }
2895 
2896         if (m_sidebar->currentItem() != currentSidebarItem) {
2897             m_sidebar->setCurrentItem(currentSidebarItem);
2898         }
2899     } else {
2900         // If the generator doesn't support swapping file, then just reload
2901         // the document from the new location
2902         if (!slotAttemptReload(true, saveUrl)) {
2903             reloadedCorrectly = false;
2904         }
2905     }
2906 
2907     // In case of file swapping errors, close the document to avoid inconsistencies
2908     if (!reloadedCorrectly) {
2909         qWarning() << "The document hasn't been reloaded/swapped correctly";
2910         closeUrl();
2911     }
2912 
2913     // Restore watcher
2914     if (url().isLocalFile()) {
2915         setFileToWatch(localFilePath());
2916     }
2917 
2918     // Set correct permission taking into account the umask value
2919 #ifndef Q_OS_WIN
2920     const QString saveFilePath = saveUrl.toLocalFile();
2921     if (QFile::exists(saveFilePath)) {
2922         const mode_t mask = umask(0);
2923         umask(mask);
2924         const mode_t fileMode = 0666 & ~mask;
2925         chmod(QFile::encodeName(saveFilePath).constData(), fileMode);
2926     }
2927 #endif
2928 
2929     return true;
2930 }
2931 
2932 // If the user wants to save in the original file's format, some features might
2933 // not be available. Find out what cannot be saved in this format
2934 void Part::checkNativeSaveDataLoss(bool *out_wontSaveForms, bool *out_wontSaveAnnotations) const
2935 {
2936     bool wontSaveForms = false;
2937     bool wontSaveAnnotations = false;
2938 
2939     if (!m_document->canSaveChanges(Document::SaveFormsCapability)) {
2940         /* Set wontSaveForms only if there are forms */
2941         const int pagecount = m_document->pages();
2942 
2943         for (int pageno = 0; pageno < pagecount; ++pageno) {
2944             const Okular::Page *page = m_document->page(pageno);
2945             if (!page->formFields().empty()) {
2946                 wontSaveForms = true;
2947                 break;
2948             }
2949         }
2950     }
2951 
2952     if (!m_document->canSaveChanges(Document::SaveAnnotationsCapability)) {
2953         /* Set wontSaveAnnotations only if there are local annotations */
2954         const int pagecount = m_document->pages();
2955 
2956         for (int pageno = 0; pageno < pagecount; ++pageno) {
2957             const QList<Okular::Annotation *> annotations = m_document->page(pageno)->annotations();
2958             for (const Okular::Annotation *ann : annotations) {
2959                 if (!(ann->flags() & Okular::Annotation::External)) {
2960                     wontSaveAnnotations = true;
2961                     break;
2962                 }
2963             }
2964             if (wontSaveAnnotations) {
2965                 break;
2966             }
2967         }
2968     }
2969 
2970     *out_wontSaveForms = wontSaveForms;
2971     *out_wontSaveAnnotations = wontSaveAnnotations;
2972 }
2973 
2974 void Part::slotPreferences()
2975 {
2976     // Create dialog
2977     PreferencesDialog *dialog = new PreferencesDialog(m_pageView, Okular::Settings::self(), m_embedMode, m_document->editorCommandOverride());
2978     dialog->setAttribute(Qt::WA_DeleteOnClose);
2979 
2980     // Show it
2981     dialog->show();
2982 }
2983 
2984 void Part::slotToggleChangeColors()
2985 {
2986     slotSetChangeColors(!Okular::SettingsCore::changeColors());
2987 }
2988 
2989 void Part::slotSetChangeColors(bool active)
2990 {
2991     Okular::SettingsCore::setChangeColors(active);
2992     Okular::Settings::self()->save();
2993 }
2994 
2995 void Part::slotAccessibilityPreferences()
2996 {
2997     // Create dialog
2998     PreferencesDialog *dialog = new PreferencesDialog(m_pageView, Okular::Settings::self(), m_embedMode, m_document->editorCommandOverride());
2999     dialog->setAttribute(Qt::WA_DeleteOnClose);
3000 
3001     // Show it
3002     dialog->switchToAccessibilityPage();
3003     dialog->show();
3004 }
3005 
3006 void Part::slotAnnotationPreferences()
3007 {
3008     // Create dialog
3009     PreferencesDialog *dialog = new PreferencesDialog(m_pageView, Okular::Settings::self(), m_embedMode, m_document->editorCommandOverride());
3010     dialog->setAttribute(Qt::WA_DeleteOnClose);
3011 
3012     // Show it
3013     dialog->switchToAnnotationsPage();
3014     dialog->show();
3015 }
3016 
3017 void Part::slotNewConfig()
3018 {
3019     // Apply settings here. A good policy is to check whether the setting has
3020     // changed before applying changes.
3021 
3022     // Watch File
3023     setWatchFileModeEnabled(Okular::Settings::watchFile());
3024 
3025     // Main View (pageView)
3026     m_pageView->reparseConfig();
3027 
3028     // update document settings
3029     m_document->reparseConfig();
3030 
3031     // update TOC settings
3032     if (m_tocEnabled) {
3033         m_toc->reparseConfig();
3034     }
3035 
3036     // update ThumbnailList contents
3037     if (Okular::Settings::showLeftPanel() && !m_thumbnailList->isHidden()) {
3038         m_thumbnailList->updateWidgets();
3039     }
3040 
3041     // update Reviews settings
3042     m_reviewsWidget->reparseConfig();
3043 
3044     setWindowTitleFromDocument();
3045 
3046     if (m_presentationDrawingActions) {
3047         m_presentationDrawingActions->reparseConfig();
3048         if (factory()) {
3049             factory()->refreshActionProperties();
3050         }
3051     }
3052 }
3053 
3054 void Part::slotPrintPreview()
3055 {
3056     if (m_document->pages() == 0) {
3057         return;
3058     }
3059 
3060     QPrinter printer;
3061     QString tempFilePattern;
3062 
3063     if (m_document->printingSupport() == Okular::Document::PostscriptPrinting) {
3064         tempFilePattern = (QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps"));
3065     } else if (m_document->printingSupport() == Okular::Document::NativePrinting) {
3066         tempFilePattern = (QDir::tempPath() + QLatin1String("/okular_XXXXXX.pdf"));
3067     } else {
3068         return;
3069     }
3070 
3071     // Generate a temp filename for Print to File, then release the file so generator can write to it
3072     QTemporaryFile tf(tempFilePattern);
3073     tf.setAutoRemove(true);
3074     tf.open();
3075     printer.setOutputFileName(tf.fileName());
3076     tf.close();
3077     setupPrint(printer);
3078     doPrint(printer);
3079     if (QFile::exists(printer.outputFileName())) {
3080         Okular::FilePrinterPreview previewdlg(printer.outputFileName(), widget());
3081         previewdlg.exec();
3082     }
3083 }
3084 
3085 void Part::slotShowTOCMenu(const Okular::DocumentViewport &vp, const QPoint point, const QString &title)
3086 {
3087     showMenu(m_document->page(vp.pageNumber), point, title, vp, true);
3088 }
3089 
3090 void Part::slotShowMenu(const Okular::Page *page, const QPoint point)
3091 {
3092     showMenu(page, point);
3093 }
3094 
3095 void Part::showMenu(const Okular::Page *page, const QPoint point, const QString &bookmarkTitle, const Okular::DocumentViewport &vp, bool showTOCActions)
3096 {
3097     if (m_embedMode == PrintPreviewMode) {
3098         return;
3099     }
3100 
3101     bool reallyShow = false;
3102     const bool isCurrentPage = page && page->number() == m_document->viewport().pageNumber;
3103 
3104     if (!m_showMenuBarAction) {
3105         m_showMenuBarAction = findActionInKPartHierarchy<KToggleAction>(KStandardAction::name(KStandardAction::ShowMenubar));
3106     }
3107     if (!m_showFullScreenAction) {
3108         m_showFullScreenAction = findActionInKPartHierarchy<KToggleFullScreenAction>(KStandardAction::name(KStandardAction::FullScreen));
3109     }
3110 
3111     QMenu popup;
3112     if (showTOCActions) {
3113         popup.addAction(i18n("Expand whole section"), m_toc.data(), &TOC::expandRecursively);
3114         popup.addAction(i18n("Collapse whole section"), m_toc.data(), &TOC::collapseRecursively);
3115         popup.addAction(i18n("Expand all"), m_toc.data(), &TOC::expandAll);
3116         popup.addAction(i18n("Collapse all"), m_toc.data(), &TOC::collapseAll);
3117         reallyShow = true;
3118     }
3119 
3120     QAction *addBookmark = nullptr;
3121     QAction *removeBookmark = nullptr;
3122     QAction *fitPageWidth = nullptr;
3123     if (page) {
3124         popup.addAction(new OKMenuTitle(&popup, i18n("Page %1", page->number() + 1)));
3125         if (m_thumbnailList->isVisible() && !Okular::Settings::syncThumbnailsViewport()) {
3126             const QIcon &syncIcon = QIcon::fromTheme(QStringLiteral("emblem-synchronizing"), QIcon::fromTheme(QStringLiteral("view-refresh")));
3127             popup.addAction(syncIcon, i18n("Sync Thumbnail with Page"), m_thumbnailList.data(), &ThumbnailList::syncThumbnail);
3128         }
3129         if ((!isCurrentPage && m_document->bookmarkManager()->isBookmarked(page->number())) || (isCurrentPage && m_document->bookmarkManager()->isBookmarked(m_document->viewport()))) {
3130             removeBookmark = popup.addAction(QIcon::fromTheme(QStringLiteral("bookmark-remove"), QIcon::fromTheme(QStringLiteral("edit-delete-bookmark"))), i18n("Remove Bookmark"));
3131         } else {
3132             addBookmark = popup.addAction(QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add Bookmark"));
3133         }
3134         if (m_pageView->canFitPageWidth()) {
3135             fitPageWidth = popup.addAction(QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("Fit Width"));
3136         }
3137         popup.addAction(m_prevBookmark);
3138         popup.addAction(m_nextBookmark);
3139         reallyShow = true;
3140     }
3141 
3142     const int amountOfActions = popup.actions().count();
3143     if (m_showMenuBarAction && !m_showMenuBarAction->isChecked()) {
3144         if (m_hamburgerMenuAction) {
3145             m_hamburgerMenuAction->addToMenu(&popup);
3146         } else {
3147             popup.addAction(m_showMenuBarAction);
3148         }
3149     }
3150     if (m_showFullScreenAction && m_showFullScreenAction->isChecked()) {
3151         popup.addAction(m_showFullScreenAction);
3152     }
3153     if (popup.actions().count() > amountOfActions && popup.actions().constLast()->isVisible()) {
3154         popup.insertAction(popup.actions().at(amountOfActions), new OKMenuTitle(&popup, i18n("Tools")));
3155         reallyShow = true;
3156     }
3157 
3158     if (reallyShow) {
3159         QAction *res = popup.exec(point);
3160         if (res) {
3161             if (res == addBookmark) {
3162                 if (isCurrentPage && bookmarkTitle.isEmpty()) {
3163                     m_document->bookmarkManager()->addBookmark(m_document->viewport());
3164                 } else if (!bookmarkTitle.isEmpty()) {
3165                     m_document->bookmarkManager()->addBookmark(m_document->currentDocument(), vp, bookmarkTitle);
3166                 } else {
3167                     m_document->bookmarkManager()->addBookmark(page->number());
3168                 }
3169             } else if (res == removeBookmark) {
3170                 if (isCurrentPage) {
3171                     m_document->bookmarkManager()->removeBookmark(m_document->viewport());
3172                 } else {
3173                     m_document->bookmarkManager()->removeBookmark(page->number());
3174                 }
3175             } else if (res == fitPageWidth) {
3176                 m_pageView->fitPageWidth(page->number());
3177             }
3178         }
3179     }
3180 }
3181 
3182 template<class Action> Action *Part::findActionInKPartHierarchy(const QString &actionName)
3183 {
3184     static_assert(std::is_base_of<QAction, Action>::value, "Calling this method to find something other than an Action makes no sense.");
3185     if (factory()) {
3186         const QList<KXMLGUIClient *> clients(factory()->clients());
3187         for (auto client : clients) {
3188             if (QAction *act = client->actionCollection()->action(actionName)) {
3189                 if (Action *castedAction = qobject_cast<Action *>(act)) {
3190                     return castedAction;
3191                 }
3192             }
3193         }
3194     }
3195     return nullptr;
3196 }
3197 
3198 KMainWindow *Part::findMainWindow()
3199 {
3200     auto *potentialMainWindow = parent();
3201     while (potentialMainWindow) {
3202         if (auto *mainWindow = qobject_cast<KMainWindow *>(potentialMainWindow)) {
3203             return mainWindow;
3204         }
3205         potentialMainWindow = potentialMainWindow->parent();
3206     }
3207     return nullptr;
3208 }
3209 
3210 void Part::slotShowProperties()
3211 {
3212     PropertiesDialog *d = new PropertiesDialog(widget(), m_document);
3213     connect(d, &QDialog::finished, d, &QObject::deleteLater);
3214     d->open();
3215 }
3216 
3217 void Part::slotShowEmbeddedFiles()
3218 {
3219     EmbeddedFilesDialog *d = new EmbeddedFilesDialog(widget(), m_document);
3220     connect(d, &QDialog::finished, d, &QObject::deleteLater);
3221     d->open();
3222 }
3223 
3224 void Part::slotShowPresentation()
3225 {
3226     if (!m_presentationWidget) {
3227         m_presentationWidget = new PresentationWidget(widget(), m_document, m_presentationDrawingActions, actionCollection());
3228     }
3229 }
3230 
3231 void Part::slotHidePresentation()
3232 {
3233     if (m_presentationWidget) {
3234         delete m_presentationWidget.data();
3235     }
3236 }
3237 
3238 void Part::slotUpdateHamburgerMenu()
3239 {
3240     auto ac = actionCollection();
3241 
3242     auto menu = m_hamburgerMenuAction->menu();
3243     if (!menu) {
3244         menu = new QMenu(widget());
3245         m_hamburgerMenuAction->setMenu(menu);
3246         if (!m_showMenuBarAction) {
3247             m_showMenuBarAction = findActionInKPartHierarchy<KToggleAction>(KStandardAction::name(KStandardAction::ShowMenubar));
3248         }
3249         m_hamburgerMenuAction->setShowMenuBarAction(m_showMenuBarAction);
3250     } else {
3251         menu->clear();
3252     }
3253 
3254     QToolBar *visibleMainToolbar = nullptr;
3255     if (auto *mainWindow = findMainWindow()) {
3256         visibleMainToolbar = mainWindow->toolBar();
3257         if (!visibleMainToolbar->isVisible()) {
3258             visibleMainToolbar = nullptr;
3259         }
3260         const auto toolbars = mainWindow->toolBars();
3261         for (const auto &toolbar : toolbars) {
3262             m_hamburgerMenuAction->hideActionsOf(toolbar);
3263         }
3264 
3265         bool menuAvailable = false; // We already know the menu bar is hidden when this menu is opened.
3266         // If no menu is available, we want to add actions to the hamburger menu to show them again.
3267         // The hamburger menu serves as the fallback that is available through the right-click context menu.
3268         if (visibleMainToolbar && visibleMainToolbar->actions().contains(m_hamburgerMenuAction)) {
3269             menuAvailable = true;
3270         }
3271         if (!menuAvailable) {
3272             menu->addAction(m_showMenuBarAction);
3273             if (!visibleMainToolbar) {
3274                 menu->addAction(findActionInKPartHierarchy(QStringLiteral("mainToolBar")));
3275             }
3276             menu->addSeparator();
3277         }
3278     }
3279 
3280     // When changing actions, keep "Simple by default, powerful when needed" in mind.
3281     // To retrieve an action, it is fastest to use a direct pointer if available (m_action), otherwise use
3282     // ac->action(actionName) and if the action isn't in the actionCollection() of this part,
3283     // use findActionInKPartHierarchy(actionName).
3284     menu->addAction(findActionInKPartHierarchy(KStandardAction::name(KStandardAction::Open)));
3285     menu->addAction(findActionInKPartHierarchy(KStandardAction::name(KStandardAction::OpenRecent)));
3286     menu->addAction(m_save);
3287     menu->addAction(m_saveAs);
3288     menu->addSeparator();
3289     menu->addAction(ac->action(QStringLiteral("mouse_drag")));
3290     if (!visibleMainToolbar || !visibleMainToolbar->actions().contains(ac->action(QStringLiteral("mouse_selecttools")))) {
3291         menu->addAction(ac->action(QStringLiteral("mouse_select")));
3292     }
3293     menu->addAction(m_copy);
3294     menu->addAction(m_find);
3295     menu->addAction(m_showLeftPanel);
3296     if (!visibleMainToolbar || visibleMainToolbar->actions().contains(ac->action(QStringLiteral("annotation_favorites")))) {
3297         menu->addAction(ac->action(QStringLiteral("mouse_toggle_annotate")));
3298     }
3299     menu->addAction(ac->action(KStandardAction::name(KStandardAction::Undo)));
3300     menu->addAction(ac->action(KStandardAction::name(KStandardAction::Redo)));
3301     menu->addSeparator();
3302 
3303     menu->addAction(findActionInKPartHierarchy(KStandardAction::name(KStandardAction::Print)));
3304     menu->addAction(m_printPreview);
3305     menu->addSeparator();
3306     menu->addAction(ac->action(QStringLiteral("add_digital_signature")));
3307     menu->addAction(m_showProperties);
3308     menu->addAction(m_openContainingFolder);
3309 #if HAVE_PURPOSE
3310     menu->addAction(m_share);
3311 #endif
3312     menu->addSeparator();
3313 
3314     menu->addAction(ac->action(QStringLiteral("zoom_to")));
3315     const QMenuBar *menuBar = m_hamburgerMenuAction->menuBar();
3316     if (menuBar && menuBar->actions().count() < 3) { // 3 to make sure none of the code below can crash.
3317         menuBar = nullptr;
3318     }
3319     auto curatedViewMenu = menu->addMenu(QIcon::fromTheme(QStringLiteral("page-2sides")), menuBar ? menuBar->actions().at(1)->text() : QStringLiteral("View"));
3320     if (!m_showFullScreenAction) {
3321         m_showFullScreenAction = findActionInKPartHierarchy<KToggleFullScreenAction>(KStandardAction::name(KStandardAction::FullScreen));
3322     }
3323     curatedViewMenu->addAction(m_showFullScreenAction);
3324     curatedViewMenu->addAction(m_showPresentation);
3325     curatedViewMenu->addSeparator();
3326     curatedViewMenu->addAction(findActionInKPartHierarchy(QStringLiteral("view_render_mode")));
3327     if (auto *viewOrientationMenu = qobject_cast<QMenu *>(factory()->container(QStringLiteral("view_orientation"), this))) {
3328         curatedViewMenu->addAction(viewOrientationMenu->menuAction());
3329     }
3330     curatedViewMenu->addAction(findActionInKPartHierarchy(QStringLiteral("view_trim_mode")));
3331     curatedViewMenu->addSeparator();
3332     curatedViewMenu->addAction(ac->action(QStringLiteral("view_toggle_forms")));
3333     m_hamburgerMenuAction->hideActionsOf(curatedViewMenu);
3334 
3335 #if HAVE_SPEECH
3336     auto speakMenu = menu->addMenu(QIcon::fromTheme(QStringLiteral("text-speak")), i18nc("@action:inmenu menu that contains actions to control text to speach", "Speak"));
3337     speakMenu->addAction(ac->action(QStringLiteral("speak_document")));
3338     speakMenu->addAction(ac->action(QStringLiteral("speak_current_page")));
3339     speakMenu->addAction(ac->action(QStringLiteral("speak_stop_all")));
3340     speakMenu->addAction(ac->action(QStringLiteral("speak_pause_resume")));
3341     m_hamburgerMenuAction->hideActionsOf(speakMenu);
3342 #endif
3343 
3344     // Add the "Settings" menu from the menu bar.
3345     if (menuBar) {
3346         menu->addAction(menuBar->actions().at(menuBar->actions().count() - 3));
3347     }
3348 }
3349 
3350 void Part::slotTogglePresentation()
3351 {
3352     if (m_document->isOpened()) {
3353         if (!m_presentationWidget) {
3354             m_presentationWidget = new PresentationWidget(widget(), m_document, m_presentationDrawingActions, actionCollection());
3355         } else {
3356             delete m_presentationWidget.data();
3357         }
3358     }
3359 }
3360 
3361 void Part::reload()
3362 {
3363     if (m_document->isOpened()) {
3364         slotReload();
3365     }
3366 }
3367 
3368 void Part::enableStartWithPrint()
3369 {
3370     m_cliPrint = true;
3371 }
3372 
3373 void Part::enableExitAfterPrint()
3374 {
3375     m_cliPrintAndExit = true;
3376 }
3377 
3378 #define kKPlugin QStringLiteral("KPlugin")
3379 
3380 void Part::slotAboutBackend()
3381 {
3382     const KPluginMetaData data = m_document->generatorInfo();
3383     if (!data.isValid()) {
3384         return;
3385     }
3386 
3387     // Here we do a bit of magic because KPluginMetaData doesn't have setters
3388     // so we get the json info from it, modify it and use that for the KAboutPluginDialog
3389     // in case the internals of KPluginMetaData change it won't be too bad, at most we're
3390     // missing the icon or the generator extra description
3391     QJsonObject rawData = data.rawData();
3392     const QIcon icon = QIcon::fromTheme(data.iconName());
3393 
3394     // fall back to mime type icon
3395     if (icon.isNull()) {
3396         const Okular::DocumentInfo documentInfo = m_document->documentInfo(QSet<DocumentInfo::Key>() << DocumentInfo::MimeType);
3397         const QString mimeTypeName = documentInfo.get(DocumentInfo::MimeType);
3398         if (!mimeTypeName.isEmpty()) {
3399             QMimeDatabase db;
3400             QMimeType type = db.mimeTypeForName(mimeTypeName);
3401             if (type.isValid()) {
3402                 QJsonObject kplugin = rawData[kKPlugin].toObject();
3403                 kplugin[QStringLiteral("Icon")] = type.iconName();
3404                 rawData[kKPlugin] = kplugin;
3405             }
3406         }
3407     }
3408 
3409     const QString extraDescription = m_document->metaData(QStringLiteral("GeneratorExtraDescription")).toString();
3410 
3411     if (!extraDescription.isEmpty()) {
3412         const QString descriptionAndLang = QStringLiteral("Description[%1]").arg(QLocale().name());
3413         QJsonObject kplugin = rawData[kKPlugin].toObject();
3414         kplugin[descriptionAndLang] = QStringLiteral("%1\n\n%2").arg(data.description(), extraDescription);
3415         rawData[kKPlugin] = kplugin;
3416     }
3417 
3418     KAboutPluginDialog dlg(KPluginMetaData(rawData, data.fileName()), widget());
3419     dlg.exec();
3420 }
3421 
3422 void Part::slotExportAs(QAction *act)
3423 {
3424     QList<QAction *> acts = m_exportAs->menu() ? m_exportAs->menu()->actions() : QList<QAction *>();
3425     int id = acts.indexOf(act);
3426     if ((id < 0) || (id >= acts.count())) {
3427         return;
3428     }
3429 
3430     QMimeDatabase mimeDatabase;
3431     QMimeType mimeType;
3432     switch (id) {
3433     case 0:
3434         mimeType = mimeDatabase.mimeTypeForName(QStringLiteral("text/plain"));
3435         break;
3436     default:
3437         mimeType = m_exportFormats.at(id - 1).mimeType();
3438         break;
3439     }
3440     QString filter = i18nc("File type name and pattern", "%1 (%2)", mimeType.comment(), mimeType.globPatterns().join(QLatin1Char(' ')));
3441 
3442     QString fileName = QFileDialog::getSaveFileName(widget(), QString(), QString(), filter);
3443 
3444     if (!fileName.isEmpty()) {
3445         bool saved = false;
3446         switch (id) {
3447         case 0:
3448             saved = m_document->exportToText(fileName);
3449             break;
3450         default:
3451             saved = m_document->exportTo(fileName, m_exportFormats.at(id - 1));
3452             break;
3453         }
3454         if (!saved) {
3455             KMessageBox::information(widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName));
3456         }
3457     }
3458 }
3459 
3460 void Part::slotReload()
3461 {
3462     // stop the dirty handler timer, otherwise we may conflict with the
3463     // auto-refresh system
3464     m_dirtyHandler->stop();
3465 
3466     slotAttemptReload();
3467 }
3468 
3469 void Part::slotPrint()
3470 {
3471     if (m_document->pages() == 0) {
3472         return;
3473     }
3474 
3475 #ifdef Q_OS_WIN
3476     QPrinter printer(QPrinter::HighResolution);
3477 #else
3478     QPrinter printer;
3479 #endif
3480     QWidget *printConfigWidget = nullptr;
3481 
3482     // Must do certain QPrinter setup before creating QPrintDialog
3483     setupPrint(printer);
3484 
3485     // Create the Print Dialog with extra config widgets if required
3486     if (m_document->canConfigurePrinter()) {
3487         printConfigWidget = m_document->printConfigurationWidget();
3488     } else {
3489         printConfigWidget = new DefaultPrintOptionsWidget();
3490     }
3491 
3492     QPrintDialog printDialog(&printer, widget());
3493     printDialog.setWindowTitle(i18nc("@title:window", "Print"));
3494     QList<QWidget *> options;
3495     if (printConfigWidget) {
3496         options << printConfigWidget;
3497     }
3498     printDialog.setOptionTabs(options);
3499 
3500     // Set the available Print Range
3501     printDialog.setMinMax(1, m_document->pages());
3502     printDialog.setFromTo(1, m_document->pages());
3503 
3504     // If the user has bookmarked pages for printing, then enable Selection
3505     if (!m_document->bookmarkedPageRange().isEmpty()) {
3506         printDialog.setOption(QAbstractPrintDialog::PrintSelection);
3507     }
3508 
3509     // If the Document type doesn't support print to both PS & PDF then disable the Print Dialog option
3510     if (printDialog.testOption(QAbstractPrintDialog::PrintToFile) && !m_document->supportsPrintToFile()) {
3511         printDialog.setOption(QAbstractPrintDialog::PrintToFile, false);
3512     }
3513 
3514     // Enable the Current Page option in the dialog.
3515     if (m_document->pages() > 1 && currentPage() > 0) {
3516         printDialog.setOption(QAbstractPrintDialog::PrintCurrentPage);
3517     }
3518 
3519     bool success = true;
3520     if (printDialog.exec()) {
3521         // set option for margins if widget is of corresponding type that holds this information
3522         PrintOptionsWidget *optionWidget = dynamic_cast<PrintOptionsWidget *>(printConfigWidget);
3523         if (optionWidget != nullptr) {
3524             printer.setFullPage(optionWidget->ignorePrintMargins());
3525         } else {
3526             // printConfigurationWidget() method should always return an object of type Okular::PrintOptionsWidget,
3527             // (signature does not (yet) require it for ABI stability reasons), so Q_EMIT a warning if the object is of another type
3528             qWarning() << "printConfigurationWidget() method did not return an Okular::PrintOptionsWidget. This is strongly discouraged!";
3529         }
3530 
3531         success = doPrint(printer);
3532     }
3533 
3534     if (m_cliPrintAndExit) {
3535         exit(success ? EXIT_SUCCESS : EXIT_FAILURE);
3536     }
3537 }
3538 
3539 void Part::setupPrint(QPrinter &printer)
3540 {
3541     printer.setPageOrientation(m_document->orientation());
3542 
3543     // title
3544     QString title = m_document->metaData(QStringLiteral("DocumentTitle")).toString();
3545     if (title.isEmpty()) {
3546         title = m_document->currentDocument().fileName();
3547     }
3548     if (!title.isEmpty()) {
3549         printer.setDocName(title);
3550     }
3551 }
3552 
3553 bool Part::doPrint(QPrinter &printer)
3554 {
3555     if (!m_document->isAllowed(Okular::AllowPrint)) {
3556         KMessageBox::error(widget(), i18n("Printing this document is not allowed."));
3557         return false;
3558     }
3559 
3560     const Document::PrintError printError = m_document->print(printer);
3561     if (printError != Document::NoPrintError) {
3562         const QString error = Okular::Document::printErrorString(printError);
3563         if (error.isEmpty()) {
3564             KMessageBox::error(widget(), i18n("Could not print the document. Unknown error. Please report to bugs.kde.org"));
3565         } else {
3566             KMessageBox::error(widget(), i18n("Could not print the document. Detailed error is \"%1\". Please report to bugs.kde.org", error));
3567         }
3568         return false;
3569     }
3570     return true;
3571 }
3572 
3573 void Part::psTransformEnded(int exit, QProcess::ExitStatus status)
3574 {
3575     Q_UNUSED(exit)
3576     if (status != QProcess::NormalExit) {
3577         return;
3578     }
3579 
3580     QProcess *senderobj = sender() ? qobject_cast<QProcess *>(sender()) : nullptr;
3581     if (senderobj) {
3582         senderobj->close();
3583         senderobj->deleteLater();
3584     }
3585 
3586     setLocalFilePath(m_temporaryLocalFile);
3587     openUrl(QUrl::fromLocalFile(m_temporaryLocalFile));
3588     m_temporaryLocalFile.clear();
3589 }
3590 
3591 void Part::displayInfoMessage(const QString &message, KMessageWidget::MessageType messageType, int duration)
3592 {
3593     if (!Okular::Settings::showOSD()) {
3594         if (messageType == KMessageWidget::Error) {
3595             KMessageBox::error(widget(), message);
3596         }
3597         return;
3598     }
3599 
3600     // hide messageWindow if string is empty
3601     if (message.isEmpty()) {
3602         m_infoMessage->animatedHide();
3603     }
3604 
3605     // display message (duration is length dependent)
3606     if (duration < 0) {
3607         duration = 500 + 100 * message.length();
3608     }
3609     m_infoTimer->start(duration);
3610     m_infoMessage->setText(message);
3611     m_infoMessage->setMessageType(messageType);
3612     m_infoMessage->setVisible(true);
3613 }
3614 
3615 void Part::errorMessage(const QString &message, int duration)
3616 {
3617     displayInfoMessage(message, KMessageWidget::Error, duration);
3618 }
3619 
3620 void Part::warningMessage(const QString &message, int duration)
3621 {
3622     displayInfoMessage(message, KMessageWidget::Warning, duration);
3623 }
3624 
3625 void Part::noticeMessage(const QString &message, int duration)
3626 {
3627     // less important message -> simpler display widget in the PageView
3628     m_pageView->displayMessage(message, QString(), PageViewMessage::Info, duration);
3629 }
3630 
3631 void Part::moveSplitter(int sideWidgetSize)
3632 {
3633     m_sidebar->moveSplitter(sideWidgetSize);
3634 }
3635 
3636 void Part::unsetDummyMode()
3637 {
3638     if (m_embedMode == PrintPreviewMode) {
3639         return;
3640     }
3641 
3642     m_sidebar->setSidebarVisibility(Okular::Settings::showLeftPanel());
3643 
3644     // add back and next in history
3645     m_historyBack = KStandardAction::documentBack(this, SLOT(slotHistoryBack()), actionCollection());
3646     m_historyBack->setWhatsThis(i18n("Go to the place you were before"));
3647     connect(m_pageView.data(), &PageView::mouseBackButtonClick, m_historyBack, &QAction::trigger);
3648 
3649     m_historyNext = KStandardAction::documentForward(this, SLOT(slotHistoryNext()), actionCollection());
3650     m_historyNext->setWhatsThis(i18n("Go to the place you were after"));
3651     connect(m_pageView.data(), &PageView::mouseForwardButtonClick, m_historyNext, &QAction::trigger);
3652 
3653     m_pageView->setupActions(actionCollection());
3654 
3655     // attach the actions of the children widgets too
3656     m_formsMessage->addAction(m_pageView->toggleFormsAction());
3657 
3658     m_signatureMessage->addAction(m_showSignaturePanel);
3659 
3660     // ensure history actions are in the correct state
3661     updateViewActions();
3662 }
3663 
3664 bool Part::handleCompressed(QString &destpath, const QString &path, KCompressionDevice::CompressionType compressionType)
3665 {
3666     m_tempfile = nullptr;
3667 
3668     // we are working with a compressed file, decompressing
3669     // temporary file for decompressing
3670     QTemporaryFile *newtempfile = new QTemporaryFile();
3671     newtempfile->setAutoRemove(true);
3672 
3673     if (!newtempfile->open()) {
3674         KMessageBox::error(widget(),
3675                            i18n("<qt><strong>File Error!</strong> Could not create temporary file "
3676                                 "<nobr><strong>%1</strong></nobr>.</qt>",
3677                                 newtempfile->errorString()));
3678         delete newtempfile;
3679         return false;
3680     }
3681 
3682     // decompression filer
3683     KCompressionDevice dev(path, compressionType);
3684 
3685     if (!dev.open(QIODevice::ReadOnly)) {
3686         KMessageBox::detailedError(widget(),
3687                                    i18n("<qt><strong>File Error!</strong> Could not open the file "
3688                                         "<nobr><strong>%1</strong></nobr> for uncompression. "
3689                                         "The file will not be loaded.</qt>",
3690                                         path),
3691                                    i18n("<qt>This error typically occurs if you do "
3692                                         "not have enough permissions to read the file. "
3693                                         "You can check ownership and permissions if you "
3694                                         "right-click on the file in the Dolphin "
3695                                         "file manager, then choose the 'Properties' option, "
3696                                         "and select 'Permissions' tab in the opened window.</qt>"));
3697 
3698         delete newtempfile;
3699         return false;
3700     }
3701 
3702     char buf[65536];
3703     int read = 0;
3704 
3705     while ((read = dev.read(buf, sizeof(buf))) > 0) {
3706         int wrtn = newtempfile->write(buf, read);
3707         if (read != wrtn) {
3708             break;
3709         }
3710     }
3711     if ((read != 0) || (newtempfile->size() == 0)) {
3712         KMessageBox::detailedError(widget(),
3713                                    i18n("<qt><strong>File Error!</strong> Could not uncompress "
3714                                         "the file <nobr><strong>%1</strong></nobr>. "
3715                                         "The file will not be loaded.</qt>",
3716                                         path),
3717                                    i18n("<qt>This error typically occurs if the file is corrupt. "
3718                                         "If you want to be sure, try to decompress the file manually "
3719                                         "using command-line tools.</qt>"));
3720         delete newtempfile;
3721         return false;
3722     }
3723     m_tempfile = newtempfile;
3724     destpath = m_tempfile->fileName();
3725     return true;
3726 }
3727 
3728 void Part::rebuildBookmarkMenu(bool unplugActions)
3729 {
3730     if (unplugActions) {
3731         unplugActionList(QStringLiteral("bookmarks_currentdocument"));
3732         qDeleteAll(m_bookmarkActions);
3733         m_bookmarkActions.clear();
3734     }
3735     QUrl u = m_document->currentDocument();
3736     if (u.isValid()) {
3737         m_bookmarkActions = m_document->bookmarkManager()->actionsForUrl(u);
3738     }
3739     bool havebookmarks = true;
3740     if (m_bookmarkActions.isEmpty()) {
3741         havebookmarks = false;
3742         QAction *a = new QAction(nullptr);
3743         a->setText(i18n("No Bookmarks"));
3744         a->setEnabled(false);
3745         m_bookmarkActions.append(a);
3746     }
3747     plugActionList(QStringLiteral("bookmarks_currentdocument"), m_bookmarkActions);
3748 
3749     if (factory()) {
3750         const QList<KXMLGUIClient *> clients(factory()->clients());
3751         bool containerFound = false;
3752         for (int i = 0; !containerFound && i < clients.size(); ++i) {
3753             QMenu *container = dynamic_cast<QMenu *>(factory()->container(QStringLiteral("bookmarks"), clients[i]));
3754             if (container && container->actions().contains(m_bookmarkActions.first())) {
3755                 container->installEventFilter(this);
3756                 containerFound = true;
3757             }
3758         }
3759     }
3760 
3761     m_prevBookmark->setEnabled(havebookmarks);
3762     m_nextBookmark->setEnabled(havebookmarks);
3763 }
3764 
3765 bool Part::eventFilter(QObject *watched, QEvent *event)
3766 {
3767     switch (event->type()) {
3768     case QEvent::ContextMenu: {
3769         QContextMenuEvent *e = static_cast<QContextMenuEvent *>(event);
3770         QMenu *menu = static_cast<QMenu *>(watched);
3771 
3772         QScopedPointer<QMenu> ctxMenu(new QMenu);
3773 
3774         QPoint pos;
3775         bool ret = false;
3776         if (e->reason() == QContextMenuEvent::Mouse) {
3777             pos = e->pos();
3778             ret = aboutToShowContextMenu(menu, menu->actionAt(e->pos()), ctxMenu.data());
3779         } else if (menu->activeAction()) {
3780             pos = menu->actionGeometry(menu->activeAction()).center();
3781             ret = aboutToShowContextMenu(menu, menu->activeAction(), ctxMenu.data());
3782         }
3783         ctxMenu->exec(menu->mapToGlobal(pos));
3784 
3785         if (ret) {
3786             event->accept();
3787         }
3788         return ret;
3789     }
3790 
3791     default:
3792         break;
3793     }
3794 
3795     return KParts::ReadWritePart::eventFilter(watched, event);
3796 }
3797 
3798 void Part::updateAboutBackendAction()
3799 {
3800     const KPluginMetaData data = m_document->generatorInfo();
3801     m_aboutBackend->setEnabled(data.isValid());
3802 }
3803 
3804 void Part::resetStartArguments()
3805 {
3806     m_cliPrint = false;
3807     m_cliPrintAndExit = false;
3808 }
3809 
3810 #if HAVE_PURPOSE
3811 void Part::slotShareActionFinished(const QJsonObject &output, int error, const QString &message)
3812 {
3813     if (error) {
3814         KMessageBox::error(widget(), i18n("There was a problem sharing the document: %1", message), i18n("Share"));
3815     } else {
3816         const QString url = output[QStringLiteral("url")].toString();
3817         if (url.isEmpty()) {
3818             m_pageView->displayMessage(i18n("Document shared successfully"));
3819         } else {
3820             KMessageBox::information(widget(), i18n("You can find the shared document at: <a href=\"%1\">%1</a>", url), i18n("Share"), QString(), KMessageBox::Notify | KMessageBox::AllowLink);
3821         }
3822     }
3823 }
3824 #endif
3825 
3826 void Part::setReadWrite(bool readwrite)
3827 {
3828     m_document->setAnnotationEditingEnabled(readwrite);
3829     ReadWritePart::setReadWrite(readwrite);
3830 }
3831 
3832 void Part::enableStartWithFind(const QString &text)
3833 {
3834     m_textToFindOnOpen = QString(text);
3835 }
3836 
3837 void Part::setEditorCmd(const QString &editorCmd)
3838 {
3839     m_document->setEditorCommandOverride(editorCmd);
3840 }
3841 
3842 void Part::slotOpenContainingFolder()
3843 {
3844     KIO::highlightInFileManager({QUrl(localFilePath())});
3845 }
3846 
3847 } // namespace Okular
3848 
3849 #include "part.moc"
3850 
3851 /* kate: replace-tabs on; indent-width 4; */