File indexing completed on 2024-04-28 15:52:02

0001 /*
0002     SPDX-FileCopyrightText: 2002 Wilco Greven <greven@kde.org>
0003     SPDX-FileCopyrightText: 2002 Chris Cheney <ccheney@cheney.cx>
0004     SPDX-FileCopyrightText: 2003 Benjamin Meyer <benjamin@csh.rit.edu>
0005     SPDX-FileCopyrightText: 2003-2004 Christophe Devriese <Christophe.Devriese@student.kuleuven.ac.be>
0006     SPDX-FileCopyrightText: 2003 Laurent Montel <montel@kde.org>
0007     SPDX-FileCopyrightText: 2003-2004 Albert Astals Cid <aacid@kde.org>
0008     SPDX-FileCopyrightText: 2003 Luboš Luňák <l.lunak@kde.org>
0009     SPDX-FileCopyrightText: 2003 Malcolm Hunter <malcolm.hunter@gmx.co.uk>
0010     SPDX-FileCopyrightText: 2004 Dominique Devriese <devriese@kde.org>
0011     SPDX-FileCopyrightText: 2004 Dirk Mueller <mueller@kde.org>
0012 
0013     Work sponsored by the LiMux project of the city of Munich:
0014     SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
0015 
0016     SPDX-License-Identifier: GPL-2.0-or-later
0017 */
0018 
0019 #include "shell.h"
0020 
0021 // qt/kde includes
0022 #include <KActionCollection>
0023 #include <KConfigGroup>
0024 #include <KIO/Global>
0025 #include <KLocalizedString>
0026 #include <KMessageBox>
0027 #include <KPluginFactory>
0028 #include <KRecentFilesAction>
0029 #include <KSharedConfig>
0030 #include <KStandardAction>
0031 #ifndef Q_OS_WIN
0032 #include <KStartupInfo>
0033 #include <KWindowInfo>
0034 #endif
0035 #include <KToggleFullScreenAction>
0036 #include <KToolBar>
0037 #include <KUrlMimeData>
0038 #include <KWindowSystem>
0039 #include <KXMLGUIFactory>
0040 #include <QApplication>
0041 #if HAVE_DBUS
0042 #include <QDBusConnection>
0043 #endif // HAVE_DBUS
0044 #include <QDockWidget>
0045 #include <QDragMoveEvent>
0046 #include <QFileDialog>
0047 #include <QJsonArray>
0048 #include <QMenuBar>
0049 #include <QMimeData>
0050 #include <QObject>
0051 #include <QPointer>
0052 #include <QScreen>
0053 #include <QTabBar>
0054 #include <QTabWidget>
0055 #include <QTimer>
0056 #ifdef WITH_KACTIVITIES
0057 #include <PlasmaActivities/ResourceInstance>
0058 #endif
0059 
0060 #include <kio_version.h>
0061 #include <kxmlgui_version.h>
0062 
0063 // local includes
0064 #include "../interfaces/viewerinterface.h"
0065 #include "kdocumentviewer.h"
0066 #include "shellutils.h"
0067 
0068 static const char *shouldShowMenuBarComingFromFullScreen = "shouldShowMenuBarComingFromFullScreen";
0069 static const char *shouldShowToolBarComingFromFullScreen = "shouldShowToolBarComingFromFullScreen";
0070 
0071 static const char *const SESSION_URL_KEY = "Urls";
0072 static const char *const SESSION_TAB_KEY = "ActiveTab";
0073 
0074 static constexpr char SIDEBAR_LOCKED_KEY[] = "LockSidebar";
0075 static constexpr char SIDEBAR_VISIBLE_KEY[] = "ShowSidebar";
0076 
0077 static inline QString DesktopEntryGroupKey()
0078 {
0079     return QStringLiteral("Desktop Entry");
0080 }
0081 static inline QString RecentFilesGroupKey()
0082 {
0083     return QStringLiteral("Recent Files");
0084 }
0085 static inline QString GeneralGroupKey()
0086 {
0087     return QStringLiteral("General");
0088 }
0089 
0090 class ResizableStackedWidget : public QStackedWidget
0091 {
0092     Q_OBJECT
0093 
0094 public:
0095     QSize sizeHint() const override
0096     {
0097         return currentWidget()->sizeHint();
0098     }
0099     QSize minimumSizeHint() const override
0100     {
0101         return currentWidget()->minimumSizeHint();
0102     }
0103 };
0104 
0105 /**
0106  * Groups sidebar containers in a QDockWidget.
0107  *
0108  * This control groups all the sidebar containers provided by each tab (the Part object),
0109  * allowing the user to dock it to the left and right sides of the window,
0110  * or detach it from the window altogether.
0111  */
0112 class Sidebar : public QDockWidget
0113 {
0114     Q_OBJECT
0115 
0116 public:
0117     explicit Sidebar(QWidget *parent = nullptr)
0118         : QDockWidget(parent)
0119     {
0120         setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
0121         setFeatures(defaultFeatures());
0122 
0123         m_stackedWidget = new QStackedWidget;
0124         setWidget(m_stackedWidget);
0125         // It seems that without requesting a specific minimum size, Qt
0126         // somehow calculates a (0,-1) minimum size, and then Qt gets angry
0127         // that negative sizes is not possible.
0128         setMinimumSize(10, 10);
0129     }
0130 
0131     bool isLocked() const
0132     {
0133         return features().testFlag(NoDockWidgetFeatures);
0134     }
0135 
0136     void setLocked(bool locked)
0137     {
0138         setFeatures(locked ? NoDockWidgetFeatures : defaultFeatures());
0139 
0140         // show titlebar only if not locked
0141         if (locked) {
0142             if (!m_dumbTitleWidget) {
0143                 m_dumbTitleWidget = new QWidget;
0144             }
0145             setTitleBarWidget(m_dumbTitleWidget);
0146         } else {
0147             setTitleBarWidget(nullptr);
0148         }
0149     }
0150 
0151     int indexOf(QWidget *widget) const
0152     {
0153         return m_stackedWidget->indexOf(widget);
0154     }
0155 
0156     void addWidget(QWidget *widget)
0157     {
0158         m_stackedWidget->addWidget(widget);
0159     }
0160 
0161     void removeWidget(QWidget *widget)
0162     {
0163         m_stackedWidget->removeWidget(widget);
0164     }
0165 
0166     void setCurrentWidget(QWidget *widget)
0167     {
0168         m_stackedWidget->setCurrentWidget(widget);
0169     }
0170 
0171 private:
0172     static DockWidgetFeatures defaultFeatures()
0173     {
0174         DockWidgetFeatures dockFeatures = DockWidgetClosable | DockWidgetMovable;
0175         if (!KWindowSystem::isPlatformWayland()) { // TODO : Remove this check when QTBUG-87332 is fixed
0176             dockFeatures |= DockWidgetFloatable;
0177         }
0178 
0179         return dockFeatures;
0180     }
0181 
0182     QStackedWidget *m_stackedWidget = nullptr;
0183     QWidget *m_dumbTitleWidget = nullptr;
0184 };
0185 
0186 Shell::Shell(const QString &serializedOptions)
0187     : KParts::MainWindow()
0188     , m_menuBarWasShown(true)
0189     , m_toolBarWasShown(true)
0190     , m_isValid(true)
0191 {
0192     setObjectName(QStringLiteral("okular::Shell#"));
0193     setContextMenuPolicy(Qt::NoContextMenu);
0194     // otherwise .rc file won't be found by unit test
0195     setComponentName(QStringLiteral("okular"), QString());
0196     // set the shell's ui resource file
0197     setXMLFile(QStringLiteral("shell.rc"));
0198     m_fileformatsscanned = false;
0199     m_showMenuBarAction = nullptr;
0200     // this routine will find and load our Part.  it finds the Part by
0201     // name which is a bad idea usually.. but it's alright in this
0202     // case since our Part is made for this Shell
0203 
0204     const auto result = KPluginFactory::loadFactory(KPluginMetaData(QStringLiteral("kf6/parts/okularpart")));
0205 
0206     if (!result) {
0207         // if we couldn't find our Part, we exit since the Shell by
0208         // itself can't do anything useful
0209         m_isValid = false;
0210         KMessageBox::error(this, i18n("Unable to find the Okular component: %1", result.errorString));
0211         return;
0212     } else {
0213         m_partFactory = result.plugin;
0214     }
0215 
0216     // now that the Part plugin is loaded, create the part
0217     KParts::ReadWritePart *const firstPart = m_partFactory->create<KParts::ReadWritePart>(this);
0218     if (firstPart) {
0219         // Setup the central widget
0220         m_centralStackedWidget = new ResizableStackedWidget();
0221         setCentralWidget(m_centralStackedWidget);
0222 
0223         // Setup the welcome screen
0224         m_welcomeScreen = new WelcomeScreen(this);
0225         connect(m_welcomeScreen, &WelcomeScreen::openClicked, this, &Shell::fileOpen);
0226         connect(m_welcomeScreen, &WelcomeScreen::closeClicked, this, &Shell::hideWelcomeScreen);
0227         connect(m_welcomeScreen, &WelcomeScreen::recentItemClicked, this, [this](const QUrl &url) { openUrl(url); });
0228         connect(m_welcomeScreen, &WelcomeScreen::forgetRecentItem, this, &Shell::forgetRecentItem);
0229         m_centralStackedWidget->addWidget(m_welcomeScreen);
0230 
0231         m_welcomeScreen->installEventFilter(this);
0232 
0233         // Setup tab bar
0234         m_tabWidget = new QTabWidget(this);
0235         m_tabWidget->setTabsClosable(true);
0236         m_tabWidget->setElideMode(Qt::ElideRight);
0237         m_tabWidget->tabBar()->hide();
0238         m_tabWidget->setDocumentMode(true);
0239         m_tabWidget->setMovable(true);
0240 
0241         m_tabWidget->setAcceptDrops(true);
0242         m_tabWidget->tabBar()->installEventFilter(this);
0243 
0244         m_centralStackedWidget->addWidget(m_tabWidget);
0245 
0246         connect(m_tabWidget, &QTabWidget::currentChanged, this, &Shell::setActiveTab);
0247         connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &Shell::closeTab);
0248         connect(m_tabWidget->tabBar(), &QTabBar::tabMoved, this, &Shell::moveTabData);
0249 
0250         m_sidebar = new Sidebar;
0251         m_sidebar->setObjectName(QStringLiteral("okular_sidebar"));
0252         m_sidebar->setContextMenuPolicy(Qt::ActionsContextMenu);
0253         m_sidebar->setWindowTitle(i18n("Sidebar"));
0254         connect(m_sidebar, &QDockWidget::visibilityChanged, this, [this](bool visible) {
0255             // sync sidebar visibility with the m_showSidebarAction only if welcome screen is hidden
0256             if (m_showSidebarAction && m_centralStackedWidget->currentWidget() != m_welcomeScreen) {
0257                 m_showSidebarAction->setChecked(visible);
0258             }
0259             if (m_centralStackedWidget->currentWidget() == m_welcomeScreen) {
0260                 // MainWindow tries hard to make its child dockwidgets shown, but during
0261                 // welcome screen we don't want to see the sidebar,
0262                 // so try a bit more to actually hide it.
0263                 m_sidebar->hide();
0264             }
0265         });
0266         addDockWidget(Qt::LeftDockWidgetArea, m_sidebar);
0267 
0268         // then, setup our actions
0269         setupActions();
0270         connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &QObject::deleteLater);
0271         // and integrate the part's GUI with the shell's
0272         setupGUI(Keys | ToolBar | Save);
0273 
0274         // NOTE : apply default sidebar width only after calling setupGUI(...)
0275         resizeDocks({m_sidebar}, {200}, Qt::Horizontal);
0276 
0277         m_tabs.append(TabState(firstPart));
0278         m_tabWidget->addTab(firstPart->widget(), QString()); // triggers setActiveTab that calls createGUI( part )
0279 
0280         connectPart(firstPart);
0281 
0282         readSettings();
0283 
0284         m_unique = ShellUtils::unique(serializedOptions);
0285 #if HAVE_DBUS
0286         if (m_unique) {
0287             m_unique = QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.okular"));
0288             if (!m_unique) {
0289                 KMessageBox::information(this, i18n("There is already a unique Okular instance running. This instance won't be the unique one."));
0290             }
0291         } else {
0292             QString serviceName = QStringLiteral("org.kde.okular-") + QString::number(qApp->applicationPid());
0293             QDBusConnection::sessionBus().registerService(serviceName);
0294         }
0295         if (ShellUtils::noRaise(serializedOptions)) {
0296             setAttribute(Qt::WA_ShowWithoutActivating);
0297         }
0298 
0299         {
0300             const QString editorCmd = ShellUtils::editorCmd(serializedOptions);
0301             if (!editorCmd.isEmpty()) {
0302                 QMetaObject::invokeMethod(firstPart, "setEditorCmd", Q_ARG(QString, editorCmd));
0303             }
0304         }
0305 
0306         QDBusConnection::sessionBus().registerObject(QStringLiteral("/okularshell"), this, QDBusConnection::ExportScriptableSlots);
0307 #endif // HAVE_DBUS
0308 
0309         // Make sure that the welcome scren is visible on startup.
0310         showWelcomeScreen();
0311     } else {
0312         m_isValid = false;
0313         KMessageBox::error(this, i18n("Unable to find the Okular component."));
0314     }
0315 
0316     connect(guiFactory(), &KXMLGUIFactory::shortcutsSaved, this, &Shell::reloadAllXML);
0317 }
0318 
0319 void Shell::reloadAllXML()
0320 {
0321     for (const TabState &tab : std::as_const(m_tabs)) {
0322         tab.part->reloadXML();
0323     }
0324 }
0325 
0326 void Shell::keyPressEvent(QKeyEvent *e)
0327 {
0328     if (e->key() == Qt::Key_Escape && window()->isFullScreen()) {
0329         setFullScreen(false);
0330     }
0331 }
0332 
0333 bool Shell::eventFilter(QObject *obj, QEvent *event)
0334 {
0335     QDragMoveEvent *dmEvent = dynamic_cast<QDragMoveEvent *>(event);
0336     if (dmEvent) {
0337         bool accept = dmEvent->mimeData()->hasUrls();
0338         event->setAccepted(accept);
0339         return accept;
0340     }
0341 
0342     QDropEvent *dEvent = dynamic_cast<QDropEvent *>(event);
0343     if (dEvent) {
0344         const QList<QUrl> list = KUrlMimeData::urlsFromMimeData(dEvent->mimeData());
0345         handleDroppedUrls(list);
0346         dEvent->setAccepted(true);
0347         return true;
0348     }
0349 
0350     // Handle middle button click events on the tab bar
0351     if (obj == m_tabWidget->tabBar() && event->type() == QEvent::MouseButtonRelease) {
0352         QMouseEvent *mEvent = static_cast<QMouseEvent *>(event);
0353         if (mEvent->button() == Qt::MiddleButton) {
0354             int tabIndex = m_tabWidget->tabBar()->tabAt(mEvent->pos());
0355             if (tabIndex != -1) {
0356                 closeTab(tabIndex);
0357                 return true;
0358             }
0359         }
0360     }
0361     return KParts::MainWindow::eventFilter(obj, event);
0362 }
0363 
0364 bool Shell::isValid() const
0365 {
0366     return m_isValid;
0367 }
0368 
0369 void Shell::showOpenRecentMenu()
0370 {
0371     m_recent->menu()->popup(QCursor::pos());
0372 }
0373 
0374 Shell::~Shell()
0375 {
0376     if (!m_tabs.empty()) {
0377         writeSettings();
0378         for (const TabState &tab : std::as_const(m_tabs)) {
0379             tab.part->closeUrl(false);
0380         }
0381         m_tabs.clear();
0382     }
0383 #if HAVE_DBUS
0384     if (m_unique) {
0385         QDBusConnection::sessionBus().unregisterService(QStringLiteral("org.kde.okular"));
0386     }
0387 #endif // HAVE_DBUS
0388 
0389     delete m_tabWidget;
0390 }
0391 
0392 // Open a new document if we have space for it
0393 // This can hang if called on a unique instance and openUrl pops a messageBox
0394 bool Shell::openDocument(const QUrl &url, const QString &serializedOptions)
0395 {
0396     if (m_tabs.size() <= 0) {
0397         return false;
0398     }
0399 
0400     hideWelcomeScreen();
0401 
0402     KParts::ReadWritePart *const part = m_tabs[0].part;
0403 
0404     // Return false if we can't open new tabs and the only part is occupied
0405     if (!qobject_cast<Okular::ViewerInterface *>(part)->openNewFilesInTabs() && !part->url().isEmpty() && !ShellUtils::unique(serializedOptions)) {
0406         return false;
0407     }
0408 
0409     openUrl(url, serializedOptions);
0410 
0411     return true;
0412 }
0413 
0414 bool Shell::openDocument(const QString &urlString, const QString &serializedOptions)
0415 {
0416     return openDocument(QUrl(urlString), serializedOptions);
0417 }
0418 
0419 bool Shell::canOpenDocs(int numDocs, int desktop)
0420 {
0421     if (m_tabs.size() <= 0 || numDocs <= 0 || m_unique) {
0422         return false;
0423     }
0424 
0425     KParts::ReadWritePart *const part = m_tabs[0].part;
0426     const bool allowTabs = qobject_cast<Okular::ViewerInterface *>(part)->openNewFilesInTabs();
0427 
0428     if (!allowTabs && (numDocs > 1 || !part->url().isEmpty())) {
0429         return false;
0430     }
0431 
0432 #ifndef Q_OS_WIN
0433     const KWindowInfo winfo(window()->effectiveWinId(), NET::WMDesktop);
0434     if (winfo.desktop() != desktop) {
0435         return false;
0436     }
0437 #endif
0438 
0439     return true;
0440 }
0441 
0442 void Shell::openUrl(const QUrl &url, const QString &serializedOptions)
0443 {
0444     hideWelcomeScreen();
0445 
0446     const int activeTab = m_tabWidget->currentIndex();
0447     KParts::ReadWritePart *const activePart = m_tabs[activeTab].part;
0448     if (!activePart->url().isEmpty()) {
0449         if (m_unique) {
0450             applyOptionsToPart(activePart, serializedOptions);
0451             activePart->openUrl(url);
0452         } else {
0453             if (qobject_cast<Okular::ViewerInterface *>(activePart)->openNewFilesInTabs()) {
0454                 openNewTab(url, serializedOptions);
0455             } else {
0456                 Shell *newShell = new Shell(serializedOptions);
0457                 newShell->show();
0458                 newShell->openUrl(url, serializedOptions);
0459             }
0460         }
0461     } else {
0462         m_tabWidget->setTabText(activeTab, url.fileName());
0463         m_tabWidget->setTabToolTip(activeTab, url.fileName());
0464 
0465         applyOptionsToPart(activePart, serializedOptions);
0466         bool openOk = activePart->openUrl(url);
0467         const bool isstdin = url.fileName() == QLatin1String("-") || url.scheme() == QLatin1String("fd");
0468         if (!isstdin) {
0469             if (openOk) {
0470 #ifdef WITH_KACTIVITIES
0471                 KActivities::ResourceInstance::notifyAccessed(url);
0472 #endif
0473                 m_recent->addUrl(url);
0474             } else {
0475                 m_recent->removeUrl(url);
0476                 closeTab(activeTab);
0477             }
0478         }
0479     }
0480 }
0481 
0482 void Shell::closeUrl()
0483 {
0484     closeTab(m_tabWidget->currentIndex());
0485 
0486     // When closing the current tab two things can happen:
0487     //  * the focus was on the tab
0488     //  * the focus was somewhere in the toolbar
0489     // we don't have other places that accept focus
0490     //  * If it was on the tab, logic says it should go back to the next current tab
0491     //  * If it was on the toolbar, we could leave it there, but since we redo the menus/toolbars for the new tab, it gets kind of lost
0492     //    so it's easier to set it to the next current tab which also makes sense as consistency
0493     if (m_tabWidget->count() >= 0) {
0494         KParts::ReadWritePart *const newPart = m_tabs[m_tabWidget->currentIndex()].part;
0495         newPart->widget()->setFocus();
0496     }
0497 }
0498 
0499 void Shell::readSettings()
0500 {
0501     m_recent->loadEntries(KSharedConfig::openConfig()->group(RecentFilesGroupKey()));
0502     m_recent->setEnabled(true); // force enabling
0503 
0504     const KConfigGroup group = KSharedConfig::openConfig()->group(DesktopEntryGroupKey());
0505     bool fullScreen = group.readEntry("FullScreen", false);
0506     setFullScreen(fullScreen);
0507 
0508     if (fullScreen) {
0509         m_menuBarWasShown = group.readEntry(shouldShowMenuBarComingFromFullScreen, true);
0510         m_toolBarWasShown = group.readEntry(shouldShowToolBarComingFromFullScreen, true);
0511     }
0512 
0513     const KConfigGroup sidebarGroup = KSharedConfig::openConfig()->group(GeneralGroupKey());
0514     m_sidebar->setVisible(sidebarGroup.readEntry(SIDEBAR_VISIBLE_KEY, true));
0515     m_sidebar->setLocked(sidebarGroup.readEntry(SIDEBAR_LOCKED_KEY, true));
0516 
0517     m_showSidebarAction->setChecked(m_sidebar->isVisibleTo(this));
0518     m_lockSidebarAction->setChecked(m_sidebar->isLocked());
0519 }
0520 
0521 void Shell::writeSettings()
0522 {
0523     saveRecents();
0524 
0525     KConfigGroup sidebarGroup = KSharedConfig::openConfig()->group(GeneralGroupKey());
0526     sidebarGroup.writeEntry(SIDEBAR_LOCKED_KEY, m_sidebar->isLocked());
0527     // NOTE : Consider whether the m_showSidebarAction is checked, because
0528     // the sidebar can be forcibly hidden if the welcome screen is displayed
0529     sidebarGroup.writeEntry(SIDEBAR_VISIBLE_KEY, m_sidebar->isVisibleTo(this) || m_showSidebarAction->isChecked());
0530 
0531     KConfigGroup group = KSharedConfig::openConfig()->group(DesktopEntryGroupKey());
0532     group.writeEntry("FullScreen", m_fullScreenAction->isChecked());
0533     if (m_fullScreenAction->isChecked()) {
0534         group.writeEntry(shouldShowMenuBarComingFromFullScreen, m_menuBarWasShown);
0535         group.writeEntry(shouldShowToolBarComingFromFullScreen, m_toolBarWasShown);
0536     }
0537     KSharedConfig::openConfig()->sync();
0538 }
0539 
0540 void Shell::saveRecents()
0541 {
0542     m_recent->saveEntries(KSharedConfig::openConfig()->group(RecentFilesGroupKey()));
0543 }
0544 
0545 void Shell::setupActions()
0546 {
0547     KStandardAction::open(this, SLOT(fileOpen()), actionCollection());
0548     m_recent = KStandardAction::openRecent(this, SLOT(openUrl(QUrl)), actionCollection());
0549     m_recent->setToolBarMode(KRecentFilesAction::MenuMode);
0550     connect(m_recent, &QAction::triggered, this, &Shell::showOpenRecentMenu);
0551     connect(m_recent, &KRecentFilesAction::recentListCleared, this, &Shell::refreshRecentsOnWelcomeScreen);
0552     connect(m_welcomeScreen, &WelcomeScreen::forgetAllRecents, m_recent, &KRecentFilesAction::clear);
0553     m_recent->setToolTip(i18n("Click to open a file\nClick and hold to open a recent file"));
0554     m_recent->setWhatsThis(i18n("<b>Click</b> to open a file or <b>Click and hold</b> to select a recent file"));
0555     m_printAction = KStandardAction::print(this, SLOT(print()), actionCollection());
0556     m_printAction->setEnabled(false);
0557     m_closeAction = KStandardAction::close(this, SLOT(closeUrl()), actionCollection());
0558     m_closeAction->setEnabled(false);
0559     KStandardAction::quit(this, SLOT(close()), actionCollection());
0560 
0561     setStandardToolBarMenuEnabled(true);
0562 
0563     m_showMenuBarAction = KStandardAction::showMenubar(this, SLOT(slotShowMenubar()), actionCollection());
0564     m_fullScreenAction = KStandardAction::fullScreen(this, SLOT(slotUpdateFullScreen()), this, actionCollection());
0565 
0566     m_nextTabAction = actionCollection()->addAction(QStringLiteral("tab-next"));
0567     m_nextTabAction->setText(i18n("Next Tab"));
0568     actionCollection()->setDefaultShortcuts(m_nextTabAction, KStandardShortcut::tabNext());
0569     m_nextTabAction->setEnabled(false);
0570     connect(m_nextTabAction, &QAction::triggered, this, &Shell::activateNextTab);
0571 
0572     m_prevTabAction = actionCollection()->addAction(QStringLiteral("tab-previous"));
0573     m_prevTabAction->setText(i18n("Previous Tab"));
0574     actionCollection()->setDefaultShortcuts(m_prevTabAction, KStandardShortcut::tabPrev());
0575     m_prevTabAction->setEnabled(false);
0576     connect(m_prevTabAction, &QAction::triggered, this, &Shell::activatePrevTab);
0577 
0578     m_undoCloseTab = actionCollection()->addAction(QStringLiteral("undo-close-tab"));
0579     m_undoCloseTab->setText(i18n("Undo close tab"));
0580     actionCollection()->setDefaultShortcut(m_undoCloseTab, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_T));
0581     m_undoCloseTab->setIcon(QIcon::fromTheme(QStringLiteral("edit-undo")));
0582     m_undoCloseTab->setEnabled(false);
0583     connect(m_undoCloseTab, &QAction::triggered, this, &Shell::undoCloseTab);
0584 
0585     m_lockSidebarAction = actionCollection()->addAction(QStringLiteral("okular_lock_sidebar"));
0586     m_lockSidebarAction->setCheckable(true);
0587     m_lockSidebarAction->setIcon(QIcon::fromTheme(QStringLiteral("lock")));
0588     m_lockSidebarAction->setText(i18n("Lock Sidebar"));
0589     connect(m_lockSidebarAction, &QAction::triggered, m_sidebar, &Sidebar::setLocked);
0590     m_sidebar->addAction(m_lockSidebarAction);
0591 }
0592 
0593 void Shell::saveProperties(KConfigGroup &group)
0594 {
0595     if (!m_isValid) { // part couldn't be loaded, nothing to save
0596         return;
0597     }
0598 
0599     // Gather lists of settings to preserve
0600     QStringList urls;
0601     for (const TabState &tab : std::as_const(m_tabs)) {
0602         urls.append(tab.part->url().url());
0603     }
0604     group.writePathEntry(SESSION_URL_KEY, urls);
0605     group.writeEntry(SESSION_TAB_KEY, m_tabWidget->currentIndex());
0606 }
0607 
0608 void Shell::readProperties(const KConfigGroup &group)
0609 {
0610     // Reopen documents based on saved settings
0611     QStringList urls = group.readPathEntry(SESSION_URL_KEY, QStringList());
0612     int desiredTab = group.readEntry<int>(SESSION_TAB_KEY, 0);
0613 
0614     while (!urls.isEmpty()) {
0615         openUrl(QUrl(urls.takeFirst()));
0616     }
0617 
0618     if (desiredTab < m_tabs.size()) {
0619         setActiveTab(desiredTab);
0620     }
0621 }
0622 
0623 void Shell::fileOpen()
0624 {
0625     // this slot is called whenever the File->Open menu is selected,
0626     // the Open shortcut is pressed (usually CTRL+O) or the Open toolbar
0627     // button is clicked
0628     const int activeTab = m_tabWidget->currentIndex();
0629     if (!m_fileformatsscanned) {
0630         const KDocumentViewer *const doc = qobject_cast<KDocumentViewer *>(m_tabs[activeTab].part);
0631         Q_ASSERT(doc);
0632 
0633         m_fileformats = doc->supportedMimeTypes();
0634 
0635         m_fileformatsscanned = true;
0636     }
0637 
0638     QUrl startDir;
0639     const KParts::ReadWritePart *const curPart = m_tabs[activeTab].part;
0640     if (curPart->url().isLocalFile()) {
0641         startDir = KIO::upUrl(curPart->url());
0642     }
0643     if (startDir.isEmpty() || (startDir == QUrl::fromLocalFile(QDir::rootPath()))) {
0644         startDir = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
0645     }
0646 
0647     QPointer<QFileDialog> dlg(new QFileDialog(this));
0648     dlg->setDirectoryUrl(startDir);
0649     dlg->setAcceptMode(QFileDialog::AcceptOpen);
0650     dlg->setOption(QFileDialog::HideNameFilterDetails, true);
0651     dlg->setFileMode(QFileDialog::ExistingFiles); // Allow selection of more than one file
0652 
0653     QMimeDatabase mimeDatabase;
0654     // Unfortunately non Plasma file dialogs don't support the "All supported files" when using
0655     // setMimeTypeFilters instead of setNameFilters, so for those use setNameFilters which is a bit
0656     // worse because doesn't show you pdf files named bla.blo when you say "show me the pdf files", but
0657     // that's solvable by choosing "All Files" and it's not that common while it's more convenient to
0658     // only get shown the files that the application can open by default instead of all of them
0659     const bool useMimeTypeFilters = qgetenv("XDG_CURRENT_DESKTOP").toLower() == "kde";
0660     if (useMimeTypeFilters) {
0661         QStringList mimetypes;
0662         for (const QString &mimeName : std::as_const(m_fileformats)) {
0663             QMimeType mimeType = mimeDatabase.mimeTypeForName(mimeName);
0664             mimetypes << mimeType.name();
0665         }
0666         mimetypes.prepend(QStringLiteral("application/octet-stream"));
0667         dlg->setMimeTypeFilters(mimetypes);
0668     } else {
0669         QSet<QString> globPatterns;
0670         QMap<QString, QStringList> namedGlobs;
0671         for (const QString &mimeName : std::as_const(m_fileformats)) {
0672             QMimeType mimeType = mimeDatabase.mimeTypeForName(mimeName);
0673             const QStringList globs(mimeType.globPatterns());
0674             if (globs.isEmpty()) {
0675                 continue;
0676             }
0677 
0678             globPatterns.unite(QSet<QString>(globs.begin(), globs.end()));
0679 
0680             namedGlobs[mimeType.comment()].append(globs);
0681         }
0682         QStringList namePatterns;
0683         for (auto it = namedGlobs.cbegin(); it != namedGlobs.cend(); ++it) {
0684             namePatterns.append(it.key() + QLatin1String(" (") + it.value().join(QLatin1Char(' ')) + QLatin1Char(')'));
0685         }
0686 
0687         const QStringList allGlobPatterns = globPatterns.values();
0688         namePatterns.prepend(i18n("All files (*)"));
0689         namePatterns.prepend(i18n("All supported files (%1)", allGlobPatterns.join(QLatin1Char(' '))));
0690         dlg->setNameFilters(namePatterns);
0691     }
0692 
0693     dlg->setWindowTitle(i18n("Open Document"));
0694     if (dlg->exec() && dlg) {
0695         const QList<QUrl> urlList = dlg->selectedUrls();
0696         for (const QUrl &url : urlList) {
0697             openUrl(url);
0698         }
0699     }
0700 
0701     if (dlg) {
0702         delete dlg.data();
0703     }
0704 }
0705 
0706 void Shell::tryRaise(const QString &startupId)
0707 {
0708 #ifndef Q_OS_WIN
0709     if (KWindowSystem::isPlatformWayland()) {
0710         KWindowSystem::setCurrentXdgActivationToken(startupId);
0711     } else if (KWindowSystem::isPlatformX11()) {
0712         KStartupInfo::setNewStartupId(window()->windowHandle(), startupId.toUtf8());
0713     }
0714 #else
0715     Q_UNUSED(startupId);
0716 #endif
0717 
0718     KWindowSystem::activateWindow(window()->windowHandle());
0719 }
0720 
0721 // only called when starting the program
0722 void Shell::setFullScreen(bool useFullScreen)
0723 {
0724     if (useFullScreen) {
0725         setWindowState(windowState() | Qt::WindowFullScreen); // set
0726     } else {
0727         setWindowState(windowState() & ~Qt::WindowFullScreen); // reset
0728     }
0729 }
0730 
0731 void Shell::setCaption(const QString &caption)
0732 {
0733     bool modified = false;
0734 
0735     const int activeTab = m_tabWidget->currentIndex();
0736     if (activeTab != -1) {
0737         KParts::ReadWritePart *const activePart = m_tabs[activeTab].part;
0738         QString tabCaption = activePart->url().fileName();
0739         if (activePart->isModified()) {
0740             modified = true;
0741             if (!tabCaption.isEmpty()) {
0742                 tabCaption.append(QStringLiteral(" *"));
0743             }
0744         }
0745 
0746         m_tabWidget->setTabText(activeTab, tabCaption);
0747     }
0748 
0749     setCaption(caption, modified);
0750 }
0751 
0752 void Shell::showEvent(QShowEvent *e)
0753 {
0754     if (!menuBar()->isNativeMenuBar() && m_showMenuBarAction) {
0755         m_showMenuBarAction->setChecked(menuBar()->isVisible());
0756     }
0757 
0758     KParts::MainWindow::showEvent(e);
0759 }
0760 
0761 void Shell::slotUpdateFullScreen()
0762 {
0763     if (m_fullScreenAction->isChecked()) {
0764         m_menuBarWasShown = !menuBar()->isHidden();
0765         menuBar()->hide();
0766 
0767         m_toolBarWasShown = !toolBar()->isHidden();
0768         toolBar()->hide();
0769 
0770         KToggleFullScreenAction::setFullScreen(this, true);
0771     } else {
0772         if (m_menuBarWasShown) {
0773             menuBar()->show();
0774         }
0775         if (m_toolBarWasShown) {
0776             toolBar()->show();
0777         }
0778         KToggleFullScreenAction::setFullScreen(this, false);
0779     }
0780 }
0781 
0782 void Shell::slotShowMenubar()
0783 {
0784     if (menuBar()->isHidden()) {
0785         menuBar()->show();
0786     } else {
0787         menuBar()->hide();
0788     }
0789 }
0790 
0791 QSize Shell::sizeHint() const
0792 {
0793     const QSize baseSize = QApplication::primaryScreen()->availableSize() * 0.6;
0794     // Set an arbitrary yet sensible sane minimum size for very small screens;
0795     // for example we don't want people using 1366x768 screens to get a tiny
0796     // default window size of 820 x 460 which will elide most of the toolbar buttons.
0797     return baseSize.expandedTo(QSize(1000, 700));
0798 }
0799 
0800 bool Shell::queryClose()
0801 {
0802     if (m_tabs.count() > 1) {
0803         const QString dontAskAgainName = QStringLiteral("ShowTabWarning");
0804         KMessageBox::ButtonCode dummy = KMessageBox::PrimaryAction;
0805         if (KMessageBox::shouldBeShownTwoActions(dontAskAgainName, dummy)) {
0806             QDialog *dialog = new QDialog(this);
0807             dialog->setWindowTitle(i18n("Confirm Close"));
0808 
0809             QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog);
0810             buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No);
0811             KGuiItem::assign(buttonBox->button(QDialogButtonBox::Yes), KGuiItem(i18n("Close Tabs"), QStringLiteral("tab-close")));
0812             KGuiItem::assign(buttonBox->button(QDialogButtonBox::No), KStandardGuiItem::cancel());
0813 
0814             bool checkboxResult = true;
0815             const int result = KMessageBox::createKMessageBox(dialog,
0816                                                               buttonBox,
0817                                                               QMessageBox::Question,
0818                                                               i18n("You are about to close %1 tabs. Are you sure you want to continue?", m_tabs.count()),
0819                                                               QStringList(),
0820                                                               i18n("Warn me when I attempt to close multiple tabs"),
0821                                                               &checkboxResult,
0822                                                               KMessageBox::Notify);
0823 
0824             if (!checkboxResult) {
0825                 KMessageBox::saveDontShowAgainTwoActions(dontAskAgainName, dummy);
0826             }
0827 
0828             if (result != QDialogButtonBox::Yes) {
0829                 return false;
0830             }
0831         }
0832     }
0833 
0834     for (int i = 0; i < m_tabs.size(); ++i) {
0835         KParts::ReadWritePart *const part = m_tabs[i].part;
0836 
0837         // To resolve confusion about multiple modified docs, switch to relevant tab
0838         if (part->isModified()) {
0839             setActiveTab(i);
0840         }
0841 
0842         if (!part->queryClose()) {
0843             return false;
0844         }
0845     }
0846     return true;
0847 }
0848 
0849 void Shell::setActiveTab(int tab)
0850 {
0851     if (m_showSidebarAction) {
0852         m_showSidebarAction->disconnect();
0853     }
0854 
0855     m_tabWidget->setCurrentIndex(tab);
0856 
0857     // NOTE : createGUI(...) breaks the visibility of the sidebar, so we need
0858     // to save and restore it
0859     const bool isSidebarVisible = m_sidebar->isVisible();
0860     createGUI(m_tabs[tab].part);
0861     m_sidebar->setVisible(isSidebarVisible);
0862 
0863     // dock KPart's sidebar if new and make it current
0864     Okular::ViewerInterface *iPart = qobject_cast<Okular::ViewerInterface *>(m_tabs[tab].part);
0865     Q_ASSERT(iPart);
0866     QWidget *sideContainer = iPart->getSideContainer();
0867     if (m_sidebar->indexOf(sideContainer) == -1) {
0868         m_sidebar->addWidget(sideContainer);
0869         if (m_sidebar->maximumWidth() > sideContainer->maximumWidth()) {
0870             m_sidebar->setMaximumWidth(sideContainer->maximumWidth());
0871         }
0872     }
0873     m_sidebar->setCurrentWidget(sideContainer);
0874 
0875     m_showSidebarAction = m_tabs[tab].part->actionCollection()->action(QStringLiteral("show_leftpanel"));
0876     Q_ASSERT(m_showSidebarAction);
0877     m_showSidebarAction->disconnect();
0878     m_showSidebarAction->setChecked(m_sidebar->isVisibleTo(this));
0879     connect(m_showSidebarAction, &QAction::triggered, m_sidebar, &Sidebar::setVisible);
0880 
0881     m_printAction->setEnabled(m_tabs[tab].printEnabled);
0882     m_closeAction->setEnabled(m_tabs[tab].closeEnabled);
0883 }
0884 
0885 void Shell::closeTab(int tab)
0886 {
0887     KParts::ReadWritePart *const part = m_tabs[tab].part;
0888     QUrl url = part->url();
0889     bool closeSuccess = part->closeUrl();
0890     if (closeSuccess && m_tabs.count() > 1) {
0891         if (part->factory()) {
0892             part->factory()->removeClient(part);
0893         }
0894         part->disconnect();
0895 
0896         Okular::ViewerInterface *iPart = qobject_cast<Okular::ViewerInterface *>(m_tabs[tab].part);
0897         Q_ASSERT(iPart);
0898         QWidget *sideContainer = iPart->getSideContainer();
0899         m_sidebar->removeWidget(sideContainer);
0900         connect(part, &QObject::destroyed, sideContainer, &QObject::deleteLater);
0901 
0902         part->deleteLater();
0903         m_tabs.removeAt(tab);
0904         m_tabWidget->removeTab(tab);
0905         m_undoCloseTab->setEnabled(true);
0906         m_closedTabUrls.append(url);
0907 
0908         if (m_tabWidget->count() == 1) {
0909             m_tabWidget->tabBar()->hide();
0910             m_nextTabAction->setEnabled(false);
0911             m_prevTabAction->setEnabled(false);
0912         }
0913     } else if (closeSuccess && m_tabs.count() == 1) {
0914         // Show welcome screen when the last tab is closed.
0915 
0916         showWelcomeScreen();
0917     }
0918 }
0919 
0920 void Shell::openNewTab(const QUrl &url, const QString &serializedOptions)
0921 {
0922     const int previousActiveTab = m_tabWidget->currentIndex();
0923     KParts::ReadWritePart *const activePart = m_tabs[previousActiveTab].part;
0924 
0925     hideWelcomeScreen();
0926 
0927     bool activateTabIfAlreadyOpen;
0928     QMetaObject::invokeMethod(activePart, "activateTabIfAlreadyOpenFile", Q_RETURN_ARG(bool, activateTabIfAlreadyOpen));
0929 
0930     if (activateTabIfAlreadyOpen) {
0931         const int tabIndex = findTabIndex(url);
0932 
0933         if (tabIndex >= 0) {
0934             setActiveTab(tabIndex);
0935             m_recent->addUrl(url);
0936             return;
0937         }
0938     }
0939 
0940     // Tabs are hidden when there's only one, so show it
0941     if (m_tabs.size() == 1) {
0942         m_tabWidget->tabBar()->show();
0943         m_nextTabAction->setEnabled(true);
0944         m_prevTabAction->setEnabled(true);
0945     }
0946 
0947     const int newIndex = m_tabs.size();
0948 
0949     // Make new part
0950     m_tabs.append(TabState(m_partFactory->create<KParts::ReadWritePart>(this)));
0951     connectPart(m_tabs[newIndex].part);
0952 
0953     // Update GUI
0954     KParts::ReadWritePart *const part = m_tabs[newIndex].part;
0955     m_tabWidget->addTab(part->widget(), url.fileName());
0956     m_tabWidget->setTabToolTip(newIndex, url.fileName());
0957 
0958     applyOptionsToPart(part, serializedOptions);
0959 
0960     setActiveTab(m_tabs.size() - 1);
0961 
0962     if (part->openUrl(url)) {
0963         m_recent->addUrl(url);
0964     } else {
0965         setActiveTab(previousActiveTab);
0966         closeTab(m_tabs.size() - 1);
0967         m_recent->removeUrl(url);
0968     }
0969 }
0970 
0971 void Shell::applyOptionsToPart(QObject *part, const QString &serializedOptions)
0972 {
0973     KDocumentViewer *const doc = qobject_cast<KDocumentViewer *>(part);
0974     const QString find = ShellUtils::find(serializedOptions);
0975     if (ShellUtils::startInPresentation(serializedOptions)) {
0976         doc->startPresentation();
0977     }
0978     if (ShellUtils::showPrintDialog(serializedOptions)) {
0979         QMetaObject::invokeMethod(part, "enableStartWithPrint");
0980     }
0981     if (ShellUtils::showPrintDialogAndExit(serializedOptions)) {
0982         QMetaObject::invokeMethod(part, "enableExitAfterPrint");
0983     }
0984     if (!find.isEmpty()) {
0985         QMetaObject::invokeMethod(part, "enableStartWithFind", Q_ARG(QString, find));
0986     }
0987 }
0988 
0989 void Shell::connectPart(const KParts::ReadWritePart *part)
0990 {
0991     // We're abusing the fact we know the part is our part here
0992     connect(this, SIGNAL(moveSplitter(int)), part, SLOT(moveSplitter(int)));                     // clazy:exclude=old-style-connect
0993     connect(part, SIGNAL(enablePrintAction(bool)), this, SLOT(setPrintEnabled(bool)));           // clazy:exclude=old-style-connect
0994     connect(part, SIGNAL(enableCloseAction(bool)), this, SLOT(setCloseEnabled(bool)));           // clazy:exclude=old-style-connect
0995     connect(part, SIGNAL(mimeTypeChanged(QMimeType)), this, SLOT(setTabIcon(QMimeType)));        // clazy:exclude=old-style-connect
0996     connect(part, SIGNAL(urlsDropped(QList<QUrl>)), this, SLOT(handleDroppedUrls(QList<QUrl>))); // clazy:exclude=old-style-connect
0997     // clang-format off
0998     // Otherwise the QSize,QSize gets turned into QSize, QSize that is not normalized signals and is slightly slower
0999     connect(part, SIGNAL(fitWindowToPage(QSize,QSize)), this, SLOT(slotFitWindowToPage(QSize,QSize)));   // clazy:exclude=old-style-connect
1000     // clang-format on
1001 }
1002 
1003 void Shell::print()
1004 {
1005     QMetaObject::invokeMethod(m_tabs[m_tabWidget->currentIndex()].part, "slotPrint");
1006 }
1007 
1008 void Shell::setPrintEnabled(bool enabled)
1009 {
1010     int i = findTabIndex(sender());
1011     if (i != -1) {
1012         m_tabs[i].printEnabled = enabled;
1013         if (i == m_tabWidget->currentIndex()) {
1014             m_printAction->setEnabled(enabled);
1015         }
1016     }
1017 }
1018 
1019 void Shell::setCloseEnabled(bool enabled)
1020 {
1021     int i = findTabIndex(sender());
1022     if (i != -1) {
1023         m_tabs[i].closeEnabled = enabled;
1024         if (i == m_tabWidget->currentIndex()) {
1025             m_closeAction->setEnabled(enabled);
1026         }
1027     }
1028 }
1029 
1030 void Shell::activateNextTab()
1031 {
1032     if (m_tabs.size() < 2) {
1033         return;
1034     }
1035 
1036     const int activeTab = m_tabWidget->currentIndex();
1037     const int nextTab = (activeTab == m_tabs.size() - 1) ? 0 : activeTab + 1;
1038 
1039     setActiveTab(nextTab);
1040 }
1041 
1042 void Shell::activatePrevTab()
1043 {
1044     if (m_tabs.size() < 2) {
1045         return;
1046     }
1047 
1048     const int activeTab = m_tabWidget->currentIndex();
1049     const int prevTab = (activeTab == 0) ? m_tabs.size() - 1 : activeTab - 1;
1050 
1051     setActiveTab(prevTab);
1052 }
1053 
1054 void Shell::undoCloseTab()
1055 {
1056     if (m_closedTabUrls.isEmpty()) {
1057         return;
1058     }
1059 
1060     const QUrl lastTabUrl = m_closedTabUrls.takeLast();
1061 
1062     if (m_closedTabUrls.isEmpty()) {
1063         m_undoCloseTab->setEnabled(false);
1064     }
1065 
1066     openUrl(lastTabUrl);
1067 }
1068 
1069 void Shell::setTabIcon(const QMimeType &mimeType)
1070 {
1071     int i = findTabIndex(sender());
1072     if (i != -1) {
1073         m_tabWidget->setTabIcon(i, QIcon::fromTheme(mimeType.iconName()));
1074     }
1075 }
1076 
1077 int Shell::findTabIndex(QObject *sender) const
1078 {
1079     for (int i = 0; i < m_tabs.size(); ++i) {
1080         if (m_tabs[i].part == sender) {
1081             return i;
1082         }
1083     }
1084     return -1;
1085 }
1086 
1087 int Shell::findTabIndex(const QUrl &url) const
1088 {
1089     auto it = std::find_if(m_tabs.begin(), m_tabs.end(), [&url](const TabState state) { return state.part->url() == url; });
1090     return (it != m_tabs.end()) ? std::distance(m_tabs.begin(), it) : -1;
1091 }
1092 
1093 void Shell::handleDroppedUrls(const QList<QUrl> &urls)
1094 {
1095     for (const QUrl &url : urls) {
1096         openUrl(url);
1097     }
1098 }
1099 
1100 void Shell::moveTabData(int from, int to)
1101 {
1102     m_tabs.move(from, to);
1103 }
1104 
1105 void Shell::slotFitWindowToPage(const QSize pageViewSize, const QSize pageSize)
1106 {
1107     const int xOffset = pageViewSize.width() - pageSize.width();
1108     const int yOffset = pageViewSize.height() - pageSize.height();
1109     showNormal();
1110     resize(width() - xOffset, height() - yOffset);
1111     Q_EMIT moveSplitter(pageSize.width());
1112 }
1113 
1114 void Shell::hideWelcomeScreen()
1115 {
1116     m_sidebar->setVisible(m_showSidebarAction->isChecked());
1117     m_centralStackedWidget->setCurrentWidget(m_tabWidget);
1118     m_showSidebarAction->setEnabled(true);
1119 }
1120 
1121 void Shell::showWelcomeScreen()
1122 {
1123     m_showSidebarAction->setEnabled(false);
1124     m_centralStackedWidget->setCurrentWidget(m_welcomeScreen);
1125     m_sidebar->setVisible(false);
1126 
1127     refreshRecentsOnWelcomeScreen();
1128 }
1129 
1130 void Shell::refreshRecentsOnWelcomeScreen()
1131 {
1132     saveRecents();
1133     m_welcomeScreen->loadRecents();
1134 }
1135 
1136 void Shell::forgetRecentItem(QUrl const &url)
1137 {
1138     if (m_recent != nullptr) {
1139         m_recent->removeUrl(url);
1140         saveRecents();
1141         refreshRecentsOnWelcomeScreen();
1142     }
1143 }
1144 
1145 #include "shell.moc"
1146 
1147 /* kate: replace-tabs on; indent-width 4; */