File indexing completed on 2024-05-05 04:19:20

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 "viewmainpage.h"
0021 #include "config-gwenview.h"
0022 
0023 // Qt
0024 #include <QApplication>
0025 #include <QCheckBox>
0026 #include <QItemSelectionModel>
0027 #include <QMenu>
0028 #include <QShortcut>
0029 #include <QVBoxLayout>
0030 
0031 // KF
0032 #include <KActionCategory>
0033 #include <KActionCollection>
0034 #include <KLocalizedString>
0035 #include <KMessageBox>
0036 #include <KModelIndexProxyMapper>
0037 #include <KSqueezedTextLabel>
0038 #include <KToggleAction>
0039 #if HAVE_KACTIVITIES
0040 #include <PlasmaActivities/ResourceInstance>
0041 #endif
0042 
0043 // Local
0044 #include "fileoperations.h"
0045 #include "gwenview_app_debug.h"
0046 #include "splitter.h"
0047 #include "spotlightmode.h"
0048 #include <gvcore.h>
0049 #include <lib/documentview/abstractdocumentviewadapter.h>
0050 #include <lib/documentview/abstractrasterimageviewtool.h>
0051 #include <lib/documentview/documentview.h>
0052 #include <lib/documentview/documentviewcontainer.h>
0053 #include <lib/documentview/documentviewcontroller.h>
0054 #include <lib/documentview/documentviewsynchronizer.h>
0055 #include <lib/fullscreenbar.h>
0056 #include <lib/gvdebug.h>
0057 #include <lib/gwenviewconfig.h>
0058 #include <lib/mimetypeutils.h>
0059 #include <lib/paintutils.h>
0060 #include <lib/semanticinfo/sorteddirmodel.h>
0061 #include <lib/slidecontainer.h>
0062 #include <lib/slideshow.h>
0063 #include <lib/statusbartoolbutton.h>
0064 #include <lib/stylesheetutils.h>
0065 #include <lib/thumbnailview/thumbnailbarview.h>
0066 #include <lib/zoommode.h>
0067 #include <lib/zoomwidget.h>
0068 
0069 namespace Gwenview
0070 {
0071 #undef ENABLE_LOG
0072 #undef LOG
0073 // #define ENABLE_LOG
0074 #ifdef ENABLE_LOG
0075 #define LOG(x) qCDebug(GWENVIEW_APP_LOG) << x
0076 #else
0077 #define LOG(x) ;
0078 #endif
0079 
0080 const int ViewMainPage::MaxViewCount = 6;
0081 
0082 /*
0083  * Layout of mThumbnailSplitter is:
0084  *
0085  * +-mThumbnailSplitter------------------------------------------------+
0086  * |+-mAdapterContainer-----------------------------------------------+|
0087  * ||+-mDocumentViewContainer----------------------------------------+||
0088  * |||+-DocumentView----------------++-DocumentView-----------------+|||
0089  * ||||                             ||                              ||||
0090  * ||||                             ||                              ||||
0091  * ||||                             ||                              ||||
0092  * ||||                             ||                              ||||
0093  * ||||                             ||                              ||||
0094  * ||||                             ||                              ||||
0095  * |||+-----------------------------++------------------------------+|||
0096  * ||+---------------------------------------------------------------+||
0097  * ||+-mToolContainer------------------------------------------------+||
0098  * |||                                                               |||
0099  * ||+---------------------------------------------------------------+||
0100  * |+-----------------------------------------------------------------+|
0101  * |===================================================================|
0102  * |+-mThumbnailBar---------------------------------------------------+|
0103  * ||                                                                 ||
0104  * ||                                                                 ||
0105  * |+-mStatusBarContainer---------------------------------------------+|
0106  * ||[mToggleSideBarButton][mToggleThumbnailBarButton]   [mZoomWidget]||
0107  * |+-----------------------------------------------------------------+|
0108  * +-------------------------------------------------------------------+
0109  */
0110 struct ViewMainPagePrivate {
0111     ViewMainPage *q = nullptr;
0112     SlideShow *mSlideShow = nullptr;
0113     KActionCollection *mActionCollection = nullptr;
0114     GvCore *mGvCore = nullptr;
0115     KModelIndexProxyMapper *mDirModelToBarModelProxyMapper = nullptr;
0116     QSplitter *mThumbnailSplitter = nullptr;
0117     QWidget *mAdapterContainer = nullptr;
0118     DocumentViewController *mDocumentViewController = nullptr;
0119     QList<DocumentView *> mDocumentViews;
0120     DocumentViewSynchronizer *mSynchronizer = nullptr;
0121     QToolButton *mToggleSideBarButton = nullptr;
0122     QToolButton *mToggleThumbnailBarButton = nullptr;
0123     ZoomWidget *mZoomWidget = nullptr;
0124     DocumentViewContainer *mDocumentViewContainer = nullptr;
0125     SlideContainer *mToolContainer = nullptr;
0126     QWidget *mStatusBarContainer = nullptr;
0127     ThumbnailBarView *mThumbnailBar = nullptr;
0128     KToggleAction *mToggleThumbnailBarAction = nullptr;
0129     KToggleAction *mSynchronizeAction = nullptr;
0130     QCheckBox *mSynchronizeCheckBox = nullptr;
0131     KSqueezedTextLabel *mDocumentCountLabel = nullptr;
0132     SpotlightMode *mSpotlightMode = nullptr;
0133 
0134     bool mCompareMode;
0135     ZoomMode::Enum mZoomMode;
0136 
0137     void setupWidgets()
0138     {
0139         mToolContainer = new SlideContainer;
0140         mToolContainer->setAutoFillBackground(true);
0141         mToolContainer->setBackgroundRole(QPalette::Mid);
0142         //--
0143         mStatusBarContainer = new QWidget;
0144         mStatusBarContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
0145         mToggleSideBarButton = new StatusBarToolButton;
0146         mToggleThumbnailBarButton = new StatusBarToolButton;
0147         mZoomWidget = new ZoomWidget;
0148         mSynchronizeCheckBox = new QCheckBox(i18n("Synchronize"));
0149         mSynchronizeCheckBox->hide();
0150         mDocumentCountLabel = new KSqueezedTextLabel;
0151         mDocumentCountLabel->setAlignment(Qt::AlignCenter);
0152         mDocumentCountLabel->setTextElideMode(Qt::ElideRight);
0153         QMargins labelMargins = mDocumentCountLabel->contentsMargins();
0154         labelMargins.setLeft(15);
0155         labelMargins.setRight(15);
0156         mDocumentCountLabel->setContentsMargins(labelMargins);
0157 
0158         auto statusBarContainerLayout = new QHBoxLayout(mStatusBarContainer);
0159         // Use toolbar-like margins and spacing
0160         int margins = q->style()->pixelMetric(QStyle::PM_ToolBarItemMargin) + q->style()->pixelMetric(QStyle::PM_ToolBarFrameWidth);
0161         statusBarContainerLayout->setContentsMargins(margins, margins, margins, margins);
0162         statusBarContainerLayout->setSpacing(q->style()->pixelMetric(QStyle::PM_ToolBarItemSpacing));
0163         statusBarContainerLayout->addWidget(mToggleSideBarButton);
0164         statusBarContainerLayout->addWidget(mToggleThumbnailBarButton);
0165         statusBarContainerLayout->addStretch();
0166         statusBarContainerLayout->addWidget(mSynchronizeCheckBox);
0167         // Ensure document count label takes up all available space,
0168         // so its autohide feature works properly (stretch factor = 1)
0169         statusBarContainerLayout->addWidget(mDocumentCountLabel, 1);
0170         statusBarContainerLayout->addStretch();
0171         statusBarContainerLayout->addWidget(mZoomWidget);
0172         //--
0173         mAdapterContainer = new QWidget;
0174 
0175         auto adapterContainerLayout = new QVBoxLayout(mAdapterContainer);
0176         adapterContainerLayout->setContentsMargins(0, 0, 0, 0);
0177         adapterContainerLayout->setSpacing(0);
0178         mDocumentViewContainer = new DocumentViewContainer;
0179         mDocumentViewContainer->setAutoFillBackground(true);
0180         mDocumentViewContainer->setBackgroundRole(QPalette::Base);
0181         adapterContainerLayout->addWidget(mDocumentViewContainer);
0182         adapterContainerLayout->addWidget(mToolContainer);
0183         //--
0184         mSpotlightMode = new SpotlightMode(mDocumentViewContainer, mActionCollection);
0185         adapterContainerLayout->addWidget(mDocumentViewContainer);
0186         adapterContainerLayout->addWidget(mToolContainer);
0187         //--
0188         mThumbnailBar = new ThumbnailBarView;
0189         auto delegate = new ThumbnailBarItemDelegate(mThumbnailBar);
0190         mThumbnailBar->setItemDelegate(delegate);
0191         mThumbnailBar->setSelectionMode(QAbstractItemView::ExtendedSelection);
0192         //--
0193         Qt::Orientation orientation = GwenviewConfig::thumbnailBarOrientation();
0194         mThumbnailSplitter = new Splitter(orientation == Qt::Horizontal ? Qt::Vertical : Qt::Horizontal, q);
0195         mThumbnailBar->setOrientation(orientation);
0196         mThumbnailBar->setThumbnailAspectRatio(GwenviewConfig::thumbnailAspectRatio());
0197         mThumbnailBar->setRowCount(GwenviewConfig::thumbnailBarRowCount());
0198         mThumbnailSplitter->addWidget(mAdapterContainer);
0199         mThumbnailSplitter->addWidget(mThumbnailBar);
0200         mThumbnailSplitter->setSizes(GwenviewConfig::thumbnailSplitterSizes());
0201         // Show the thumbnail bar after setting the parent to avoid recreating
0202         // the native window and to avoid QTBUG-87345.
0203         mThumbnailBar->setVisible(GwenviewConfig::thumbnailBarIsVisible());
0204         mThumbnailBar->installEventFilter(q);
0205 
0206         auto viewMainPageLayout = new QVBoxLayout(q);
0207         viewMainPageLayout->setContentsMargins(0, 0, 0, 0);
0208         viewMainPageLayout->setSpacing(0);
0209         viewMainPageLayout->addWidget(mThumbnailSplitter);
0210         viewMainPageLayout->addWidget(mStatusBarContainer);
0211         //--
0212         mDocumentViewController = new DocumentViewController(mActionCollection, q);
0213         mDocumentViewController->setZoomWidget(mZoomWidget);
0214         mDocumentViewController->setToolContainer(mToolContainer);
0215         mSynchronizer = new DocumentViewSynchronizer(&mDocumentViews, q);
0216     }
0217 
0218     void setupThumbnailBarStyleSheet()
0219     {
0220         QPalette pal = mGvCore->palette(GvCore::NormalViewPalette);
0221         mThumbnailBar->setPalette(pal);
0222         Qt::Orientation orientation = mThumbnailBar->orientation();
0223         QColor bgColor = pal.color(QPalette::Normal, QPalette::Base);
0224         QColor bgSelColor = pal.color(QPalette::Normal, QPalette::Highlight);
0225         QColor bgHovColor = pal.color(QPalette::Normal, QPalette::Highlight);
0226 
0227         // Avoid dark and bright colors
0228         bgColor.setHsv(bgColor.hue(), bgColor.saturation(), (127 + 3 * bgColor.value()) / 4);
0229 
0230         // Hover uses lighter/faded version of select color. Combine with bgColor to adapt to different backgrounds
0231         bgHovColor.setHsv(bgHovColor.hue(), (bgHovColor.saturation() / 2), ((bgHovColor.value() + bgColor.value()) / 2));
0232 
0233         QColor leftBorderColor = PaintUtils::adjustedHsv(bgColor, 0, 0, qMin(20, 255 - bgColor.value()));
0234         QColor rightBorderColor = PaintUtils::adjustedHsv(bgColor, 0, 0, -qMin(40, bgColor.value()));
0235         QColor borderSelColor = PaintUtils::adjustedHsv(bgSelColor, 0, 0, -qMin(60, bgSelColor.value()));
0236 
0237         QString itemCss =
0238             "QListView::item {"
0239             "   background-color: %1;"
0240             "   border-left: 1px solid %2;"
0241             "   border-right: 1px solid %3;"
0242             "}";
0243         itemCss = itemCss.arg(StyleSheetUtils::gradient(orientation, bgColor, 46),
0244                               StyleSheetUtils::gradient(orientation, leftBorderColor, 36),
0245                               StyleSheetUtils::gradient(orientation, rightBorderColor, 26));
0246 
0247         QString itemSelCss =
0248             "QListView::item:selected {"
0249             "   background-color: %1;"
0250             "   border-left: 1px solid %2;"
0251             "   border-right: 1px solid %2;"
0252             "}";
0253         itemSelCss = itemSelCss.arg(StyleSheetUtils::gradient(orientation, bgSelColor, 56), StyleSheetUtils::rgba(borderSelColor));
0254 
0255         QString itemHovCss =
0256             "QListView::item:hover:!selected {"
0257             "  background-color: %1;"
0258             "  border-left: 1px solid %2;"
0259             "  border-right: 1px solid %3;"
0260             "}";
0261         itemHovCss = itemHovCss.arg(StyleSheetUtils::gradient(orientation, bgHovColor, 56),
0262                                     StyleSheetUtils::rgba(leftBorderColor),
0263                                     StyleSheetUtils::rgba(rightBorderColor));
0264 
0265         QString css = itemCss + itemSelCss + itemHovCss;
0266         if (orientation == Qt::Vertical) {
0267             css.replace(QLatin1String("left"), QLatin1String("top")).replace(QLatin1String("right"), QLatin1String("bottom"));
0268         }
0269 
0270         mThumbnailBar->setStyleSheet(css);
0271     }
0272 
0273     DocumentView *createDocumentView()
0274     {
0275         DocumentView *view = mDocumentViewContainer->createView();
0276 
0277         // Connect context menu
0278         // If you need to connect another view signal, make sure it is disconnected in deleteDocumentView
0279         QObject::connect(view, &DocumentView::contextMenuRequested, q, &ViewMainPage::showContextMenu);
0280 
0281         QObject::connect(view, &DocumentView::completed, q, &ViewMainPage::completed);
0282         QObject::connect(view, &DocumentView::previousImageRequested, q, &ViewMainPage::previousImageRequested);
0283         QObject::connect(view, &DocumentView::nextImageRequested, q, &ViewMainPage::nextImageRequested);
0284         QObject::connect(view, &DocumentView::openUrlRequested, q, &ViewMainPage::openUrlRequested);
0285         QObject::connect(view, &DocumentView::openDirUrlRequested, q, &ViewMainPage::openDirUrlRequested);
0286         QObject::connect(view, &DocumentView::captionUpdateRequested, q, &ViewMainPage::captionUpdateRequested);
0287         QObject::connect(view, &DocumentView::toggleFullScreenRequested, q, &ViewMainPage::toggleFullScreenRequested);
0288         QObject::connect(view, &DocumentView::focused, q, &ViewMainPage::slotViewFocused);
0289         QObject::connect(view, &DocumentView::hudTrashClicked, q, &ViewMainPage::trashView);
0290         QObject::connect(view, &DocumentView::hudDeselectClicked, q, &ViewMainPage::deselectView);
0291 
0292         QObject::connect(view, &DocumentView::videoFinished, mSlideShow, &SlideShow::resumeAndGoToNextUrl);
0293 
0294         mDocumentViews << view;
0295 
0296         return view;
0297     }
0298 
0299     void deleteDocumentView(DocumentView *view)
0300     {
0301         if (mDocumentViewController->view() == view) {
0302             mDocumentViewController->setView(nullptr);
0303         }
0304 
0305         // Make sure we do not get notified about this view while it is going away.
0306         // mDocumentViewController->deleteView() animates the view deletion so
0307         // the view still exists for a short while when we come back to the
0308         // event loop)
0309         QObject::disconnect(view, nullptr, q, nullptr);
0310         QObject::disconnect(view, nullptr, mSlideShow, nullptr);
0311 
0312         mDocumentViews.removeOne(view);
0313         mDocumentViewContainer->deleteView(view);
0314     }
0315 
0316     void saveSplitterConfig()
0317     {
0318         if (mThumbnailBar->isVisible()) {
0319             if (mThumbnailSplitter->sizes().constLast() > 0) {
0320                 GwenviewConfig::setThumbnailSplitterSizes(mThumbnailSplitter->sizes());
0321             } else {
0322                 // mThumbnailBar's size has been collapsed to 0. We reset the sizes to default so users can't "lose" the bar.
0323                 const bool didUseDefaults = GwenviewConfig::self()->useDefaults(true);
0324                 const auto defaultSplitterSizes = GwenviewConfig::thumbnailSplitterSizes();
0325                 GwenviewConfig::self()->useDefaults(didUseDefaults);
0326                 GwenviewConfig::setThumbnailSplitterSizes(defaultSplitterSizes);
0327             }
0328         }
0329     }
0330 
0331     DocumentView *currentView() const
0332     {
0333         return mDocumentViewController->view();
0334     }
0335 
0336     void setCurrentView(DocumentView *view)
0337     {
0338         DocumentView *oldView = currentView();
0339         if (view == oldView) {
0340             return;
0341         }
0342         if (oldView) {
0343             oldView->setCurrent(false);
0344         }
0345         view->setCurrent(true);
0346         mDocumentViewController->setView(view);
0347         mSynchronizer->setCurrentView(view);
0348 
0349         QModelIndex index = indexForView(view);
0350         if (index.isValid()) {
0351             // Index may be invalid when Gwenview is started as
0352             // `gwenview /foo/image.png` because in this situation it loads image.png
0353             // *before* listing /foo (because it matters less to the user)
0354             mThumbnailBar->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Current);
0355         }
0356         QObject::connect(view, &DocumentView::currentToolChanged, q, &ViewMainPage::updateFocus);
0357     }
0358 
0359     QModelIndex indexForView(DocumentView *view) const
0360     {
0361         QUrl url = view->url();
0362         if (!url.isValid()) {
0363             qCWarning(GWENVIEW_APP_LOG) << "View does not display any document!";
0364             return {};
0365         }
0366 
0367         SortedDirModel *dirModel = mGvCore->sortedDirModel();
0368         QModelIndex srcIndex = dirModel->indexForUrl(url);
0369         if (!mDirModelToBarModelProxyMapper) {
0370             // Delay the initialization of the mapper to its first use because
0371             // mThumbnailBar->model() is not set after ViewMainPage ctor is
0372             // done.
0373             const_cast<ViewMainPagePrivate *>(this)->mDirModelToBarModelProxyMapper = new KModelIndexProxyMapper(dirModel, mThumbnailBar->model(), q);
0374         }
0375         QModelIndex index = mDirModelToBarModelProxyMapper->mapLeftToRight(srcIndex);
0376         return index;
0377     }
0378 
0379     void applyPalette(bool fullScreenMode)
0380     {
0381         mDocumentViewContainer->applyPalette(mGvCore->palette(fullScreenMode ? GvCore::FullScreenViewPalette : GvCore::NormalViewPalette));
0382         setupThumbnailBarStyleSheet();
0383     }
0384 
0385     void updateDocumentCountLabel()
0386     {
0387         const int current = mThumbnailBar->currentIndex().row() + 1; // zero-based
0388         const int total = mThumbnailBar->model()->rowCount();
0389         const QString text = i18nc("@info:status %1 current document index, %2 total documents", "%1 of %2", current, total);
0390         mDocumentCountLabel->setText(text);
0391     }
0392 };
0393 
0394 ViewMainPage::ViewMainPage(QWidget *parent, SlideShow *slideShow, KActionCollection *actionCollection, GvCore *gvCore)
0395     : QWidget(parent)
0396     , d(new ViewMainPagePrivate)
0397 {
0398     d->q = this;
0399     d->mDirModelToBarModelProxyMapper = nullptr; // Initialized later
0400     d->mSlideShow = slideShow;
0401     d->mActionCollection = actionCollection;
0402     d->mGvCore = gvCore;
0403     d->mCompareMode = false;
0404 
0405     auto enterKeyShortcut = new QShortcut(Qt::Key_Return, this);
0406     connect(enterKeyShortcut, &QShortcut::activated, this, &ViewMainPage::slotEnterPressed);
0407 
0408     d->setupWidgets();
0409 
0410     auto view = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "View"), actionCollection);
0411 
0412     d->mToggleThumbnailBarAction = view->add<KToggleAction>(QStringLiteral("toggle_thumbnailbar"));
0413     d->mToggleThumbnailBarAction->setText(i18n("Show Thumbnails"));
0414     // clang-format off
0415     // i18n: For languages that are read right to left, "right side" refers to the left side in this context.
0416     d->mToggleThumbnailBarAction->setWhatsThis(xi18nc("@info:whatsthis", "<para>This toggles a small bar showing all the other images in the current folder. "
0417         "Selecting one of them changes the currently viewed image.</para><para>Select multiple images at the same time to see them side by side.</para>"
0418         "<para>The bar can optionally be moved to the right side of the screen and contain multiple rows of images; <link url='%1'>configure these options in the settings</link>.</para>", QStringLiteral("gwenview:/config/imageview")));
0419         // Keep the previous link address in sync with MainWindow::Private::SettingsOpenerHelper::eventFilter().
0420     // clang-format on
0421     actionCollection->setDefaultShortcut(d->mToggleThumbnailBarAction, Qt::CTRL | Qt::Key_B);
0422     connect(d->mToggleThumbnailBarAction, &KToggleAction::triggered, this, &ViewMainPage::setThumbnailBarVisibility);
0423     d->mToggleThumbnailBarButton->setDefaultAction(d->mToggleThumbnailBarAction);
0424     connect(d->mThumbnailSplitter, &QSplitter::splitterMoved, this, [this]() {
0425         // The splitter can be moved until the bar has a size of zero which makes it invisible.
0426         const bool isThumbnailBarVisible = d->mThumbnailSplitter->sizes().constLast() > 0;
0427         GwenviewConfig::setThumbnailBarIsVisible(isThumbnailBarVisible);
0428         d->mToggleThumbnailBarAction->setChecked(isThumbnailBarVisible);
0429     });
0430 
0431     d->mSynchronizeAction = view->add<KToggleAction>(QStringLiteral("synchronize_views"));
0432     d->mSynchronizeAction->setText(i18n("Synchronize"));
0433     actionCollection->setDefaultShortcut(d->mSynchronizeAction, Qt::CTRL | Qt::Key_Y);
0434     connect(d->mSynchronizeAction, &QAction::toggled, d->mSynchronizer, &DocumentViewSynchronizer::setActive);
0435     // Ensure mSynchronizeAction and mSynchronizeCheckBox are in sync
0436     connect(d->mSynchronizeAction, &QAction::toggled, d->mSynchronizeCheckBox, &QAbstractButton::setChecked);
0437     connect(d->mSynchronizeCheckBox, &QAbstractButton::toggled, d->mSynchronizeAction, &QAction::setChecked);
0438 
0439     // Connections for the document count
0440     connect(d->mThumbnailBar, &ThumbnailBarView::rowsInsertedSignal, this, &ViewMainPage::slotDirModelItemsAddedOrRemoved);
0441     connect(d->mThumbnailBar, &ThumbnailBarView::rowsRemovedSignal, this, &ViewMainPage::slotDirModelItemsAddedOrRemoved);
0442 
0443     connect(qApp, &QApplication::paletteChanged, this, [this]() {
0444         d->applyPalette(window()->isFullScreen());
0445     });
0446 
0447     installEventFilter(this);
0448 }
0449 
0450 ViewMainPage::~ViewMainPage()
0451 {
0452     delete d;
0453 }
0454 
0455 void ViewMainPage::loadConfig()
0456 {
0457     d->applyPalette(window()->isFullScreen());
0458 
0459     // FIXME: Not symmetric with saveConfig(). Check if it matters.
0460     for (DocumentView *view : qAsConst(d->mDocumentViews)) {
0461         view->loadAdapterConfig();
0462     }
0463 
0464     Qt::Orientation orientation = GwenviewConfig::thumbnailBarOrientation();
0465     d->mThumbnailSplitter->setOrientation(orientation == Qt::Horizontal ? Qt::Vertical : Qt::Horizontal);
0466     d->mThumbnailBar->setOrientation(orientation);
0467     d->setupThumbnailBarStyleSheet();
0468     d->mThumbnailBar->setVisible(GwenviewConfig::thumbnailBarIsVisible());
0469     d->mToggleThumbnailBarAction->setChecked(GwenviewConfig::thumbnailBarIsVisible());
0470 
0471     int oldRowCount = d->mThumbnailBar->rowCount();
0472     int newRowCount = GwenviewConfig::thumbnailBarRowCount();
0473     if (oldRowCount != newRowCount) {
0474         d->mThumbnailBar->setUpdatesEnabled(false);
0475         int gridSize = d->mThumbnailBar->gridSize().width();
0476 
0477         d->mThumbnailBar->setRowCount(newRowCount);
0478 
0479         // Adjust splitter to ensure thumbnail size remains the same
0480         int delta = (newRowCount - oldRowCount) * gridSize;
0481         QList<int> sizes = d->mThumbnailSplitter->sizes();
0482         Q_ASSERT(sizes.count() == 2);
0483         sizes[0] -= delta;
0484         sizes[1] += delta;
0485         d->mThumbnailSplitter->setSizes(sizes);
0486 
0487         d->mThumbnailBar->setUpdatesEnabled(true);
0488     }
0489 
0490     d->mZoomMode = GwenviewConfig::zoomMode();
0491     if (GwenviewConfig::spotlightMode() != d->mActionCollection->action(QStringLiteral("view_toggle_spotlightmode"))->isChecked())
0492         if (parentWidget()->isVisible())
0493             d->mActionCollection->action(QStringLiteral("view_toggle_spotlightmode"))->trigger();
0494 }
0495 
0496 void ViewMainPage::saveConfig()
0497 {
0498     d->saveSplitterConfig();
0499     GwenviewConfig::setThumbnailBarIsVisible(d->mToggleThumbnailBarAction->isChecked());
0500 }
0501 
0502 void ViewMainPage::setThumbnailBarVisibility(bool visible)
0503 {
0504     d->saveSplitterConfig();
0505     d->mThumbnailBar->setVisible(visible);
0506     if (visible && d->mThumbnailSplitter->sizes().constLast() <= 0) {
0507         // mThumbnailBar is supposed to be made visible but its splitter has a size of 0 making it invisible.
0508         // We load the last good sizes from the config.
0509         d->mThumbnailSplitter->setSizes(GwenviewConfig::thumbnailSplitterSizes());
0510     }
0511 }
0512 
0513 int ViewMainPage::statusBarHeight() const
0514 {
0515     return d->mStatusBarContainer->height();
0516 }
0517 
0518 void ViewMainPage::setStatusBarVisible(bool visible)
0519 {
0520     d->mStatusBarContainer->setVisible(visible);
0521 }
0522 
0523 void ViewMainPage::setFullScreenMode(bool fullScreenMode)
0524 {
0525     if (fullScreenMode) {
0526         d->mThumbnailBar->setVisible(false);
0527     } else {
0528         d->mThumbnailBar->setVisible(d->mToggleThumbnailBarAction->isChecked());
0529     }
0530     d->applyPalette(fullScreenMode);
0531     d->mToggleThumbnailBarAction->setEnabled(!fullScreenMode);
0532 }
0533 
0534 ThumbnailBarView *ViewMainPage::thumbnailBar() const
0535 {
0536     return d->mThumbnailBar;
0537 }
0538 
0539 inline void addActionToMenu(QMenu *menu, KActionCollection *actionCollection, const char *name)
0540 {
0541     QAction *action = actionCollection->action(name);
0542     if (action) {
0543         menu->addAction(action);
0544     }
0545 }
0546 
0547 inline void addActionToMenu(QMenu *menu, KActionCollection *actionCollection, const QString &name)
0548 {
0549     QAction *action = actionCollection->action(name);
0550     if (action) {
0551         menu->addAction(action);
0552     }
0553 }
0554 
0555 void ViewMainPage::showContextMenu()
0556 {
0557     QMenu menu(this);
0558     addActionToMenu(&menu, d->mActionCollection, "fullscreen");
0559     menu.addSeparator();
0560     addActionToMenu(&menu, d->mActionCollection, "go_previous");
0561     addActionToMenu(&menu, d->mActionCollection, "go_next");
0562     if (d->currentView()->canZoom()) {
0563         menu.addSeparator();
0564         addActionToMenu(&menu, d->mActionCollection, "view_actual_size");
0565         addActionToMenu(&menu, d->mActionCollection, "view_zoom_to_fit");
0566         addActionToMenu(&menu, d->mActionCollection, "view_zoom_in");
0567         addActionToMenu(&menu, d->mActionCollection, "view_zoom_out");
0568         addActionToMenu(&menu, d->mActionCollection, "view_toggle_birdeyeview");
0569     }
0570     menu.addSeparator();
0571     QMenu backgroundColorModeMenu(i18nc("@item:inmenu", "Background Color Mode"), this);
0572     addActionToMenu(&backgroundColorModeMenu, d->mActionCollection, "view_background_colormode_auto");
0573     addActionToMenu(&backgroundColorModeMenu, d->mActionCollection, "view_background_colormode_light");
0574     addActionToMenu(&backgroundColorModeMenu, d->mActionCollection, "view_background_colormode_neutral");
0575     addActionToMenu(&backgroundColorModeMenu, d->mActionCollection, "view_background_colormode_dark");
0576     backgroundColorModeMenu.setIcon(backgroundColorModeMenu.actions().at(0)->icon());
0577     menu.addMenu(&backgroundColorModeMenu);
0578     if (d->mCompareMode) {
0579         menu.addSeparator();
0580         addActionToMenu(&menu, d->mActionCollection, "synchronize_views");
0581     }
0582 
0583     menu.addSeparator();
0584     addActionToMenu(&menu, d->mActionCollection, KStandardAction::name(KStandardAction::Copy));
0585     addActionToMenu(&menu, d->mActionCollection, "file_copy_to");
0586     addActionToMenu(&menu, d->mActionCollection, "file_move_to");
0587     addActionToMenu(&menu, d->mActionCollection, "file_link_to");
0588     menu.addSeparator();
0589     addActionToMenu(&menu, d->mActionCollection, "file_open_in_new_window");
0590     addActionToMenu(&menu, d->mActionCollection, "file_open_with");
0591     addActionToMenu(&menu, d->mActionCollection, "file_open_containing_folder");
0592 
0593     menu.exec(QCursor::pos());
0594 }
0595 
0596 QSize ViewMainPage::sizeHint() const
0597 {
0598     return QSize(400, 300);
0599 }
0600 
0601 QSize ViewMainPage::minimumSizeHint() const
0602 {
0603     if (!layout()) {
0604         return QSize();
0605     }
0606 
0607     QSize minimumSize = layout()->minimumSize();
0608     if (window()->isFullScreen()) {
0609         // Check minimum width of the overlay fullscreen bar
0610         // since there is no layout link which could do this
0611         const FullScreenBar *fullScreenBar = findChild<FullScreenBar *>();
0612         if (fullScreenBar && fullScreenBar->layout()) {
0613             const int fullScreenBarWidth = fullScreenBar->layout()->minimumSize().width();
0614             if (fullScreenBarWidth > minimumSize.width()) {
0615                 minimumSize.setWidth(fullScreenBarWidth);
0616             }
0617         }
0618     }
0619     return minimumSize;
0620 }
0621 
0622 QUrl ViewMainPage::url() const
0623 {
0624     GV_RETURN_VALUE_IF_FAIL(d->currentView(), QUrl());
0625     return d->currentView()->url();
0626 }
0627 
0628 Document::Ptr ViewMainPage::currentDocument() const
0629 {
0630     if (!d->currentView()) {
0631         LOG("!d->documentView()");
0632         return {};
0633     }
0634 
0635     return d->currentView()->document();
0636 }
0637 
0638 bool ViewMainPage::isEmpty() const
0639 {
0640     return !currentDocument();
0641 }
0642 
0643 RasterImageView *ViewMainPage::imageView() const
0644 {
0645     if (!d->currentView()) {
0646         return nullptr;
0647     }
0648     return d->currentView()->imageView();
0649 }
0650 
0651 DocumentView *ViewMainPage::documentView() const
0652 {
0653     return d->currentView();
0654 }
0655 
0656 void ViewMainPage::openUrl(const QUrl &url)
0657 {
0658     openUrls(QList<QUrl>() << url, url);
0659 }
0660 
0661 void ViewMainPage::openUrls(const QList<QUrl> &allUrls, const QUrl &currentUrl)
0662 {
0663     DocumentView::Setup setup;
0664 
0665     QSet<QUrl> urls(allUrls.begin(), allUrls.end());
0666     d->mCompareMode = urls.count() > 1;
0667 
0668     using ViewForUrlMap = QMap<QUrl, DocumentView *>;
0669     ViewForUrlMap viewForUrlMap;
0670 
0671     if (!d->mDocumentViews.isEmpty()) {
0672         d->mDocumentViewContainer->updateSetup(d->mDocumentViews.last());
0673     }
0674     if (d->mDocumentViews.isEmpty() || d->mZoomMode == ZoomMode::Autofit) {
0675         setup.valid = true;
0676         setup.zoomToFit = true;
0677     } else {
0678         setup = d->mDocumentViews.last()->setup();
0679     }
0680     // Destroy views which show urls we don't care about, remove from "urls" the
0681     // urls which already have a view.
0682     for (DocumentView *view : qAsConst(d->mDocumentViews)) {
0683         QUrl url = view->url();
0684         if (urls.contains(url)) {
0685             // view displays an url we must display, keep it
0686             urls.remove(url);
0687             viewForUrlMap.insert(url, view);
0688         } else {
0689             // view url is not interesting, drop it
0690             d->deleteDocumentView(view);
0691         }
0692     }
0693 
0694     // Create view for remaining urls
0695     for (const QUrl &url : qAsConst(urls)) {
0696         if (d->mDocumentViews.count() >= MaxViewCount) {
0697             qCWarning(GWENVIEW_APP_LOG) << "Too many documents to show";
0698             break;
0699         }
0700         DocumentView *view = d->createDocumentView();
0701         viewForUrlMap.insert(url, view);
0702     }
0703 
0704     // Set sortKey to match url order
0705     int sortKey = 0;
0706     for (const QUrl &url : allUrls) {
0707         viewForUrlMap[url]->setSortKey(sortKey);
0708         ++sortKey;
0709     }
0710 
0711     d->mDocumentViewContainer->updateLayout();
0712 
0713     // Load urls for new views. Do it only now because the view must have the
0714     // correct size before it starts loading its url. Do not do it later because
0715     // view->url() needs to be set for the next loop.
0716     ViewForUrlMap::ConstIterator it = viewForUrlMap.constBegin(), end = viewForUrlMap.constEnd();
0717     for (; it != end; ++it) {
0718         QUrl url = it.key();
0719         DocumentView *view = it.value();
0720         DocumentView::Setup savedSetup = d->mDocumentViewContainer->savedSetup(url);
0721         view->openUrl(url, d->mZoomMode == ZoomMode::Individual && savedSetup.valid ? savedSetup : setup);
0722 #if HAVE_KACTIVITIES
0723         if (GwenviewConfig::historyEnabled()) {
0724             KActivities::ResourceInstance::notifyAccessed(url);
0725         }
0726 #endif
0727     }
0728 
0729     // Init views
0730     for (DocumentView *view : qAsConst(d->mDocumentViews)) {
0731         view->setCompareMode(d->mCompareMode);
0732         if (view->url() == currentUrl) {
0733             d->setCurrentView(view);
0734         } else {
0735             view->setCurrent(false);
0736         }
0737     }
0738 
0739     d->mSynchronizeCheckBox->setVisible(d->mCompareMode);
0740     if (d->mCompareMode) {
0741         d->mSynchronizer->setActive(d->mSynchronizeCheckBox->isChecked());
0742     } else {
0743         d->mSynchronizer->setActive(false);
0744     }
0745 
0746     d->updateDocumentCountLabel();
0747     d->mDocumentCountLabel->setVisible(!d->mCompareMode);
0748 }
0749 
0750 void ViewMainPage::reload()
0751 {
0752     DocumentView *view = d->currentView();
0753     if (!view) {
0754         return;
0755     }
0756     Document::Ptr doc = view->document();
0757     if (!doc) {
0758         qCWarning(GWENVIEW_APP_LOG) << "!doc";
0759         return;
0760     }
0761     if (doc->isModified()) {
0762         KGuiItem cont = KStandardGuiItem::cont();
0763         cont.setText(i18nc("@action:button", "Discard Changes and Reload"));
0764         int answer = KMessageBox::warningContinueCancel(this,
0765                                                         i18nc("@info", "This image has been modified. Reloading it will discard all your changes."),
0766                                                         QString() /* caption */,
0767                                                         cont);
0768         if (answer != KMessageBox::Continue) {
0769             return;
0770         }
0771     }
0772     doc->reload();
0773     // Call openUrl again because DocumentView may need to switch to a new
0774     // adapter (for example because document was broken and it is not anymore)
0775     d->currentView()->openUrl(doc->url(), d->currentView()->setup());
0776 }
0777 
0778 void ViewMainPage::reset()
0779 {
0780     d->mDocumentViewController->reset();
0781     d->mDocumentViewContainer->reset();
0782     d->mDocumentViews.clear();
0783 }
0784 
0785 void ViewMainPage::slotViewFocused(DocumentView *view)
0786 {
0787     d->setCurrentView(view);
0788 }
0789 
0790 void ViewMainPage::slotEnterPressed()
0791 {
0792     DocumentView *view = d->currentView();
0793     if (view) {
0794         AbstractRasterImageViewTool *tool = view->currentTool();
0795         if (tool) {
0796             QKeyEvent event(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier);
0797             tool->keyPressEvent(&event);
0798             if (event.isAccepted()) {
0799                 return;
0800             }
0801         }
0802     }
0803 }
0804 
0805 bool ViewMainPage::eventFilter(QObject *watched, QEvent *event)
0806 {
0807     if (watched == d->mThumbnailBar && event->type() == QEvent::KeyPress) {
0808         auto ke = static_cast<QKeyEvent *>(event);
0809         switch (ke->key()) {
0810         case Qt::Key_Left:
0811             if (QApplication::isRightToLeft()) {
0812                 Q_EMIT nextImageRequested();
0813             } else {
0814                 Q_EMIT previousImageRequested();
0815             }
0816             return true;
0817         case Qt::Key_Up:
0818             Q_EMIT previousImageRequested();
0819             return true;
0820         case Qt::Key_Right:
0821             if (QApplication::isRightToLeft()) {
0822                 Q_EMIT previousImageRequested();
0823             } else {
0824                 Q_EMIT nextImageRequested();
0825             }
0826             return true;
0827         case Qt::Key_Down:
0828             Q_EMIT nextImageRequested();
0829             return true;
0830         default:
0831             break;
0832         }
0833     } else if (event->type() == QEvent::ShortcutOverride) {
0834         const int key = static_cast<QKeyEvent *>(event)->key();
0835         if (key == Qt::Key_Space || key == Qt::Key_Escape) {
0836             const DocumentView *view = d->currentView();
0837             if (view) {
0838                 AbstractRasterImageViewTool *tool = view->currentTool();
0839                 if (tool) {
0840                     QKeyEvent toolKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
0841                     tool->keyPressEvent(&toolKeyEvent);
0842                     if (toolKeyEvent.isAccepted()) {
0843                         event->accept();
0844                     }
0845                 }
0846             }
0847         }
0848         // Leave fullscreen when viewing an image
0849         if (window()->isFullScreen() && key == Qt::Key_Escape) {
0850             d->mActionCollection->action(QStringLiteral("leave_fullscreen"))->trigger();
0851             event->accept();
0852         }
0853     }
0854 
0855     return QWidget::eventFilter(watched, event);
0856 }
0857 
0858 void ViewMainPage::trashView(DocumentView *view)
0859 {
0860     QUrl url = view->url();
0861     deselectView(view);
0862     FileOperations::trash(QList<QUrl>() << url, this);
0863 }
0864 
0865 void ViewMainPage::deselectView(DocumentView *view)
0866 {
0867     DocumentView *newCurrentView = nullptr;
0868     if (view == d->currentView()) {
0869         // We need to find a new view to set as current
0870         int idx = d->mDocumentViews.indexOf(view);
0871         if (idx + 1 < d->mDocumentViews.count()) {
0872             newCurrentView = d->mDocumentViews.at(idx + 1);
0873         } else if (idx > 0) {
0874             newCurrentView = d->mDocumentViews.at(idx - 1);
0875         } else {
0876             GV_WARN_AND_RETURN("No view found to set as current");
0877         }
0878     }
0879 
0880     QModelIndex index = d->indexForView(view);
0881     QItemSelectionModel *selectionModel = d->mThumbnailBar->selectionModel();
0882     selectionModel->select(index, QItemSelectionModel::Deselect);
0883 
0884     if (newCurrentView) {
0885         d->setCurrentView(newCurrentView);
0886     }
0887 }
0888 
0889 QToolButton *ViewMainPage::toggleSideBarButton() const
0890 {
0891     return d->mToggleSideBarButton;
0892 }
0893 
0894 void ViewMainPage::showMessageWidget(QGraphicsWidget *widget, Qt::Alignment align)
0895 {
0896     d->mDocumentViewContainer->showMessageWidget(widget, align);
0897 }
0898 
0899 void ViewMainPage::updateFocus(const AbstractRasterImageViewTool *tool)
0900 {
0901     if (!tool) {
0902         d->mDocumentViewContainer->setFocus();
0903     }
0904 }
0905 
0906 void ViewMainPage::slotDirModelItemsAddedOrRemoved()
0907 {
0908     d->updateDocumentCountLabel();
0909 }
0910 
0911 void ViewMainPage::setSpotlightMode(bool spotlightmode)
0912 {
0913     GwenviewConfig::setSpotlightMode(spotlightmode);
0914     d->mSpotlightMode->setVisibleSpotlightModeQuitButton(spotlightmode);
0915     if (spotlightmode) {
0916         d->mThumbnailBar->setVisible(false);
0917     } else {
0918         d->mThumbnailBar->setVisible(d->mToggleThumbnailBarAction->isChecked());
0919     }
0920 }
0921 
0922 } // namespace
0923 
0924 #include "moc_viewmainpage.cpp"