File indexing completed on 2024-05-12 08:17:15

0001 /*
0002 Gwenview: an image viewer
0003 Copyright 2007 Aurélien Gâteau <agateau@kde.org>
0004 
0005 This program is free software; you can redistribute it and/or
0006 modify it under the terms of the GNU General Public License
0007 as published by the Free Software Foundation; either version 2
0008 of the License, or (at your option) any later version.
0009 
0010 This program is distributed in the hope that it will be useful,
0011 but WITHOUT ANY WARRANTY; without even the implied warranty of
0012 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013 GNU General Public License for more details.
0014 
0015 You should have received a copy of the GNU General Public License
0016 along with this program; if not, write to the Free Software
0017 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
0018 
0019 */
0020 #include "mainwindow.h"
0021 #include <config-gwenview.h>
0022 
0023 // Qt
0024 #include <QActionGroup>
0025 #include <QApplication>
0026 #include <QClipboard>
0027 #include <QDateTime>
0028 #include <QDialog>
0029 #include <QFileDialog>
0030 #include <QLineEdit>
0031 #include <QMenuBar>
0032 #include <QMouseEvent>
0033 #include <QPushButton>
0034 #include <QShortcut>
0035 #include <QSplitter>
0036 #include <QStackedWidget>
0037 #include <QTimer>
0038 #include <QUndoGroup>
0039 #include <QUrl>
0040 #include <QVBoxLayout>
0041 
0042 #ifdef Q_OS_OSX
0043 #include <QFileOpenEvent>
0044 #endif
0045 #include <QJsonArray>
0046 #include <QJsonObject>
0047 
0048 // KF
0049 #include <KActionCategory>
0050 #include <KActionCollection>
0051 #include <KDirLister>
0052 #include <KDirModel>
0053 #include <KFileItem>
0054 #include <KHamburgerMenu>
0055 #include <KLinkItemSelectionModel>
0056 #include <KLocalizedString>
0057 #include <KMessageBox>
0058 #include <KMessageWidget>
0059 #include <KProtocolManager>
0060 #include <KRecentFilesAction>
0061 #include <KStandardShortcut>
0062 #include <KToggleFullScreenAction>
0063 #include <KToolBar>
0064 #include <KToolBarPopupAction>
0065 #include <KUrlComboBox>
0066 #include <KUrlNavigator>
0067 #include <KXMLGUIFactory>
0068 #include <kwidgetsaddons_version.h>
0069 #include <kxmlgui_version.h>
0070 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0071 #include "lib/semanticinfo/semanticinfodirmodel.h"
0072 #endif
0073 #if HAVE_PURPOSE
0074 #include <Purpose/AlternativesModel>
0075 #include <Purpose/Menu>
0076 #include <purpose_version.h>
0077 #endif
0078 
0079 // Local
0080 #include "alignwithsidebarwidgetaction.h"
0081 #include "configdialog.h"
0082 #include "documentinfoprovider.h"
0083 #include "fileopscontextmanageritem.h"
0084 #include "folderviewcontextmanageritem.h"
0085 #include "fullscreencontent.h"
0086 #include "gvcore.h"
0087 #include "gwenview_app_debug.h"
0088 #include "imageopscontextmanageritem.h"
0089 #include "infocontextmanageritem.h"
0090 #include "viewmainpage.h"
0091 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0092 #include "semanticinfocontextmanageritem.h"
0093 #endif
0094 #include "browsemainpage.h"
0095 #include "preloader.h"
0096 #include "savebar.h"
0097 #include "sidebar.h"
0098 #include "splitter.h"
0099 #include "startmainpage.h"
0100 #include "thumbnailviewhelper.h"
0101 #include <lib/archiveutils.h>
0102 #include <lib/contextmanager.h>
0103 #include <lib/disabledactionshortcutmonitor.h>
0104 #include <lib/document/documentfactory.h>
0105 #include <lib/documentonlyproxymodel.h>
0106 #include <lib/gvdebug.h>
0107 #include <lib/gwenviewconfig.h>
0108 #include <lib/hud/hudbuttonbox.h>
0109 #include <lib/mimetypeutils.h>
0110 #ifdef HAVE_QTDBUS
0111 #include <QDBusConnection>
0112 #include <QDBusPendingReply>
0113 #include <QDBusReply>
0114 #include <lib/mpris2/mpris2service.h>
0115 #endif
0116 #include <lib/print/printhelper.h>
0117 #include <lib/semanticinfo/sorteddirmodel.h>
0118 #include <lib/signalblocker.h>
0119 #include <lib/slideshow.h>
0120 #include <lib/thumbnailprovider/thumbnailprovider.h>
0121 #include <lib/thumbnailview/thumbnailbarview.h>
0122 #include <lib/thumbnailview/thumbnailview.h>
0123 #include <lib/urlutils.h>
0124 
0125 namespace Gwenview
0126 {
0127 #undef ENABLE_LOG
0128 #undef LOG
0129 // #define ENABLE_LOG
0130 #ifdef ENABLE_LOG
0131 #define LOG(x) qCDebug(GWENVIEW_APP_LOG) << x
0132 #else
0133 #define LOG(x) ;
0134 #endif
0135 
0136 static const int BROWSE_PRELOAD_DELAY = 1000;
0137 static const int VIEW_PRELOAD_DELAY = 100;
0138 
0139 static const char *SESSION_CURRENT_PAGE_KEY = "Page";
0140 static const char *SESSION_URL_KEY = "Url";
0141 
0142 enum MainPageId {
0143     StartMainPageId,
0144     BrowseMainPageId,
0145     ViewMainPageId,
0146 };
0147 
0148 struct MainWindowState {
0149     bool mToolBarVisible;
0150 };
0151 
0152 /*
0153 
0154 Layout of the main window looks like this:
0155 
0156 .-mCentralSplitter-----------------------------.
0157 |.-mSideBar--. .-mContentWidget---------------.|
0158 ||           | |.-mSaveBar-------------------.||
0159 ||           | ||                            |||
0160 ||           | |'----------------------------'||
0161 ||           | |.-mViewStackedWidget---------.||
0162 ||           | ||                            |||
0163 ||           | ||                            |||
0164 ||           | ||                            |||
0165 ||           | ||                            |||
0166 ||           | |'----------------------------'||
0167 |'-----------' '------------------------------'|
0168 '----------------------------------------------'
0169 
0170 */
0171 struct MainWindow::Private {
0172     GvCore *mGvCore = nullptr;
0173     MainWindow *q = nullptr;
0174     QSplitter *mCentralSplitter = nullptr;
0175     QWidget *mContentWidget = nullptr;
0176     ViewMainPage *mViewMainPage = nullptr;
0177     KUrlNavigator *mUrlNavigator = nullptr;
0178     ThumbnailView *mThumbnailView = nullptr;
0179     ThumbnailView *mActiveThumbnailView = nullptr;
0180     DocumentInfoProvider *mDocumentInfoProvider = nullptr;
0181     ThumbnailViewHelper *mThumbnailViewHelper = nullptr;
0182     QPointer<ThumbnailProvider> mThumbnailProvider;
0183     BrowseMainPage *mBrowseMainPage = nullptr;
0184     StartMainPage *mStartMainPage = nullptr;
0185     SideBar *mSideBar = nullptr;
0186     KMessageWidget *mSharedMessage = nullptr;
0187     QStackedWidget *mViewStackedWidget = nullptr;
0188     FullScreenContent *mFullScreenContent = nullptr;
0189     SaveBar *mSaveBar = nullptr;
0190     bool mStartSlideShowWhenDirListerCompleted;
0191     SlideShow *mSlideShow = nullptr;
0192 #ifdef HAVE_QTDBUS
0193     Mpris2Service *mMpris2Service = nullptr;
0194 #endif
0195     Preloader *mPreloader = nullptr;
0196     bool mPreloadDirectionIsForward;
0197 
0198     QActionGroup *mViewModeActionGroup = nullptr;
0199     KRecentFilesAction *mFileOpenRecentAction = nullptr;
0200     QAction *mBrowseAction = nullptr;
0201     QAction *mViewAction = nullptr;
0202     QAction *mGoUpAction = nullptr;
0203     QAction *mGoToPreviousAction = nullptr;
0204     QAction *mGoToNextAction = nullptr;
0205     QAction *mGoToFirstAction = nullptr;
0206     QAction *mGoToLastAction = nullptr;
0207     KToggleAction *mToggleSideBarAction = nullptr;
0208     KToggleAction *mToggleOperationsSideBarAction = nullptr;
0209     QAction *mFullScreenAction = nullptr;
0210     QAction *mToggleSlideShowAction = nullptr;
0211     KToggleAction *mShowMenuBarAction = nullptr;
0212     KToggleAction *mShowStatusBarAction = nullptr;
0213     QPointer<HudButtonBox> hudButtonBox = nullptr;
0214 #if HAVE_PURPOSE
0215     Purpose::Menu *mShareMenu = nullptr;
0216     KToolBarPopupAction *mShareAction = nullptr;
0217 #endif
0218     KHamburgerMenu *mHamburgerMenu = nullptr;
0219 
0220     SortedDirModel *mDirModel = nullptr;
0221     DocumentOnlyProxyModel *mThumbnailBarModel = nullptr;
0222     KLinkItemSelectionModel *mThumbnailBarSelectionModel = nullptr;
0223     ContextManager *mContextManager = nullptr;
0224 
0225     MainWindowState mStateBeforeFullScreen;
0226 
0227     QString mCaption;
0228 
0229     MainPageId mCurrentMainPageId;
0230 
0231     QDateTime mFullScreenLeftAt;
0232     uint screenSaverDbusCookie = 0;
0233 
0234     void setupContextManager()
0235     {
0236         mContextManager = new ContextManager(mDirModel, q);
0237         connect(mContextManager, &ContextManager::selectionChanged, q, &MainWindow::slotSelectionChanged);
0238         connect(mContextManager, &ContextManager::currentDirUrlChanged, q, &MainWindow::slotCurrentDirUrlChanged);
0239     }
0240 
0241     void setupWidgets()
0242     {
0243         mFullScreenContent = new FullScreenContent(q, mGvCore);
0244         connect(mContextManager, &ContextManager::currentUrlChanged, mFullScreenContent, &FullScreenContent::setCurrentUrl);
0245 
0246         mCentralSplitter = new Splitter(Qt::Horizontal, q);
0247         q->setCentralWidget(mCentralSplitter);
0248 
0249         // Left side of splitter
0250         mSideBar = new SideBar(mCentralSplitter);
0251 
0252         // Right side of splitter
0253         mContentWidget = new QWidget(mCentralSplitter);
0254 
0255         mSharedMessage = new KMessageWidget(mContentWidget);
0256         mSharedMessage->setVisible(false);
0257 
0258         mSaveBar = new SaveBar(mContentWidget, q->actionCollection());
0259         connect(mContextManager, &ContextManager::currentUrlChanged, mSaveBar, &SaveBar::setCurrentUrl);
0260         mViewStackedWidget = new QStackedWidget(mContentWidget);
0261         auto layout = new QVBoxLayout(mContentWidget);
0262         layout->addWidget(mSharedMessage);
0263         layout->addWidget(mSaveBar);
0264         layout->addWidget(mViewStackedWidget);
0265         layout->setContentsMargins(0, 0, 0, 0);
0266         layout->setSpacing(0);
0267         ////
0268 
0269         mStartSlideShowWhenDirListerCompleted = false;
0270         mSlideShow = new SlideShow(q);
0271         connect(mContextManager, &ContextManager::currentUrlChanged, mSlideShow, &SlideShow::setCurrentUrl);
0272 
0273         setupThumbnailView(mViewStackedWidget);
0274         setupViewMainPage(mViewStackedWidget);
0275         setupStartMainPage(mViewStackedWidget);
0276         mViewStackedWidget->addWidget(mBrowseMainPage);
0277         mViewStackedWidget->addWidget(mViewMainPage);
0278         mViewStackedWidget->addWidget(mStartMainPage);
0279         mViewStackedWidget->setCurrentWidget(mBrowseMainPage);
0280 
0281         mCentralSplitter->setStretchFactor(0, 0);
0282         mCentralSplitter->setStretchFactor(1, 1);
0283         mCentralSplitter->setChildrenCollapsible(false);
0284 
0285         mThumbnailView->setFocus();
0286 
0287         connect(mSaveBar, &SaveBar::requestSaveAll, mGvCore, &GvCore::saveAll);
0288         connect(mSaveBar, &SaveBar::goToUrl, q, &MainWindow::goToUrl);
0289 
0290         connect(mSlideShow, &SlideShow::goToUrl, q, &MainWindow::goToUrl);
0291     }
0292 
0293     void setupThumbnailView(QWidget *parent)
0294     {
0295         Q_ASSERT(mContextManager);
0296         mBrowseMainPage = new BrowseMainPage(parent, q->actionCollection(), mGvCore);
0297 
0298         mThumbnailView = mBrowseMainPage->thumbnailView();
0299         mThumbnailView->viewport()->installEventFilter(q);
0300         mThumbnailView->setSelectionModel(mContextManager->selectionModel());
0301         mUrlNavigator = mBrowseMainPage->urlNavigator();
0302 
0303         mDocumentInfoProvider = new DocumentInfoProvider(mDirModel);
0304         mThumbnailView->setDocumentInfoProvider(mDocumentInfoProvider);
0305 
0306         mThumbnailViewHelper = new ThumbnailViewHelper(mDirModel, q->actionCollection());
0307         connect(mContextManager, &ContextManager::currentDirUrlChanged, mThumbnailViewHelper, &ThumbnailViewHelper::setCurrentDirUrl);
0308         mThumbnailView->setThumbnailViewHelper(mThumbnailViewHelper);
0309 
0310         mThumbnailBarSelectionModel = new KLinkItemSelectionModel(mThumbnailBarModel, mContextManager->selectionModel(), q);
0311 
0312         // Connect thumbnail view
0313         connect(mThumbnailView, &ThumbnailView::indexActivated, q, &MainWindow::slotThumbnailViewIndexActivated);
0314 
0315         // Connect delegate
0316         QAbstractItemDelegate *delegate = mThumbnailView->itemDelegate();
0317         connect(delegate, SIGNAL(saveDocumentRequested(QUrl)), mGvCore, SLOT(save(QUrl)));
0318         connect(delegate, SIGNAL(rotateDocumentLeftRequested(QUrl)), mGvCore, SLOT(rotateLeft(QUrl)));
0319         connect(delegate, SIGNAL(rotateDocumentRightRequested(QUrl)), mGvCore, SLOT(rotateRight(QUrl)));
0320         connect(delegate, SIGNAL(showDocumentInFullScreenRequested(QUrl)), q, SLOT(showDocumentInFullScreen(QUrl)));
0321         connect(delegate, SIGNAL(setDocumentRatingRequested(QUrl, int)), mGvCore, SLOT(setRating(QUrl, int)));
0322 
0323         // Connect url navigator
0324         connect(mUrlNavigator, &KUrlNavigator::urlChanged, q, &MainWindow::openDirUrl);
0325     }
0326 
0327     void setupViewMainPage(QWidget *parent)
0328     {
0329         mViewMainPage = new ViewMainPage(parent, mSlideShow, q->actionCollection(), mGvCore);
0330         connect(mViewMainPage, &ViewMainPage::captionUpdateRequested, q, &MainWindow::slotUpdateCaption);
0331         connect(mViewMainPage, &ViewMainPage::completed, q, &MainWindow::slotPartCompleted);
0332         connect(mViewMainPage, &ViewMainPage::previousImageRequested, q, &MainWindow::goToPrevious);
0333         connect(mViewMainPage, &ViewMainPage::nextImageRequested, q, &MainWindow::goToNext);
0334         connect(mViewMainPage, &ViewMainPage::openUrlRequested, q, &MainWindow::openUrl);
0335         connect(mViewMainPage, &ViewMainPage::openDirUrlRequested, q, &MainWindow::openDirUrl);
0336 
0337         setupThumbnailBar(mViewMainPage->thumbnailBar());
0338     }
0339 
0340     void setupThumbnailBar(ThumbnailView *bar)
0341     {
0342         Q_ASSERT(mThumbnailBarModel);
0343         Q_ASSERT(mThumbnailBarSelectionModel);
0344         Q_ASSERT(mDocumentInfoProvider);
0345         Q_ASSERT(mThumbnailViewHelper);
0346         bar->setModel(mThumbnailBarModel);
0347         bar->setSelectionModel(mThumbnailBarSelectionModel);
0348         bar->setDocumentInfoProvider(mDocumentInfoProvider);
0349         bar->setThumbnailViewHelper(mThumbnailViewHelper);
0350     }
0351 
0352     void setupStartMainPage(QWidget *parent)
0353     {
0354         mStartMainPage = new StartMainPage(parent, mGvCore);
0355         connect(mStartMainPage, &StartMainPage::urlSelected, q, &MainWindow::slotStartMainPageUrlSelected);
0356         connect(mStartMainPage, &StartMainPage::recentFileRemoved, [this](const QUrl &url) {
0357             mFileOpenRecentAction->removeUrl(url);
0358         });
0359         connect(mStartMainPage, &StartMainPage::recentFilesCleared, [this]() {
0360             mFileOpenRecentAction->clear();
0361         });
0362     }
0363 
0364     void installDisabledActionShortcutMonitor(QAction *action, const char *slot)
0365     {
0366         auto monitor = new DisabledActionShortcutMonitor(action, q);
0367         connect(monitor, SIGNAL(activated()), q, slot);
0368     }
0369 
0370     void setupActions()
0371     {
0372         KActionCollection *actionCollection = q->actionCollection();
0373         auto file = new KActionCategory(i18nc("@title actions category", "File"), actionCollection);
0374         auto view = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "View"), actionCollection);
0375 
0376         file->addAction(KStandardAction::Save, q, SLOT(saveCurrent()));
0377         file->addAction(KStandardAction::SaveAs, q, SLOT(saveCurrentAs()));
0378         file->addAction(KStandardAction::Open, q, SLOT(openFile()));
0379         mFileOpenRecentAction = KStandardAction::openRecent(q, SLOT(openUrl(QUrl)), q);
0380         connect(mFileOpenRecentAction, &KRecentFilesAction::recentListCleared, mGvCore, &GvCore::clearRecentFilesAndFolders);
0381         auto clearAction = mFileOpenRecentAction->menu()->findChild<QAction *>("clear_action");
0382         if (clearAction) {
0383             clearAction->setText(i18nc("@action Open Recent menu", "Clear List"));
0384         }
0385         file->addAction("file_open_recent", mFileOpenRecentAction);
0386         file->addAction(KStandardAction::Print, q, SLOT(print()));
0387         file->addAction(KStandardAction::PrintPreview, q, SLOT(printPreview()));
0388         file->addAction(KStandardAction::Quit, qApp, SLOT(closeAllWindows()));
0389 
0390         QAction *action = file->addAction(QStringLiteral("reload"), q, SLOT(reload()));
0391         action->setText(i18nc("@action reload the currently viewed image", "Reload"));
0392         action->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
0393         actionCollection->setDefaultShortcuts(action, KStandardShortcut::reload());
0394 
0395         QAction *replaceLocationAction = actionCollection->addAction(QStringLiteral("replace_location"));
0396         replaceLocationAction->setText(i18nc("@action:inmenu Navigation Bar", "Replace Location"));
0397         actionCollection->setDefaultShortcut(replaceLocationAction, Qt::CTRL | Qt::Key_L);
0398         connect(replaceLocationAction, &QAction::triggered, q, &MainWindow::replaceLocation);
0399 
0400         mBrowseAction = view->addAction(QStringLiteral("browse"));
0401         mBrowseAction->setText(i18nc("@action:intoolbar Switch to file list", "Browse"));
0402         mBrowseAction->setToolTip(i18nc("@info:tooltip", "Browse folders for images"));
0403         mBrowseAction->setCheckable(true);
0404         mBrowseAction->setIcon(QIcon::fromTheme(QStringLiteral("view-list-icons")));
0405         actionCollection->setDefaultShortcut(mBrowseAction, Qt::Key_Escape);
0406         connect(mViewMainPage, &ViewMainPage::goToBrowseModeRequested, mBrowseAction, &QAction::trigger);
0407 
0408         mViewAction = view->addAction(QStringLiteral("view"));
0409         mViewAction->setText(i18nc("@action:intoolbar Switch to image view", "View"));
0410         mViewAction->setToolTip(i18nc("@info:tooltip", "View selected images"));
0411         mViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-preview")));
0412         mViewAction->setCheckable(true);
0413 
0414         mViewModeActionGroup = new QActionGroup(q);
0415         mViewModeActionGroup->addAction(mBrowseAction);
0416         mViewModeActionGroup->addAction(mViewAction);
0417 
0418         connect(mViewModeActionGroup, &QActionGroup::triggered, q, &MainWindow::setActiveViewModeAction);
0419 
0420         mFullScreenAction = KStandardAction::fullScreen(q, &MainWindow::toggleFullScreen, q, actionCollection);
0421         QList<QKeySequence> shortcuts = mFullScreenAction->shortcuts();
0422         shortcuts.append(QKeySequence(Qt::Key_F11));
0423         actionCollection->setDefaultShortcuts(mFullScreenAction, shortcuts);
0424 
0425         connect(mViewMainPage, &ViewMainPage::toggleFullScreenRequested, mFullScreenAction, &QAction::trigger);
0426 
0427         QAction *leaveFullScreenAction = view->addAction("leave_fullscreen", q, SLOT(leaveFullScreen()));
0428         leaveFullScreenAction->setIcon(QIcon::fromTheme(QStringLiteral("view-restore")));
0429         leaveFullScreenAction->setText(i18nc("@action", "Exit Full Screen"));
0430 
0431         mGoToPreviousAction = view->addAction("go_previous", q, SLOT(goToPrevious()));
0432         mGoToPreviousAction->setPriority(QAction::LowPriority);
0433         mGoToPreviousAction->setIcon(QIcon::fromTheme(QGuiApplication::layoutDirection() == Qt::LeftToRight ? "go-previous" : "go-previous-symbolic-rtl"));
0434         mGoToPreviousAction->setText(i18nc("@action Go to previous image", "Previous"));
0435         mGoToPreviousAction->setToolTip(i18nc("@info:tooltip", "Go to previous image"));
0436         installDisabledActionShortcutMonitor(mGoToPreviousAction, SLOT(showFirstDocumentReached()));
0437 
0438         mGoToNextAction = view->addAction(QStringLiteral("go_next"), q, SLOT(goToNext()));
0439         mGoToNextAction->setPriority(QAction::LowPriority);
0440         mGoToNextAction->setIcon(QIcon::fromTheme(QGuiApplication::layoutDirection() == Qt::LeftToRight ? "go-next" : "go-next-symbolic-rtl"));
0441         mGoToNextAction->setText(i18nc("@action Go to next image", "Next"));
0442         mGoToNextAction->setToolTip(i18nc("@info:tooltip", "Go to next image"));
0443         installDisabledActionShortcutMonitor(mGoToNextAction, SLOT(showLastDocumentReached()));
0444 
0445         mGoToFirstAction = view->addAction(QStringLiteral("go_first"), q, SLOT(goToFirst()));
0446         mGoToFirstAction->setPriority(QAction::LowPriority);
0447         mGoToFirstAction->setIcon(QIcon::fromTheme(QStringLiteral("go-first-view")));
0448         mGoToFirstAction->setText(i18nc("@action Go to first image", "First"));
0449         mGoToFirstAction->setToolTip(i18nc("@info:tooltip", "Go to first image"));
0450         actionCollection->setDefaultShortcut(mGoToFirstAction, Qt::Key_Home);
0451 
0452         mGoToLastAction = view->addAction(QStringLiteral("go_last"), q, SLOT(goToLast()));
0453         mGoToLastAction->setPriority(QAction::LowPriority);
0454         mGoToLastAction->setIcon(QIcon::fromTheme(QStringLiteral("go-last-view")));
0455         mGoToLastAction->setText(i18nc("@action Go to last image", "Last"));
0456         mGoToLastAction->setToolTip(i18nc("@info:tooltip", "Go to last image"));
0457         actionCollection->setDefaultShortcut(mGoToLastAction, Qt::Key_End);
0458 
0459         mPreloadDirectionIsForward = true;
0460 
0461         mGoUpAction = view->addAction(KStandardAction::Up, q, SLOT(goUp()));
0462 
0463         action = view->addAction("go_start_page", q, SLOT(showStartMainPage()));
0464         action->setPriority(QAction::LowPriority);
0465         action->setIcon(QIcon::fromTheme(QStringLiteral("go-home")));
0466         action->setText(i18nc("@action", "Start Page"));
0467         action->setToolTip(i18nc("@info:tooltip", "Open the start page"));
0468         actionCollection->setDefaultShortcuts(action, KStandardShortcut::home());
0469 
0470         mToggleSideBarAction = view->add<KToggleAction>(QStringLiteral("toggle_sidebar"));
0471         connect(mToggleSideBarAction, &KToggleAction::triggered, q, &MainWindow::toggleSideBar);
0472         mToggleSideBarAction->setIcon(QIcon::fromTheme(QStringLiteral("view-sidetree")));
0473         actionCollection->setDefaultShortcut(mToggleSideBarAction, Qt::Key_F4);
0474         mToggleSideBarAction->setText(i18nc("@action", "Sidebar"));
0475         connect(mBrowseMainPage->toggleSideBarButton(), &QAbstractButton::clicked, mToggleSideBarAction, &QAction::trigger);
0476         connect(mViewMainPage->toggleSideBarButton(), &QAbstractButton::clicked, mToggleSideBarAction, &QAction::trigger);
0477 
0478         mToggleOperationsSideBarAction = view->add<KToggleAction>(QStringLiteral("toggle_operations_sidebar"));
0479         mToggleOperationsSideBarAction->setText(i18nc("@action opens crop, rename, etc.", "Show Editing Tools"));
0480         mToggleOperationsSideBarAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-image"), QIcon::fromTheme(QStringLiteral("document-edit"))));
0481         connect(mToggleOperationsSideBarAction, &KToggleAction::triggered, q, &MainWindow::toggleOperationsSideBar);
0482         connect(mSideBar, &QTabWidget::currentChanged, mToggleOperationsSideBarAction, [=]() {
0483             mToggleOperationsSideBarAction->setChecked(mSideBar->isVisible() && mSideBar->currentPage() == QLatin1String("operations"));
0484         });
0485 
0486         mToggleSlideShowAction = view->addAction(QStringLiteral("toggle_slideshow"), q, SLOT(toggleSlideShow()));
0487         q->updateSlideShowAction();
0488         connect(mSlideShow, &SlideShow::stateChanged, q, &MainWindow::updateSlideShowAction);
0489 
0490         q->setStandardToolBarMenuEnabled(true);
0491 
0492         mShowMenuBarAction = static_cast<KToggleAction *>(view->addAction(KStandardAction::ShowMenubar, q, SLOT(toggleMenuBar())));
0493         mShowStatusBarAction = static_cast<KToggleAction *>(view->addAction(KStandardAction::ShowStatusbar, q, SLOT(toggleStatusBar(bool))));
0494 
0495         actionCollection->setDefaultShortcut(mShowStatusBarAction, Qt::Key_F3);
0496 
0497         view->addAction(KStandardAction::name(KStandardAction::KeyBindings),
0498                         KStandardAction::keyBindings(q, &MainWindow::configureShortcuts, actionCollection));
0499 
0500         connect(q->guiFactory(), &KXMLGUIFactory::shortcutsSaved, q, [this]() {
0501             q->guiFactory()->refreshActionProperties();
0502         });
0503 
0504         view->addAction(KStandardAction::Preferences, q, SLOT(showConfigDialog()));
0505 
0506         view->addAction(KStandardAction::ConfigureToolbars, q, SLOT(configureToolbars()));
0507 
0508 #if HAVE_PURPOSE
0509         mShareAction = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("document-share")), i18nc("@action Share images", "Share"), q);
0510 
0511         mShareAction->setPopupMode(KToolBarPopupAction::InstantPopup);
0512         actionCollection->addAction(QStringLiteral("share"), mShareAction);
0513         mShareMenu = new Purpose::Menu(q);
0514         mShareAction->setMenu(mShareMenu);
0515 
0516         connect(mShareMenu, &Purpose::Menu::finished, q, [this](const QJsonObject &output, int error, const QString &message) {
0517             if (error && error != KIO::ERR_USER_CANCELED) {
0518                 mSharedMessage->setText(i18n("Error while sharing: %1", message));
0519                 mSharedMessage->setMessageType(KMessageWidget::MessageType::Error);
0520                 mSharedMessage->animatedShow();
0521             } else {
0522                 const QString url = output[QStringLiteral("url")].toString();
0523 
0524                 if (!url.isEmpty()) {
0525                     mSharedMessage->setText(i18n("The shared image link (<a href=\"%1\">%1</a>) has been copied to the clipboard.", url));
0526                     mSharedMessage->setMessageType(KMessageWidget::MessageType::Positive);
0527                     mSharedMessage->animatedShow();
0528                     QApplication::clipboard()->setText(url);
0529                 } else {
0530                     mSharedMessage->setVisible(false);
0531                 }
0532             }
0533         });
0534 #endif
0535 
0536         auto alignWithSideBarWidgetAction = new AlignWithSideBarWidgetAction(q);
0537         alignWithSideBarWidgetAction->setSideBar(mSideBar);
0538         actionCollection->addAction("align_with_sidebar", alignWithSideBarWidgetAction);
0539 
0540         mHamburgerMenu = KStandardAction::hamburgerMenu(nullptr, nullptr, actionCollection);
0541         mHamburgerMenu->setShowMenuBarAction(mShowMenuBarAction);
0542         mHamburgerMenu->setMenuBar(q->menuBar());
0543         connect(mHamburgerMenu, &KHamburgerMenu::aboutToShowMenu, q, [this]() {
0544             this->updateHamburgerMenu();
0545             // Immediately disconnect. We only need to run this once, but on demand.
0546             // NOTE: The nullptr at the end disconnects all connections between
0547             // q and mHamburgerMenu's aboutToShowMenu signal.
0548             disconnect(mHamburgerMenu, &KHamburgerMenu::aboutToShowMenu, q, nullptr);
0549         });
0550     }
0551 
0552     void updateHamburgerMenu()
0553     {
0554         KActionCollection *actionCollection = q->actionCollection();
0555         auto menu = new QMenu;
0556         menu->addAction(actionCollection->action(KStandardAction::name(KStandardAction::Open)));
0557         menu->addAction(actionCollection->action(KStandardAction::name(KStandardAction::OpenRecent)));
0558         menu->addAction(actionCollection->action(KStandardAction::name(KStandardAction::Save)));
0559         menu->addAction(actionCollection->action(KStandardAction::name(KStandardAction::SaveAs)));
0560         menu->addAction(actionCollection->action(KStandardAction::name(KStandardAction::Print)));
0561         menu->addAction(actionCollection->action(KStandardAction::name(KStandardAction::PrintPreview)));
0562         menu->addSeparator();
0563         menu->addAction(actionCollection->action(KStandardAction::name(KStandardAction::Copy)));
0564         menu->addAction(actionCollection->action(QStringLiteral("file_trash")));
0565         menu->addSeparator();
0566         menu->addAction(mBrowseAction);
0567         menu->addAction(mViewAction);
0568         menu->addAction(actionCollection->action(QStringLiteral("sort_by")));
0569         menu->addAction(mFullScreenAction);
0570         menu->addAction(mToggleSlideShowAction);
0571         menu->addSeparator();
0572 #if HAVE_PURPOSE
0573         menu->addMenu(mShareMenu);
0574 #endif
0575         auto configureMenu = new QMenu(i18nc("@title:menu submenu for actions that open configuration dialogs", "Configure"));
0576         configureMenu->addAction(actionCollection->action(QStringLiteral("options_configure_keybinding")));
0577         configureMenu->addAction(actionCollection->action(QStringLiteral("options_configure_toolbars")));
0578         configureMenu->addAction(actionCollection->action(QStringLiteral("options_configure")));
0579         menu->addMenu(configureMenu);
0580         mHamburgerMenu->setMenu(menu);
0581     }
0582 
0583     void setupUndoActions()
0584     {
0585         // There is no KUndoGroup similar to KUndoStack. This code basically
0586         // does the same as KUndoStack, but for the KUndoGroup actions.
0587         QUndoGroup *undoGroup = DocumentFactory::instance()->undoGroup();
0588         QAction *action;
0589         KActionCollection *actionCollection = q->actionCollection();
0590         auto edit = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "Edit"), actionCollection);
0591 
0592         action = undoGroup->createRedoAction(actionCollection);
0593         action->setObjectName(KStandardAction::name(KStandardAction::Redo));
0594         action->setIcon(QIcon::fromTheme(QStringLiteral("edit-redo")));
0595         action->setIconText(i18n("Redo"));
0596         actionCollection->setDefaultShortcuts(action, KStandardShortcut::redo());
0597 
0598         edit->addAction(action->objectName(), action);
0599 
0600         action = undoGroup->createUndoAction(actionCollection);
0601         action->setObjectName(KStandardAction::name(KStandardAction::Undo));
0602         action->setIcon(QIcon::fromTheme(QStringLiteral("edit-undo")));
0603         action->setIconText(i18n("Undo"));
0604         actionCollection->setDefaultShortcuts(action, KStandardShortcut::undo());
0605         edit->addAction(action->objectName(), action);
0606     }
0607 
0608     void setupContextManagerItems()
0609     {
0610         Q_ASSERT(mContextManager);
0611         KActionCollection *actionCollection = q->actionCollection();
0612 
0613         // Create context manager items
0614         auto folderViewItem = new FolderViewContextManagerItem(mContextManager);
0615         connect(folderViewItem, &FolderViewContextManagerItem::urlChanged, q, &MainWindow::folderViewUrlChanged);
0616 
0617         auto infoItem = new InfoContextManagerItem(mContextManager);
0618 
0619 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0620         SemanticInfoContextManagerItem *semanticInfoItem = nullptr;
0621         semanticInfoItem = new SemanticInfoContextManagerItem(mContextManager, actionCollection, mViewMainPage);
0622 #endif
0623 
0624         auto imageOpsItem = new ImageOpsContextManagerItem(mContextManager, q);
0625 
0626         auto fileOpsItem = new FileOpsContextManagerItem(mContextManager, mThumbnailView, actionCollection, q);
0627 
0628         // Fill sidebar
0629         SideBarPage *page;
0630         page = new SideBarPage(QIcon::fromTheme(QStringLiteral("folder")), i18n("Folders"));
0631         page->setObjectName(QStringLiteral("folders"));
0632         page->addWidget(folderViewItem->widget());
0633         page->layout()->setContentsMargins(0, 0, 0, 0);
0634         mSideBar->addPage(page);
0635 
0636         page = new SideBarPage(QIcon::fromTheme(QStringLiteral("documentinfo")), i18n("Information"));
0637         page->setObjectName(QStringLiteral("information"));
0638 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0639         // If we have semantic info, we want to share the sidebar using a splitter,
0640         // so the user can dynamically resize the two widgets.
0641         if (semanticInfoItem) {
0642             auto splitter = new QSplitter;
0643             splitter->setObjectName(QStringLiteral("information_splitter")); // This name is used to find it when loading previous sizes.
0644             splitter->setOrientation(Qt::Vertical);
0645             splitter->setHandleWidth(5);
0646 
0647             splitter->addWidget(infoItem->widget());
0648             splitter->setCollapsible(0, false);
0649 
0650             // Cram the semantic info widget and a separator into a separate widget,
0651             // so that they can be added to the splitter together (layouts can't be added directly).
0652             // This will give the splitter a visible separator between the two widgets.
0653             auto separator = new QFrame;
0654             separator->setFrameShape(QFrame::HLine);
0655             separator->setLineWidth(1);
0656 
0657             auto container = new QWidget;
0658             auto containerLayout = new QVBoxLayout(container);
0659             containerLayout->setContentsMargins(0, 0, 0, 0);
0660             containerLayout->addWidget(separator);
0661             containerLayout->addWidget(semanticInfoItem->widget());
0662 
0663             splitter->addWidget(container);
0664             splitter->setCollapsible(1, false);
0665 
0666             page->addWidget(splitter);
0667         } else {
0668             page->addWidget(infoItem->widget());
0669         }
0670 #else
0671         page->addWidget(infoItem->widget());
0672 #endif
0673         mSideBar->addPage(page);
0674 
0675         page = new SideBarPage(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Operations"));
0676         page->setObjectName(QStringLiteral("operations"));
0677         page->addWidget(imageOpsItem->widget());
0678         auto separator = new QFrame;
0679         separator->setFrameShape(QFrame::HLine);
0680         separator->setLineWidth(1);
0681         page->addWidget(separator);
0682         page->addWidget(fileOpsItem->widget());
0683         page->addStretch();
0684         mSideBar->addPage(page);
0685     }
0686 
0687     void initDirModel()
0688     {
0689         mDirModel->setKindFilter(MimeTypeUtils::KIND_DIR | MimeTypeUtils::KIND_ARCHIVE | MimeTypeUtils::KIND_RASTER_IMAGE | MimeTypeUtils::KIND_SVG_IMAGE
0690                                  | MimeTypeUtils::KIND_VIDEO);
0691 
0692         connect(mDirModel, &QAbstractItemModel::rowsInserted, q, &MainWindow::slotDirModelNewItems);
0693 
0694         connect(mDirModel, &QAbstractItemModel::rowsRemoved, q, &MainWindow::updatePreviousNextActions);
0695         connect(mDirModel, &QAbstractItemModel::modelReset, q, &MainWindow::updatePreviousNextActions);
0696 
0697         connect(mDirModel->dirLister(), SIGNAL(completed()), q, SLOT(slotDirListerCompleted()));
0698     }
0699 
0700     void setupThumbnailBarModel()
0701     {
0702         mThumbnailBarModel = new DocumentOnlyProxyModel(q);
0703         mThumbnailBarModel->setSourceModel(mDirModel);
0704     }
0705 
0706     bool indexIsDirOrArchive(const QModelIndex &index) const
0707     {
0708         Q_ASSERT(index.isValid());
0709         KFileItem item = mDirModel->itemForIndex(index);
0710         return ArchiveUtils::fileItemIsDirOrArchive(item);
0711     }
0712 
0713     void goTo(const QModelIndex &index)
0714     {
0715         if (!index.isValid()) {
0716             return;
0717         }
0718         mThumbnailView->setCurrentIndex(index);
0719         mThumbnailView->scrollTo(index);
0720     }
0721 
0722     void goTo(int offset)
0723     {
0724         mPreloadDirectionIsForward = offset > 0;
0725         QModelIndex index = mContextManager->selectionModel()->currentIndex();
0726         index = mDirModel->index(index.row() + offset, 0);
0727         if (index.isValid() && !indexIsDirOrArchive(index)) {
0728             goTo(index);
0729         }
0730     }
0731 
0732     void goToFirstDocument()
0733     {
0734         QModelIndex index;
0735         for (int row = 0;; ++row) {
0736             index = mDirModel->index(row, 0);
0737             if (!index.isValid()) {
0738                 return;
0739             }
0740 
0741             if (!indexIsDirOrArchive(index)) {
0742                 break;
0743             }
0744         }
0745         goTo(index);
0746     }
0747 
0748     void goToLastDocument()
0749     {
0750         QModelIndex index = mDirModel->index(mDirModel->rowCount() - 1, 0);
0751         goTo(index);
0752     }
0753 
0754     void setupFullScreenContent()
0755     {
0756         mFullScreenContent->init(q->actionCollection(), mViewMainPage, mSlideShow);
0757         setupThumbnailBar(mFullScreenContent->thumbnailBar());
0758     }
0759 
0760     inline void setActionEnabled(const char *name, bool enabled)
0761     {
0762         QAction *action = q->actionCollection()->action(name);
0763         if (action) {
0764             action->setEnabled(enabled);
0765         } else {
0766             qCWarning(GWENVIEW_APP_LOG) << "Action" << name << "not found";
0767         }
0768     }
0769 
0770     void setActionsDisabledOnStartMainPageEnabled(bool enabled)
0771     {
0772         mBrowseAction->setEnabled(enabled);
0773         mViewAction->setEnabled(enabled);
0774         mToggleSideBarAction->setEnabled(enabled);
0775         mToggleOperationsSideBarAction->setEnabled(enabled);
0776         mShowStatusBarAction->setEnabled(enabled);
0777         mFullScreenAction->setEnabled(enabled);
0778         mToggleSlideShowAction->setEnabled(enabled);
0779 
0780         setActionEnabled("reload", enabled);
0781         setActionEnabled("go_start_page", enabled);
0782         setActionEnabled("add_folder_to_places", enabled);
0783     }
0784 
0785     void updateActions()
0786     {
0787         bool isRasterImage = false;
0788         bool canSave = false;
0789         bool isModified = false;
0790         const QUrl url = mContextManager->currentUrl();
0791 
0792         if (url.isValid()) {
0793             isRasterImage = mContextManager->currentUrlIsRasterImage();
0794             canSave = isRasterImage;
0795             isModified = DocumentFactory::instance()->load(url)->isModified();
0796             if (mCurrentMainPageId != ViewMainPageId && mContextManager->selectedFileItemList().count() != 1) {
0797                 // Saving only makes sense if exactly one image is selected
0798                 canSave = false;
0799             }
0800         }
0801 
0802         KActionCollection *actionCollection = q->actionCollection();
0803         actionCollection->action(QStringLiteral("file_save"))->setEnabled(canSave && isModified);
0804         actionCollection->action(QStringLiteral("file_save_as"))->setEnabled(canSave);
0805         actionCollection->action(QStringLiteral("file_print"))->setEnabled(isRasterImage);
0806         actionCollection->action(QStringLiteral("file_print_preview"))->setEnabled(isRasterImage);
0807 
0808 #if HAVE_PURPOSE
0809         const KFileItemList selectedFiles = mContextManager->selectedFileItemList();
0810 
0811         if (selectedFiles.isEmpty()) {
0812             mShareAction->setEnabled(false);
0813         } else {
0814             mShareAction->setEnabled(true);
0815 
0816             QJsonArray urls;
0817             for (const KFileItem &fileItem : selectedFiles) {
0818                 urls << QJsonValue(fileItem.url().toString());
0819             }
0820 
0821             mShareMenu->model()->setInputData(QJsonObject{{QStringLiteral("mimeType"), MimeTypeUtils::urlMimeType(url)}, {QStringLiteral("urls"), urls}});
0822             mShareMenu->model()->setPluginType(QStringLiteral("Export"));
0823             mShareMenu->reload();
0824         }
0825 #endif
0826     }
0827 
0828     bool sideBarVisibility() const
0829     {
0830         switch (mCurrentMainPageId) {
0831         case StartMainPageId:
0832             GV_WARN_AND_RETURN_VALUE(false, "Sidebar not implemented on start page");
0833             break;
0834         case BrowseMainPageId:
0835             return GwenviewConfig::sideBarVisible();
0836         case ViewMainPageId:
0837             return q->isFullScreen() ? GwenviewConfig::sideBarVisibleViewModeFullScreen() : GwenviewConfig::sideBarVisible();
0838         }
0839         return false;
0840     }
0841 
0842     void saveSideBarVisibility(const bool visible)
0843     {
0844         switch (mCurrentMainPageId) {
0845         case StartMainPageId:
0846             GV_WARN_AND_RETURN("Sidebar not implemented on start page");
0847             break;
0848         case BrowseMainPageId:
0849             GwenviewConfig::setSideBarVisible(visible);
0850             break;
0851         case ViewMainPageId:
0852             q->isFullScreen() ? GwenviewConfig::setSideBarVisibleViewModeFullScreen(visible) : GwenviewConfig::setSideBarVisible(visible);
0853             break;
0854         }
0855     }
0856 
0857     bool statusBarVisibility() const
0858     {
0859         switch (mCurrentMainPageId) {
0860         case StartMainPageId:
0861             GV_WARN_AND_RETURN_VALUE(false, "Statusbar not implemented on start page");
0862             break;
0863         case BrowseMainPageId:
0864             return GwenviewConfig::statusBarVisibleBrowseMode();
0865         case ViewMainPageId:
0866             return q->isFullScreen() ? GwenviewConfig::statusBarVisibleViewModeFullScreen() : GwenviewConfig::statusBarVisibleViewMode();
0867         }
0868         return false;
0869     }
0870 
0871     void saveStatusBarVisibility(const bool visible)
0872     {
0873         switch (mCurrentMainPageId) {
0874         case StartMainPageId:
0875             GV_WARN_AND_RETURN("Statusbar not implemented on start page");
0876             break;
0877         case BrowseMainPageId:
0878             GwenviewConfig::setStatusBarVisibleBrowseMode(visible);
0879             break;
0880         case ViewMainPageId:
0881             q->isFullScreen() ? GwenviewConfig::setStatusBarVisibleViewModeFullScreen(visible) : GwenviewConfig::setStatusBarVisibleViewMode(visible);
0882             break;
0883         }
0884     }
0885 
0886     void loadSplitterConfig()
0887     {
0888         const QList<int> sizes = GwenviewConfig::sideBarSplitterSizes();
0889         if (!sizes.isEmpty()) {
0890             mCentralSplitter->setSizes(sizes);
0891         }
0892     }
0893 
0894     void saveSplitterConfig()
0895     {
0896         if (mSideBar->isVisible()) {
0897             GwenviewConfig::setSideBarSplitterSizes(mCentralSplitter->sizes());
0898         }
0899     }
0900 
0901     void loadInformationSplitterConfig()
0902     {
0903         const QList<int> sizes = GwenviewConfig::informationSplitterSizes();
0904         if (!sizes.isEmpty()) {
0905             // Find the splitter inside the sidebar by objectName.
0906             auto informationSidebar = mSideBar->findChild<QSplitter *>(QStringLiteral("information_splitter"), Qt::FindChildrenRecursively);
0907             if (informationSidebar) {
0908                 informationSidebar->setSizes(sizes);
0909             } else {
0910                 qCWarning(GWENVIEW_APP_LOG) << "Could not find information splitter in sidebar when loading old position.";
0911             }
0912         }
0913     }
0914 
0915     void saveInformationSplitterConfig()
0916     {
0917         auto informationSidebar = mSideBar->findChild<QSplitter *>(QStringLiteral("information_splitter"), Qt::FindChildrenRecursively);
0918         if (informationSidebar) {
0919             GwenviewConfig::setInformationSplitterSizes(informationSidebar->sizes());
0920         } else {
0921             qCWarning(GWENVIEW_APP_LOG) << "Could not find information splitter in sidebar when saving new position.";
0922         }
0923     }
0924 
0925     void setScreenSaverEnabled(bool enabled)
0926     {
0927 #ifdef HAVE_QTDBUS
0928         if (enabled) {
0929             QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"),
0930                                                                   QStringLiteral("/ScreenSaver"),
0931                                                                   QStringLiteral("org.freedesktop.ScreenSaver"),
0932                                                                   QStringLiteral("Inhibit"));
0933             message << QGuiApplication::desktopFileName();
0934             message << i18n("Viewing media");
0935             QDBusReply<uint> reply = QDBusConnection::sessionBus().call(message);
0936             if (reply.isValid()) {
0937                 screenSaverDbusCookie = reply.value();
0938             }
0939         } else {
0940             if (screenSaverDbusCookie != 0) {
0941                 QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"),
0942                                                                       QStringLiteral("/ScreenSaver"),
0943                                                                       QStringLiteral("org.freedesktop.ScreenSaver"),
0944                                                                       QStringLiteral("UnInhibit"));
0945                 message << static_cast<uint>(screenSaverDbusCookie);
0946                 screenSaverDbusCookie = 0;
0947                 QDBusConnection::sessionBus().send(message);
0948             }
0949         }
0950 #endif
0951     }
0952 
0953     void assignThumbnailProviderToThumbnailView(ThumbnailView *thumbnailView)
0954     {
0955         GV_RETURN_IF_FAIL(thumbnailView);
0956         if (mActiveThumbnailView) {
0957             mActiveThumbnailView->setThumbnailProvider(nullptr);
0958         }
0959         thumbnailView->setThumbnailProvider(mThumbnailProvider);
0960         mActiveThumbnailView = thumbnailView;
0961         if (mActiveThumbnailView->isVisible()) {
0962             mThumbnailProvider->stop();
0963             mActiveThumbnailView->generateThumbnailsForItems();
0964         }
0965     }
0966 
0967     void autoAssignThumbnailProvider()
0968     {
0969         if (mCurrentMainPageId == ViewMainPageId) {
0970             if (q->windowState() & Qt::WindowFullScreen) {
0971                 assignThumbnailProviderToThumbnailView(mFullScreenContent->thumbnailBar());
0972             } else {
0973                 assignThumbnailProviderToThumbnailView(mViewMainPage->thumbnailBar());
0974             }
0975         } else if (mCurrentMainPageId == BrowseMainPageId) {
0976             assignThumbnailProviderToThumbnailView(mThumbnailView);
0977         } else if (mCurrentMainPageId == StartMainPageId) {
0978             assignThumbnailProviderToThumbnailView(mStartMainPage->recentFoldersView());
0979         }
0980     }
0981 
0982     enum class ShowPreview { Yes, No };
0983     void print(ShowPreview showPreview)
0984     {
0985         if (!mContextManager->currentUrlIsRasterImage()) {
0986             return;
0987         }
0988 
0989         Document::Ptr doc = DocumentFactory::instance()->load(mContextManager->currentUrl());
0990         PrintHelper printHelper(q);
0991         if (showPreview == ShowPreview::Yes) {
0992             printHelper.printPreview(doc);
0993         } else {
0994             printHelper.print(doc);
0995         }
0996     }
0997 
0998     /// Handles the clicking of links in help texts if the clicked url uses the "gwenview:" scheme.
0999     class InternalUrlClickedHandler : public QObject
1000     {
1001     public:
1002         InternalUrlClickedHandler(MainWindow *parent)
1003             : QObject(parent)
1004         {
1005             Q_CHECK_PTR(parent);
1006         }
1007 
1008         inline bool eventFilter(QObject * /* watched */, QEvent *event) override
1009         {
1010             if (event->type() != QEvent::WhatsThisClicked) {
1011                 return false;
1012             }
1013             const QString linkAddress = static_cast<QWhatsThisClickedEvent *>(event)->href();
1014             if (!linkAddress.startsWith(QStringLiteral("gwenview:"))) {
1015                 // This eventFilter only handles our internal "gwenview" scheme. Everything else is handled by KXmlGui::KToolTipHelper.
1016                 return false;
1017             }
1018             event->accept();
1019             auto linkPathList = linkAddress.split(QLatin1Char('/'));
1020             linkPathList.removeFirst(); // remove "gwenview:/"
1021             Q_ASSERT(!linkPathList.isEmpty());
1022             Q_ASSERT_X(linkPathList.constFirst() == QStringLiteral("config"), "link resolver", "Handling of this URL is not yet implemented");
1023             linkPathList.removeFirst(); // remove "config/"
1024             auto mainWindow = static_cast<MainWindow *>(parent());
1025             if (linkPathList.isEmpty()) {
1026                 mainWindow->showConfigDialog();
1027             } else if (linkPathList.constFirst() == QStringLiteral("general")) {
1028                 mainWindow->showConfigDialog(0); // "0" should open General.
1029             } else if (linkPathList.constFirst() == QStringLiteral("imageview")) {
1030                 mainWindow->showConfigDialog(1); // "1" should open Image View.
1031             } else if (linkPathList.constFirst() == QStringLiteral("advanced")) {
1032                 mainWindow->showConfigDialog(2); // "2" should open Advanced.
1033             } else {
1034                 Q_ASSERT_X(false, "config link resolver", "Handling of this config URL is not yet implemented");
1035             }
1036             return true;
1037         }
1038     };
1039 };
1040 
1041 MainWindow::MainWindow()
1042     : KXmlGuiWindow()
1043     , d(new MainWindow::Private)
1044 {
1045     d->q = this;
1046     d->mCurrentMainPageId = StartMainPageId;
1047     d->mDirModel = new SortedDirModel(this);
1048     d->setupContextManager();
1049     d->setupThumbnailBarModel();
1050     d->mGvCore = new GvCore(this, d->mDirModel);
1051     d->mPreloader = new Preloader(this);
1052     d->mThumbnailProvider = new ThumbnailProvider();
1053     d->mActiveThumbnailView = nullptr;
1054     d->initDirModel();
1055     d->setupWidgets();
1056     d->setupActions();
1057     d->setupUndoActions();
1058     d->setupContextManagerItems();
1059     d->setupFullScreenContent();
1060 
1061     d->updateActions();
1062     updatePreviousNextActions();
1063     d->mSaveBar->initActionDependentWidgets();
1064 
1065     createGUI();
1066     loadConfig();
1067 
1068     connect(DocumentFactory::instance(), &DocumentFactory::modifiedDocumentListChanged, this, &MainWindow::slotModifiedDocumentListChanged);
1069     connect(qApp, &QApplication::focusChanged, this, &MainWindow::onFocusChanged);
1070 
1071 #ifdef HAVE_QTDBUS
1072     d->mMpris2Service =
1073         new Mpris2Service(d->mSlideShow, d->mContextManager, d->mToggleSlideShowAction, d->mFullScreenAction, d->mGoToPreviousAction, d->mGoToNextAction, this);
1074 #endif
1075 
1076     auto ratingMenu = static_cast<QMenu *>(guiFactory()->container(QStringLiteral("rating"), this));
1077     ratingMenu->setIcon(QIcon::fromTheme(QStringLiteral("rating-unrated")));
1078 #ifdef GWENVIEW_SEMANTICINFO_BACKEND_NONE
1079     if (ratingMenu) {
1080         ratingMenu->menuAction()->setVisible(false);
1081     }
1082 #endif
1083     setAutoSaveSettings();
1084 #ifdef Q_OS_OSX
1085     qApp->installEventFilter(this);
1086 #endif
1087     qApp->installEventFilter(new Private::InternalUrlClickedHandler(this));
1088 }
1089 
1090 MainWindow::~MainWindow()
1091 {
1092     delete d->mThumbnailProvider;
1093     delete d;
1094 }
1095 
1096 ContextManager *MainWindow::contextManager() const
1097 {
1098     return d->mContextManager;
1099 }
1100 
1101 ViewMainPage *MainWindow::viewMainPage() const
1102 {
1103     return d->mViewMainPage;
1104 }
1105 
1106 void MainWindow::setCaption(const QString &caption)
1107 {
1108     // Keep a trace of caption to use it in slotModifiedDocumentListChanged()
1109     d->mCaption = caption;
1110     KXmlGuiWindow::setCaption(caption);
1111 }
1112 
1113 void MainWindow::setCaption(const QString &caption, bool modified)
1114 {
1115     d->mCaption = caption;
1116     KXmlGuiWindow::setCaption(caption, modified);
1117 }
1118 
1119 void MainWindow::slotUpdateCaption(const QString &caption)
1120 {
1121     const QUrl url = d->mContextManager->currentUrl();
1122     const QList<QUrl> list = DocumentFactory::instance()->modifiedDocumentList();
1123     setCaption(caption, list.contains(url));
1124 }
1125 
1126 void MainWindow::slotModifiedDocumentListChanged()
1127 {
1128     d->updateActions();
1129     slotUpdateCaption(d->mCaption);
1130 }
1131 
1132 void MainWindow::setInitialUrl(const QUrl &_url)
1133 {
1134     Q_ASSERT(_url.isValid());
1135     QUrl url = UrlUtils::fixUserEnteredUrl(_url);
1136     d->mGvCore->setTrackFileManagerSorting(true);
1137     syncSortOrder(url);
1138 
1139     if (UrlUtils::urlIsDirectory(url)) {
1140         d->mBrowseAction->trigger();
1141         openDirUrl(url);
1142     } else {
1143         openUrl(url);
1144     }
1145 }
1146 
1147 void MainWindow::startSlideShow()
1148 {
1149     d->mViewAction->trigger();
1150     // We need to wait until we have listed all images in the dirlister to
1151     // start the slideshow because the SlideShow objects needs an image list to
1152     // work.
1153     d->mStartSlideShowWhenDirListerCompleted = true;
1154 }
1155 
1156 void MainWindow::setActiveViewModeAction(QAction *action)
1157 {
1158     if (action == d->mViewAction) {
1159         d->mCurrentMainPageId = ViewMainPageId;
1160         // Switching to view mode
1161         d->mViewStackedWidget->setCurrentWidget(d->mViewMainPage);
1162         openSelectedDocuments();
1163         d->mPreloadDirectionIsForward = true;
1164         QTimer::singleShot(VIEW_PRELOAD_DELAY, this, &MainWindow::preloadNextUrl);
1165     } else {
1166         d->mCurrentMainPageId = BrowseMainPageId;
1167         // Switching to browse mode
1168         d->mViewStackedWidget->setCurrentWidget(d->mBrowseMainPage);
1169         if (!d->mViewMainPage->isEmpty() && KProtocolManager::supportsListing(d->mViewMainPage->url())) {
1170             // Reset the view to spare resources, but don't do it if we can't
1171             // browse the url, otherwise if the user starts Gwenview this way:
1172             // gwenview http://example.com/example.png
1173             // and switch to browse mode, switching back to view mode won't bring
1174             // his image back.
1175             d->mViewMainPage->reset();
1176         }
1177         setCaption(d->mUrlNavigator->locationUrl().adjusted(QUrl::RemoveScheme).toString());
1178     }
1179     d->autoAssignThumbnailProvider();
1180     toggleSideBar(d->sideBarVisibility());
1181     toggleStatusBar(d->statusBarVisibility());
1182 
1183     Q_EMIT viewModeChanged();
1184 }
1185 
1186 void MainWindow::slotThumbnailViewIndexActivated(const QModelIndex &index)
1187 {
1188     if (!index.isValid()) {
1189         return;
1190     }
1191 
1192     KFileItem item = d->mDirModel->itemForIndex(index);
1193     syncSortOrder(item.url());
1194     if (item.isDir()) {
1195         // Item is a dir, open it
1196         openDirUrl(item.url());
1197     } else {
1198         QString protocol = ArchiveUtils::protocolForMimeType(item.mimetype());
1199         if (!protocol.isEmpty()) {
1200             // Item is an archive, tweak url then open it
1201             QUrl url = item.url();
1202             url.setScheme(protocol);
1203             openDirUrl(url);
1204         } else {
1205             // Item is a document, switch to view mode
1206             d->mViewAction->trigger();
1207         }
1208     }
1209 }
1210 
1211 void MainWindow::openSelectedDocuments()
1212 {
1213     if (d->mCurrentMainPageId != ViewMainPageId) {
1214         return;
1215     }
1216 
1217     int count = 0;
1218     QList<QUrl> urls;
1219     const auto list = d->mContextManager->selectedFileItemList();
1220     for (const auto &item : list) {
1221         if (!item.isNull() && !ArchiveUtils::fileItemIsDirOrArchive(item)) {
1222             urls << item.url();
1223 
1224             ++count;
1225             if (count == ViewMainPage::MaxViewCount) {
1226                 break;
1227             }
1228         }
1229     }
1230 
1231     if (urls.isEmpty()) {
1232         // Selection contains no fitting items
1233         // Switch back to browsing mode
1234         d->mBrowseAction->trigger();
1235         d->mViewMainPage->reset();
1236         return;
1237     }
1238 
1239     QUrl currentUrl = d->mContextManager->currentUrl();
1240     if (currentUrl.isEmpty() || !urls.contains(currentUrl)) {
1241         // There is no current URL or it doesn't belong to selection
1242         // This can happen when user manually selects a group of items
1243         currentUrl = urls.first();
1244     }
1245 
1246     d->mViewMainPage->openUrls(urls, currentUrl);
1247 }
1248 
1249 void MainWindow::goUp()
1250 {
1251     if (d->mCurrentMainPageId == BrowseMainPageId) {
1252         QUrl url = d->mContextManager->currentDirUrl();
1253         url = KIO::upUrl(url);
1254         openDirUrl(url);
1255     } else {
1256         d->mBrowseAction->trigger();
1257     }
1258 }
1259 
1260 void MainWindow::showStartMainPage()
1261 {
1262     d->mCurrentMainPageId = StartMainPageId;
1263     d->setActionsDisabledOnStartMainPageEnabled(false);
1264 
1265     d->saveSplitterConfig();
1266     d->mSideBar->hide();
1267     d->mViewStackedWidget->setCurrentWidget(d->mStartMainPage);
1268 
1269     d->updateActions();
1270     updatePreviousNextActions();
1271     d->mContextManager->setCurrentDirUrl(QUrl());
1272     d->mContextManager->setCurrentUrl(QUrl());
1273 
1274     d->autoAssignThumbnailProvider();
1275 }
1276 
1277 void MainWindow::slotStartMainPageUrlSelected(const QUrl &url)
1278 {
1279     d->setActionsDisabledOnStartMainPageEnabled(true);
1280 
1281     if (d->mBrowseAction->isChecked()) {
1282         // Silently uncheck the action so that setInitialUrl() does the right thing
1283         SignalBlocker blocker(d->mBrowseAction);
1284         d->mBrowseAction->setChecked(false);
1285     }
1286 
1287     setInitialUrl(url);
1288 }
1289 
1290 void MainWindow::openDirUrl(const QUrl &url)
1291 {
1292     const QUrl currentUrl = d->mContextManager->currentDirUrl();
1293 
1294     if (url == currentUrl) {
1295         return;
1296     }
1297 
1298     if (url.isParentOf(currentUrl)) {
1299         // Keep first child between url and currentUrl selected
1300         // If currentPath is      "/home/user/photos/2008/event"
1301         // and wantedPath is      "/home/user/photos"
1302         // pathToSelect should be "/home/user/photos/2008"
1303 
1304         // To anyone considering using QUrl::toLocalFile() instead of
1305         // QUrl::path() here. Please don't, using QUrl::path() is the right
1306         // thing to do here.
1307         const QString currentPath = QDir::cleanPath(currentUrl.adjusted(QUrl::StripTrailingSlash).path());
1308         const QString wantedPath = QDir::cleanPath(url.adjusted(QUrl::StripTrailingSlash).path());
1309         const QChar separator('/');
1310         const int slashCount = wantedPath.count(separator);
1311         const QString pathToSelect = currentPath.section(separator, 0, slashCount + 1);
1312         QUrl urlToSelect = url;
1313         urlToSelect.setPath(pathToSelect);
1314         d->mContextManager->setUrlToSelect(urlToSelect);
1315     }
1316     d->mThumbnailProvider->stop();
1317     d->mContextManager->setCurrentDirUrl(url);
1318     d->mGvCore->addUrlToRecentFolders(url);
1319     d->mViewMainPage->reset();
1320 }
1321 
1322 void MainWindow::folderViewUrlChanged(const QUrl &url)
1323 {
1324     const QUrl currentUrl = d->mContextManager->currentDirUrl();
1325 
1326     if (url == currentUrl) {
1327         switch (d->mCurrentMainPageId) {
1328         case ViewMainPageId:
1329             d->mBrowseAction->trigger();
1330             break;
1331         case BrowseMainPageId:
1332             d->mViewAction->trigger();
1333             break;
1334         case StartMainPageId:
1335             break;
1336         }
1337     } else {
1338         openDirUrl(url);
1339     }
1340 }
1341 
1342 void MainWindow::syncSortOrder(const QUrl &url)
1343 {
1344     if (!d->mGvCore->trackFileManagerSorting()) {
1345         return;
1346     }
1347 
1348 #ifdef HAVE_QTDBUS
1349     QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.FileManager1"),
1350                                                           QStringLiteral("/org/freedesktop/FileManager1"),
1351                                                           QStringLiteral("org.freedesktop.FileManager1"),
1352                                                           QStringLiteral("SortOrderForUrl"));
1353 
1354     QUrl dirUrl = url;
1355     dirUrl = dirUrl.adjusted(QUrl::RemoveFilename);
1356     message << dirUrl.toString();
1357 
1358     QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(message);
1359     auto watcher = new QDBusPendingCallWatcher(call, this);
1360 
1361     connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *call) {
1362         QDBusPendingReply<QString, QString> reply = *call;
1363         // Fail silently
1364         if (!reply.isError()) {
1365             QString columnName = reply.argumentAt<0>();
1366             QString orderName = reply.argumentAt<1>();
1367 
1368             int column = -1;
1369             int sortRole = -1;
1370             Qt::SortOrder order = orderName == QStringLiteral("descending") ? Qt::DescendingOrder : Qt::AscendingOrder;
1371 
1372             if (columnName == "text") {
1373                 column = KDirModel::Name;
1374                 sortRole = Qt::DisplayRole;
1375                 qCDebug(GWENVIEW_APP_LOG) << "Sorting according to file manager: text";
1376             } else if (columnName == "modificationtime") {
1377                 column = KDirModel::ModifiedTime;
1378                 sortRole = Qt::DisplayRole;
1379                 qCDebug(GWENVIEW_APP_LOG) << "Sorting according to file manager: modtime";
1380             } else if (columnName == "size") {
1381                 column = KDirModel::Size;
1382                 sortRole = Qt::DisplayRole;
1383                 qCDebug(GWENVIEW_APP_LOG) << "Sorting according to file manager: size";
1384 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
1385             } else if (columnName == "rating") {
1386                 column = KDirModel::Name;
1387                 sortRole = SemanticInfoDirModel::RatingRole;
1388                 qCDebug(GWENVIEW_APP_LOG) << "Sorting according to file manager: rating";
1389 #endif
1390             }
1391 
1392             if (column >= 0 && sortRole >= 0) {
1393                 d->mDirModel->setSortRole(sortRole);
1394                 d->mDirModel->sort(column, order);
1395             }
1396         }
1397         call->deleteLater();
1398     });
1399 #endif
1400 }
1401 
1402 void MainWindow::toggleSideBar(bool visible)
1403 {
1404     d->saveSplitterConfig();
1405     d->mToggleSideBarAction->setChecked(visible);
1406     d->mToggleOperationsSideBarAction->setChecked(visible && d->mSideBar->currentPage() == QLatin1String("operations"));
1407     d->saveSideBarVisibility(visible);
1408     d->mSideBar->setVisible(visible);
1409 
1410     const QString iconName = QApplication::isRightToLeft() ? (visible ? "sidebar-collapse-right" : "sidebar-expand-right")
1411                                                            : (visible ? "sidebar-collapse-left" : "sidebar-expand-left");
1412     const QString toolTip = visible ? i18nc("@info:tooltip", "Hide sidebar") : i18nc("@info:tooltip", "Show sidebar");
1413 
1414     const QList<QToolButton *> buttonList{d->mBrowseMainPage->toggleSideBarButton(), d->mViewMainPage->toggleSideBarButton()};
1415     for (auto button : buttonList) {
1416         button->setIcon(QIcon::fromTheme(iconName));
1417         button->setToolTip(toolTip);
1418     }
1419 }
1420 
1421 void MainWindow::toggleOperationsSideBar(bool visible)
1422 {
1423     if (visible) {
1424         d->mSideBar->setCurrentPage(QLatin1String("operations"));
1425     }
1426     toggleSideBar(visible);
1427 }
1428 
1429 void MainWindow::toggleStatusBar(bool visible)
1430 {
1431     d->mShowStatusBarAction->setChecked(visible);
1432     d->saveStatusBarVisibility(visible);
1433 
1434     d->mViewMainPage->setStatusBarVisible(visible);
1435     d->mBrowseMainPage->setStatusBarVisible(visible);
1436 }
1437 
1438 void MainWindow::slotPartCompleted()
1439 {
1440     d->updateActions();
1441     const QUrl url = d->mContextManager->currentUrl();
1442     if (!url.isEmpty() && GwenviewConfig::historyEnabled()) {
1443         d->mFileOpenRecentAction->addUrl(url);
1444         d->mGvCore->addUrlToRecentFiles(url);
1445     }
1446 
1447     if (KProtocolManager::supportsListing(url)) {
1448         const QUrl dirUrl = d->mContextManager->currentDirUrl();
1449         d->mGvCore->addUrlToRecentFolders(dirUrl);
1450     }
1451 }
1452 
1453 void MainWindow::slotSelectionChanged()
1454 {
1455     if (d->mCurrentMainPageId == ViewMainPageId) {
1456         // The user selected a new file in the thumbnail view, since the
1457         // document view is visible, let's show it
1458         openSelectedDocuments();
1459     } else {
1460         // No document view, we need to load the document to set the undo group
1461         // of document factory to the correct QUndoStack
1462         QModelIndex index = d->mThumbnailView->currentIndex();
1463         KFileItem item;
1464         if (index.isValid()) {
1465             item = d->mDirModel->itemForIndex(index);
1466         }
1467         QUndoGroup *undoGroup = DocumentFactory::instance()->undoGroup();
1468         if (!item.isNull() && !ArchiveUtils::fileItemIsDirOrArchive(item)) {
1469             QUrl url = item.url();
1470             Document::Ptr doc = DocumentFactory::instance()->load(url);
1471             undoGroup->addStack(doc->undoStack());
1472             undoGroup->setActiveStack(doc->undoStack());
1473         } else {
1474             undoGroup->setActiveStack(nullptr);
1475         }
1476     }
1477 
1478     // Update UI
1479     d->updateActions();
1480     updatePreviousNextActions();
1481 
1482     // Start preloading
1483     int preloadDelay = d->mCurrentMainPageId == ViewMainPageId ? VIEW_PRELOAD_DELAY : BROWSE_PRELOAD_DELAY;
1484     QTimer::singleShot(preloadDelay, this, &MainWindow::preloadNextUrl);
1485 }
1486 
1487 void MainWindow::slotCurrentDirUrlChanged(const QUrl &url)
1488 {
1489     if (url.isValid()) {
1490         d->mUrlNavigator->setLocationUrl(url);
1491         d->mGoUpAction->setEnabled(url.path() != "/");
1492         if (d->mCurrentMainPageId == BrowseMainPageId) {
1493             setCaption(d->mUrlNavigator->locationUrl().toDisplayString(QUrl::PreferLocalFile));
1494         }
1495     } else {
1496         d->mGoUpAction->setEnabled(false);
1497     }
1498 }
1499 
1500 void MainWindow::slotDirModelNewItems()
1501 {
1502     if (d->mContextManager->selectionModel()->hasSelection()) {
1503         updatePreviousNextActions();
1504     }
1505 }
1506 
1507 void MainWindow::slotDirListerCompleted()
1508 {
1509     if (d->mStartSlideShowWhenDirListerCompleted) {
1510         d->mStartSlideShowWhenDirListerCompleted = false;
1511         QTimer::singleShot(0, d->mToggleSlideShowAction, &QAction::trigger);
1512     }
1513     if (d->mContextManager->selectionModel()->hasSelection()) {
1514         updatePreviousNextActions();
1515     } else if (!d->mContextManager->currentUrl().isValid()) {
1516         d->goToFirstDocument();
1517 
1518         // Try to select the first directory in case there are no images to select
1519         if (!d->mContextManager->selectionModel()->hasSelection()) {
1520             const QModelIndex index = d->mThumbnailView->model()->index(0, 0);
1521             if (index.isValid()) {
1522                 d->mThumbnailView->setCurrentIndex(index);
1523             }
1524         }
1525     }
1526     d->mThumbnailView->scrollToSelectedIndex();
1527     d->mViewMainPage->thumbnailBar()->scrollToSelectedIndex();
1528     d->mFullScreenContent->thumbnailBar()->scrollToSelectedIndex();
1529 }
1530 
1531 void MainWindow::goToPrevious()
1532 {
1533     const QModelIndex currentIndex = d->mContextManager->selectionModel()->currentIndex();
1534     QModelIndex previousIndex = d->mDirModel->index(currentIndex.row(), 0);
1535 
1536     constexpr QFlags<MimeTypeUtils::Kind> allowedKinds = MimeTypeUtils::KIND_RASTER_IMAGE | MimeTypeUtils::KIND_SVG_IMAGE | MimeTypeUtils::KIND_VIDEO;
1537     KFileItem fileItem;
1538 
1539     // Besides images also folders and archives are listed as well,
1540     // we need to skip them in the slideshow
1541     do {
1542         previousIndex = d->mDirModel->index(previousIndex.row() - 1, 0);
1543         fileItem = previousIndex.data(KDirModel::FileItemRole).value<KFileItem>();
1544     } while (previousIndex.isValid() && !(allowedKinds & MimeTypeUtils::fileItemKind(fileItem)));
1545 
1546     if (!previousIndex.isValid()
1547         && (GwenviewConfig::navigationEndNotification() == SlideShow::NeverWarn
1548             || (GwenviewConfig::navigationEndNotification() == SlideShow::WarnOnSlideshow && !d->mSlideShow->isRunning() && !d->mFullScreenAction->isChecked())
1549             || d->hudButtonBox)) {
1550         d->goToLastDocument();
1551     } else if (!previousIndex.isValid()) {
1552         showFirstDocumentReached();
1553     } else {
1554         d->goTo(-1);
1555     }
1556 }
1557 
1558 void MainWindow::goToNext()
1559 {
1560     const QModelIndex currentIndex = d->mContextManager->selectionModel()->currentIndex();
1561     QModelIndex nextIndex = d->mDirModel->index(currentIndex.row(), 0);
1562 
1563     constexpr QFlags<MimeTypeUtils::Kind> allowedKinds = MimeTypeUtils::KIND_RASTER_IMAGE | MimeTypeUtils::KIND_SVG_IMAGE | MimeTypeUtils::KIND_VIDEO;
1564     KFileItem fileItem;
1565 
1566     // Besides images also folders and archives are listed as well,
1567     // we need to skip them in the slideshow
1568     do {
1569         nextIndex = d->mDirModel->index(nextIndex.row() + 1, 0);
1570         fileItem = nextIndex.data(KDirModel::FileItemRole).value<KFileItem>();
1571     } while (nextIndex.isValid() && !(allowedKinds & MimeTypeUtils::fileItemKind(fileItem)));
1572 
1573     if (!nextIndex.isValid()
1574         && (GwenviewConfig::navigationEndNotification() == SlideShow::NeverWarn
1575             || (GwenviewConfig::navigationEndNotification() == SlideShow::WarnOnSlideshow && !d->mSlideShow->isRunning() && !d->mFullScreenAction->isChecked())
1576             || d->hudButtonBox)) {
1577         d->goToFirstDocument();
1578     } else if (!nextIndex.isValid()) {
1579         showLastDocumentReached();
1580     } else {
1581         d->goTo(1);
1582     }
1583 }
1584 
1585 void MainWindow::goToFirst()
1586 {
1587     d->goToFirstDocument();
1588 }
1589 
1590 void MainWindow::goToLast()
1591 {
1592     d->goToLastDocument();
1593 }
1594 
1595 void MainWindow::goToUrl(const QUrl &url)
1596 {
1597     if (d->mCurrentMainPageId == ViewMainPageId) {
1598         d->mViewMainPage->openUrl(url);
1599     }
1600     QUrl dirUrl = url;
1601     dirUrl = dirUrl.adjusted(QUrl::RemoveFilename);
1602     if (dirUrl != d->mContextManager->currentDirUrl()) {
1603         d->mContextManager->setCurrentDirUrl(dirUrl);
1604         d->mGvCore->addUrlToRecentFolders(dirUrl);
1605     }
1606     d->mContextManager->setUrlToSelect(url);
1607 }
1608 
1609 void MainWindow::updatePreviousNextActions()
1610 {
1611     bool hasPrevious;
1612     bool hasNext;
1613     QModelIndex currentIndex = d->mContextManager->selectionModel()->currentIndex();
1614     if (currentIndex.isValid() && !d->indexIsDirOrArchive(currentIndex)) {
1615         int row = currentIndex.row();
1616         QModelIndex prevIndex = d->mDirModel->index(row - 1, 0);
1617         QModelIndex nextIndex = d->mDirModel->index(row + 1, 0);
1618         hasPrevious = GwenviewConfig::navigationEndNotification() != SlideShow::AlwaysWarn || (prevIndex.isValid() && !d->indexIsDirOrArchive(prevIndex));
1619         hasNext = GwenviewConfig::navigationEndNotification() != SlideShow::AlwaysWarn || (nextIndex.isValid() && !d->indexIsDirOrArchive(nextIndex));
1620     } else {
1621         hasPrevious = false;
1622         hasNext = false;
1623     }
1624 
1625     d->mGoToPreviousAction->setEnabled(hasPrevious);
1626     d->mGoToNextAction->setEnabled(hasNext);
1627     d->mGoToFirstAction->setEnabled(hasPrevious);
1628     d->mGoToLastAction->setEnabled(hasNext);
1629 }
1630 
1631 void MainWindow::leaveFullScreen()
1632 {
1633     if (d->mFullScreenAction->isChecked()) {
1634         d->mFullScreenAction->trigger();
1635     }
1636 }
1637 
1638 void MainWindow::toggleFullScreen(bool checked)
1639 {
1640     setUpdatesEnabled(false);
1641     if (checked) {
1642         // Save MainWindow config now, this way if we quit while in
1643         // fullscreen, we are sure latest MainWindow changes are remembered.
1644         KConfigGroup saveConfigGroup = autoSaveConfigGroup();
1645         if (!isFullScreen()) {
1646             // Save state if window manager did not already switch to fullscreen.
1647             saveMainWindowSettings(saveConfigGroup);
1648             d->mStateBeforeFullScreen.mToolBarVisible = toolBar()->isVisible();
1649         }
1650         setAutoSaveSettings(saveConfigGroup, false);
1651         resetAutoSaveSettings();
1652 
1653         // Go full screen
1654         KToggleFullScreenAction::setFullScreen(this, true);
1655         menuBar()->hide();
1656         toolBar()->hide();
1657 
1658         qApp->setProperty("KDE_COLOR_SCHEME_PATH", d->mGvCore->fullScreenPaletteName());
1659         QApplication::setPalette(d->mGvCore->palette(GvCore::FullScreenPalette));
1660 
1661         d->setScreenSaverEnabled(false);
1662     } else {
1663         setAutoSaveSettings();
1664 
1665         // Back to normal
1666         qApp->setProperty("KDE_COLOR_SCHEME_PATH", QVariant());
1667         QApplication::setPalette(d->mGvCore->palette(GvCore::NormalPalette));
1668 
1669         d->mSlideShow->pause();
1670         KToggleFullScreenAction::setFullScreen(this, false);
1671         menuBar()->setVisible(d->mShowMenuBarAction->isChecked());
1672         toolBar()->setVisible(d->mStateBeforeFullScreen.mToolBarVisible);
1673 
1674         d->setScreenSaverEnabled(true);
1675 
1676         // See resizeEvent
1677         d->mFullScreenLeftAt = QDateTime::currentDateTime();
1678     }
1679 
1680     d->mFullScreenContent->setFullScreenMode(checked);
1681     d->mBrowseMainPage->setFullScreenMode(checked);
1682     d->mViewMainPage->setFullScreenMode(checked);
1683     d->mSaveBar->setFullScreenMode(checked);
1684 
1685     toggleSideBar(d->sideBarVisibility());
1686     toggleStatusBar(d->statusBarVisibility());
1687 
1688     setUpdatesEnabled(true);
1689     d->autoAssignThumbnailProvider();
1690 }
1691 
1692 void MainWindow::saveCurrent()
1693 {
1694     d->mGvCore->save(d->mContextManager->currentUrl());
1695 }
1696 
1697 void MainWindow::saveCurrentAs()
1698 {
1699     d->mGvCore->saveAs(d->mContextManager->currentUrl());
1700 }
1701 
1702 void MainWindow::reload()
1703 {
1704     if (d->mCurrentMainPageId == ViewMainPageId) {
1705         d->mViewMainPage->reload();
1706     } else {
1707         d->mBrowseMainPage->reload();
1708     }
1709 }
1710 
1711 void MainWindow::openFile()
1712 {
1713     const QUrl dirUrl = d->mContextManager->currentDirUrl();
1714 
1715     auto dialog = new QFileDialog(this);
1716     dialog->setAttribute(Qt::WA_DeleteOnClose);
1717     dialog->setModal(true);
1718     dialog->setDirectoryUrl(dirUrl);
1719     dialog->setWindowTitle(i18nc("@title:window", "Open Image"));
1720     dialog->setMimeTypeFilters(MimeTypeUtils::imageMimeTypes());
1721     dialog->setAcceptMode(QFileDialog::AcceptOpen);
1722     connect(dialog, &QDialog::accepted, this, [this, dialog]() {
1723         if (!dialog->selectedUrls().isEmpty()) {
1724             openUrl(dialog->selectedUrls().at(0));
1725         }
1726     });
1727 
1728     dialog->show();
1729 }
1730 
1731 void MainWindow::openUrl(const QUrl &url)
1732 {
1733     d->setActionsDisabledOnStartMainPageEnabled(true);
1734     d->mContextManager->setUrlToSelect(url);
1735     d->mViewAction->trigger();
1736 }
1737 
1738 void MainWindow::showDocumentInFullScreen(const QUrl &url)
1739 {
1740     d->mContextManager->setUrlToSelect(url);
1741     d->mViewAction->trigger();
1742     d->mFullScreenAction->trigger();
1743 }
1744 
1745 void MainWindow::toggleSlideShow()
1746 {
1747     if (d->mSlideShow->isRunning()) {
1748         d->mSlideShow->pause();
1749     } else {
1750         if (!d->mViewAction->isChecked()) {
1751             d->mViewAction->trigger();
1752         }
1753         if (!d->mFullScreenAction->isChecked()) {
1754             d->mFullScreenAction->trigger();
1755         }
1756         QList<QUrl> list;
1757         for (int pos = 0; pos < d->mDirModel->rowCount(); ++pos) {
1758             QModelIndex index = d->mDirModel->index(pos, 0);
1759             KFileItem item = d->mDirModel->itemForIndex(index);
1760             MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item);
1761             switch (kind) {
1762             case MimeTypeUtils::KIND_SVG_IMAGE:
1763             case MimeTypeUtils::KIND_RASTER_IMAGE:
1764             case MimeTypeUtils::KIND_VIDEO:
1765                 list << item.url();
1766                 break;
1767             default:
1768                 break;
1769             }
1770         }
1771         d->mSlideShow->start(list);
1772     }
1773     updateSlideShowAction();
1774 }
1775 
1776 void MainWindow::updateSlideShowAction()
1777 {
1778     if (d->mSlideShow->isRunning()) {
1779         d->mToggleSlideShowAction->setText(i18n("Pause Slideshow"));
1780         d->mToggleSlideShowAction->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause")));
1781     } else {
1782         d->mToggleSlideShowAction->setText(d->mFullScreenAction->isChecked() ? i18n("Resume Slideshow") : i18n("Start Slideshow"));
1783         d->mToggleSlideShowAction->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start")));
1784     }
1785 }
1786 
1787 bool MainWindow::queryClose()
1788 {
1789     saveConfig();
1790     QList<QUrl> list = DocumentFactory::instance()->modifiedDocumentList();
1791     if (list.size() == 0) {
1792         return true;
1793     }
1794 
1795     KGuiItem yes(i18n("Save All Changes"), "document-save-all");
1796     KGuiItem no(i18n("Discard Changes"), "delete");
1797     QString msg =
1798         i18np("One image has been modified.", "%1 images have been modified.", list.size()) + '\n' + i18n("If you quit now, your changes will be lost.");
1799     int answer = KMessageBox::warningTwoActionsCancel(this, msg, QString() /* caption */, yes, no);
1800 
1801     switch (answer) {
1802     case KMessageBox::PrimaryAction:
1803         d->mGvCore->saveAll();
1804         // We need to wait a bit because the DocumentFactory is notified about
1805         // saved documents through a queued connection.
1806         qApp->processEvents();
1807         return DocumentFactory::instance()->modifiedDocumentList().isEmpty();
1808 
1809     case KMessageBox::SecondaryAction:
1810         return true;
1811 
1812     default: // cancel
1813         return false;
1814     }
1815 }
1816 
1817 void MainWindow::showConfigDialog(int page)
1818 {
1819     // Save first so changes like thumbnail zoom level are not lost when reloading config
1820     saveConfig();
1821 
1822     auto dialog = new ConfigDialog(this);
1823     dialog->setAttribute(Qt::WA_DeleteOnClose);
1824     dialog->setModal(true);
1825     connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::loadConfig);
1826     dialog->setCurrentPage(page);
1827     dialog->show();
1828 }
1829 
1830 void MainWindow::configureShortcuts()
1831 {
1832     guiFactory()->showConfigureShortcutsDialog();
1833 }
1834 
1835 void MainWindow::toggleMenuBar()
1836 {
1837     if (!d->mFullScreenAction->isChecked()) {
1838         if (!d->mShowMenuBarAction->isChecked() && (!toolBar()->isVisible() || !toolBar()->actions().contains(d->mHamburgerMenu))) {
1839             const QString accel = d->mShowMenuBarAction->shortcut().toString();
1840             KMessageBox::information(this,
1841                                      i18n("This will hide the menu bar completely."
1842                                           " You can show it again by typing %1.",
1843                                           accel),
1844                                      i18n("Hide menu bar"),
1845                                      QLatin1String("HideMenuBarWarning"));
1846         }
1847         menuBar()->setVisible(d->mShowMenuBarAction->isChecked());
1848     }
1849 }
1850 
1851 void MainWindow::loadConfig()
1852 {
1853     d->mDirModel->setBlackListedExtensions(GwenviewConfig::blackListedExtensions());
1854     d->mDirModel->adjustKindFilter(MimeTypeUtils::KIND_VIDEO, GwenviewConfig::listVideos());
1855 
1856     if (GwenviewConfig::historyEnabled()) {
1857         d->mFileOpenRecentAction->loadEntries(KConfigGroup(KSharedConfig::openConfig(), "Recent Files"));
1858         const auto mFileOpenRecentActionUrls = d->mFileOpenRecentAction->urls();
1859         for (const QUrl &url : mFileOpenRecentActionUrls) {
1860             d->mGvCore->addUrlToRecentFiles(url);
1861         }
1862     } else {
1863         d->mFileOpenRecentAction->clear();
1864     }
1865     d->mFileOpenRecentAction->setVisible(GwenviewConfig::historyEnabled());
1866 
1867     d->mStartMainPage->loadConfig();
1868     d->mViewMainPage->loadConfig();
1869     d->mBrowseMainPage->loadConfig();
1870     d->mContextManager->loadConfig();
1871     d->mSideBar->loadConfig();
1872     d->loadSplitterConfig();
1873     d->loadInformationSplitterConfig();
1874 }
1875 
1876 void MainWindow::saveConfig()
1877 {
1878     d->mFileOpenRecentAction->saveEntries(KConfigGroup(KSharedConfig::openConfig(), "Recent Files"));
1879     d->mViewMainPage->saveConfig();
1880     d->mBrowseMainPage->saveConfig();
1881     d->mContextManager->saveConfig();
1882     d->saveSplitterConfig();
1883     d->saveInformationSplitterConfig();
1884     GwenviewConfig::setFullScreenModeActive(isFullScreen());
1885     // Save the last used version when Gwenview closes so we know which settings/features the user
1886     // is aware of which is needed for migration. The version format is: two digits each for
1887     // major minor bugfix version. Never decrease this number. Increase it when needed.
1888     GwenviewConfig::setLastUsedVersion(210800);
1889 }
1890 
1891 void MainWindow::print()
1892 {
1893     d->print(Private::ShowPreview::No);
1894 }
1895 
1896 void MainWindow::printPreview()
1897 {
1898     d->print(Private::ShowPreview::Yes);
1899 }
1900 
1901 void MainWindow::preloadNextUrl()
1902 {
1903     static bool disablePreload = qgetenv("GV_MAX_UNREFERENCED_IMAGES") == "0";
1904     if (disablePreload) {
1905         qCDebug(GWENVIEW_APP_LOG) << "Preloading disabled";
1906         return;
1907     }
1908     QItemSelection selection = d->mContextManager->selectionModel()->selection();
1909     if (selection.size() != 1) {
1910         return;
1911     }
1912 
1913     QModelIndexList indexList = selection.indexes();
1914     if (indexList.isEmpty()) {
1915         return;
1916     }
1917 
1918     QModelIndex index = indexList.at(0);
1919     if (!index.isValid()) {
1920         return;
1921     }
1922 
1923     if (d->mCurrentMainPageId == ViewMainPageId) {
1924         // If we are in view mode, preload the next url, otherwise preload the
1925         // selected one
1926         int offset = d->mPreloadDirectionIsForward ? 1 : -1;
1927         index = d->mDirModel->sibling(index.row() + offset, index.column(), index);
1928         if (!index.isValid()) {
1929             return;
1930         }
1931     }
1932 
1933     KFileItem item = d->mDirModel->itemForIndex(index);
1934     if (!ArchiveUtils::fileItemIsDirOrArchive(item)) {
1935         QUrl url = item.url();
1936         if (url.isLocalFile()) {
1937             QSize size = d->mViewStackedWidget->size();
1938             d->mPreloader->preload(url, size);
1939         }
1940     }
1941 }
1942 
1943 // Set a sane initial window size
1944 QSize MainWindow::sizeHint() const
1945 {
1946     return KXmlGuiWindow::sizeHint().expandedTo(QSize(1020, 700));
1947 }
1948 
1949 void MainWindow::showEvent(QShowEvent *event)
1950 {
1951     // We need to delay initializing the action state until the menu bar has
1952     // been initialized, that's why it's done only in the showEvent()
1953     if (GwenviewConfig::lastUsedVersion() == -1 && toolBar()->actions().contains(d->mHamburgerMenu)) {
1954         menuBar()->hide();
1955     }
1956     d->mShowMenuBarAction->setChecked(menuBar()->isVisible());
1957     KXmlGuiWindow::showEvent(event);
1958 }
1959 
1960 void MainWindow::resizeEvent(QResizeEvent *event)
1961 {
1962     KXmlGuiWindow::resizeEvent(event);
1963     // This is a small hack to execute code after leaving fullscreen, and after
1964     // the window has been resized back to its former size.
1965     if (d->mFullScreenLeftAt.isValid() && d->mFullScreenLeftAt.secsTo(QDateTime::currentDateTime()) < 2) {
1966         if (d->mCurrentMainPageId == BrowseMainPageId) {
1967             d->mThumbnailView->scrollToSelectedIndex();
1968         }
1969         d->mFullScreenLeftAt = QDateTime();
1970     }
1971 }
1972 
1973 bool MainWindow::eventFilter(QObject *obj, QEvent *event)
1974 {
1975     Q_UNUSED(obj);
1976     Q_UNUSED(event);
1977 #ifdef Q_OS_OSX
1978     /**
1979      * handle Mac OS X file open events (only exist on OS X)
1980      */
1981     if (event->type() == QEvent::FileOpen) {
1982         QFileOpenEvent *fileOpenEvent = static_cast<QFileOpenEvent *>(event);
1983         openUrl(fileOpenEvent->url());
1984         return true;
1985     }
1986 #endif
1987     if (obj == d->mThumbnailView->viewport()) {
1988         switch (event->type()) {
1989         case QEvent::MouseButtonPress:
1990         case QEvent::MouseButtonDblClick:
1991             mouseButtonNavigate(static_cast<QMouseEvent *>(event));
1992             break;
1993         default:;
1994         }
1995     }
1996     return false;
1997 }
1998 
1999 void MainWindow::mousePressEvent(QMouseEvent *event)
2000 {
2001     mouseButtonNavigate(event);
2002     KXmlGuiWindow::mousePressEvent(event);
2003 }
2004 
2005 void MainWindow::mouseDoubleClickEvent(QMouseEvent *event)
2006 {
2007     mouseButtonNavigate(event);
2008     KXmlGuiWindow::mouseDoubleClickEvent(event);
2009 }
2010 
2011 void MainWindow::mouseButtonNavigate(QMouseEvent *event)
2012 {
2013     switch (event->button()) {
2014     case Qt::ForwardButton:
2015         if (d->mGoToNextAction->isEnabled()) {
2016             d->mGoToNextAction->trigger();
2017             return;
2018         }
2019         break;
2020     case Qt::BackButton:
2021         if (d->mGoToPreviousAction->isEnabled()) {
2022             d->mGoToPreviousAction->trigger();
2023             return;
2024         }
2025         break;
2026     default:;
2027     }
2028 }
2029 
2030 void MainWindow::setDistractionFreeMode(bool value)
2031 {
2032     d->mFullScreenContent->setDistractionFreeMode(value);
2033 }
2034 
2035 void MainWindow::saveProperties(KConfigGroup &group)
2036 {
2037     group.writeEntry(SESSION_CURRENT_PAGE_KEY, int(d->mCurrentMainPageId));
2038     group.writeEntry(SESSION_URL_KEY, d->mContextManager->currentUrl().toString());
2039 }
2040 
2041 void MainWindow::readProperties(const KConfigGroup &group)
2042 {
2043     const QUrl url = group.readEntry(SESSION_URL_KEY, QUrl());
2044     if (url.isValid()) {
2045         goToUrl(url);
2046     }
2047 
2048     MainPageId pageId = MainPageId(group.readEntry(SESSION_CURRENT_PAGE_KEY, int(StartMainPageId)));
2049     if (pageId == StartMainPageId) {
2050         showStartMainPage();
2051     } else if (pageId == BrowseMainPageId) {
2052         d->mBrowseAction->trigger();
2053     } else {
2054         d->mViewAction->trigger();
2055     }
2056 }
2057 
2058 void MainWindow::showFirstDocumentReached()
2059 {
2060     if (d->hudButtonBox || d->mCurrentMainPageId != ViewMainPageId) {
2061         return;
2062     }
2063     d->hudButtonBox = new HudButtonBox;
2064     d->hudButtonBox->setText(i18n("You reached the first document, what do you want to do?"));
2065     d->hudButtonBox->addButton(i18n("Stay There"));
2066     d->hudButtonBox->addAction(d->mGoToLastAction, i18n("Go to the Last Document"));
2067     d->hudButtonBox->addAction(d->mBrowseAction, i18n("Go Back to the Document List"));
2068     d->hudButtonBox->addCountDown(15000);
2069     d->mViewMainPage->showMessageWidget(d->hudButtonBox, Qt::AlignCenter);
2070 }
2071 
2072 void MainWindow::showLastDocumentReached()
2073 {
2074     if (d->hudButtonBox || d->mCurrentMainPageId != ViewMainPageId) {
2075         return;
2076     }
2077     d->hudButtonBox = new HudButtonBox;
2078     d->hudButtonBox->setText(i18n("You reached the last document, what do you want to do?"));
2079     d->hudButtonBox->addButton(i18n("Stay There"));
2080     d->hudButtonBox->addAction(d->mGoToFirstAction, i18n("Go to the First Document"));
2081     d->hudButtonBox->addAction(d->mBrowseAction, i18n("Go Back to the Document List"));
2082     d->hudButtonBox->addCountDown(15000);
2083     d->mViewMainPage->showMessageWidget(d->hudButtonBox, Qt::AlignCenter);
2084 }
2085 
2086 void MainWindow::replaceLocation()
2087 {
2088     QLineEdit *lineEdit = d->mUrlNavigator->editor()->lineEdit();
2089 
2090     // If the text field currently has focus and everything is selected,
2091     // pressing the keyboard shortcut returns the whole thing to breadcrumb mode
2092     if (d->mUrlNavigator->isUrlEditable() && lineEdit->hasFocus() && lineEdit->selectedText() == lineEdit->text()) {
2093         d->mUrlNavigator->setUrlEditable(false);
2094     } else {
2095         d->mUrlNavigator->setUrlEditable(true);
2096         d->mUrlNavigator->setFocus();
2097         lineEdit->selectAll();
2098     }
2099 }
2100 
2101 void MainWindow::onFocusChanged(QWidget *old, QWidget *now)
2102 {
2103     if (old == nullptr) {
2104         if (now != nullptr && isFullScreen()) {
2105             d->setScreenSaverEnabled(false);
2106         }
2107     } else {
2108         if (now == nullptr) {
2109             d->setScreenSaverEnabled(true);
2110         }
2111     }
2112 }
2113 
2114 } // namespace
2115 
2116 #include "moc_mainwindow.cpp"