File indexing completed on 2024-04-21 04:18:48

0001 // vim: set tabstop=4 shiftwidth=4 expandtab:
0002 /*
0003 Gwenview: an image viewer
0004 Copyright 2009 Aurélien Gâteau <agateau@kde.org>
0005 
0006 This program is free software; you can redistribute it and/or
0007 modify it under the terms of the GNU General Public License
0008 as published by the Free Software Foundation; either version 2
0009 of the License, or (at your option) any later version.
0010 
0011 This program is distributed in the hope that it will be useful,
0012 but WITHOUT ANY WARRANTY; without even the implied warranty of
0013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014 GNU General Public License for more details.
0015 
0016 You should have received a copy of the GNU General Public License
0017 along with this program; if not, write to the Free Software
0018 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
0019 
0020 */
0021 // Self
0022 #include "thumbnailpage.h"
0023 
0024 // Qt
0025 #include <QDesktopServices>
0026 #include <QDir>
0027 #include <QGraphicsOpacityEffect>
0028 #include <QIcon>
0029 #include <QProcess>
0030 #include <QPushButton>
0031 #include <QTreeView>
0032 
0033 // KF
0034 #include <KAcceleratorManager>
0035 #include <KDirLister>
0036 #include <KDirModel>
0037 #include <KIO/DesktopExecParser>
0038 #include <KIO/StatJob>
0039 #include <KIconLoader>
0040 #include <KJobWidgets>
0041 #include <KMessageBox>
0042 #include <KModelIndexProxyMapper>
0043 #include <kio/global.h>
0044 
0045 // Local
0046 #include <documentdirfinder.h>
0047 #include <importerconfigdialog.h>
0048 #include <lib/archiveutils.h>
0049 #include <lib/gwenviewconfig.h>
0050 #include <lib/kindproxymodel.h>
0051 #include <lib/recursivedirmodel.h>
0052 #include <lib/semanticinfo/sorteddirmodel.h>
0053 #include <lib/thumbnailprovider/thumbnailprovider.h>
0054 #include <lib/thumbnailview/abstractthumbnailviewhelper.h>
0055 #include <lib/thumbnailview/previewitemdelegate.h>
0056 #include <serializedurlmap.h>
0057 #include <ui_thumbnailpage.h>
0058 
0059 namespace Gwenview
0060 {
0061 static const int DEFAULT_THUMBNAIL_SIZE = 128;
0062 static const qreal DEFAULT_THUMBNAIL_ASPECT_RATIO = 3. / 2.;
0063 
0064 static const char *URL_FOR_BASE_URL_GROUP = "UrlForBaseUrl";
0065 
0066 class ImporterThumbnailViewHelper : public AbstractThumbnailViewHelper
0067 {
0068 public:
0069     ImporterThumbnailViewHelper(QObject *parent)
0070         : AbstractThumbnailViewHelper(parent)
0071     {
0072     }
0073 
0074     void showContextMenu(QWidget *) override
0075     {
0076     }
0077 
0078     void showMenuForUrlDroppedOnViewport(QWidget *, const QList<QUrl> &) override
0079     {
0080     }
0081 
0082     void showMenuForUrlDroppedOnDir(QWidget *, const QList<QUrl> &, const QUrl &) override
0083     {
0084     }
0085 };
0086 
0087 inline KFileItem itemForIndex(const QModelIndex &index)
0088 {
0089     return index.data(KDirModel::FileItemRole).value<KFileItem>();
0090 }
0091 
0092 struct ThumbnailPagePrivate : public Ui_ThumbnailPage {
0093     ThumbnailPage *q = nullptr;
0094     SerializedUrlMap mUrlMap;
0095 
0096     QIcon mSrcBaseIcon;
0097     QString mSrcBaseName;
0098     QUrl mSrcBaseUrl;
0099     QUrl mSrcUrl;
0100     KModelIndexProxyMapper *mSrcUrlModelProxyMapper = nullptr;
0101 
0102     RecursiveDirModel *mRecursiveDirModel = nullptr;
0103     QAbstractItemModel *mFinalModel = nullptr;
0104 
0105     ThumbnailProvider mThumbnailProvider;
0106 
0107     // Placeholder view
0108     QLabel *mPlaceHolderIconLabel = nullptr;
0109     QLabel *mPlaceHolderLabel = nullptr;
0110     QLabel *mRequireRestartLabel = nullptr;
0111     QPushButton *mInstallProtocolSupportButton = nullptr;
0112     QVBoxLayout *mPlaceHolderLayout = nullptr;
0113     QWidget *mPlaceHolderWidget = nullptr; // To avoid clipping in gridLayout
0114 
0115     QPushButton *mImportSelectedButton;
0116     QPushButton *mImportAllButton;
0117     QList<QUrl> mUrlList;
0118 
0119     void setupDirModel()
0120     {
0121         mRecursiveDirModel = new RecursiveDirModel(q);
0122 
0123         auto kindProxyModel = new KindProxyModel(q);
0124         kindProxyModel->setKindFilter(MimeTypeUtils::KIND_RASTER_IMAGE | MimeTypeUtils::KIND_SVG_IMAGE | MimeTypeUtils::KIND_VIDEO);
0125         kindProxyModel->setSourceModel(mRecursiveDirModel);
0126 
0127         auto sortModel = new QSortFilterProxyModel(q);
0128         sortModel->setDynamicSortFilter(true);
0129         sortModel->setSourceModel(kindProxyModel);
0130         sortModel->sort(0);
0131 
0132         mFinalModel = sortModel;
0133 
0134         QObject::connect(mFinalModel, &QAbstractItemModel::rowsInserted, q, &ThumbnailPage::updateImportButtons);
0135         QObject::connect(mFinalModel, &QAbstractItemModel::rowsRemoved, q, &ThumbnailPage::updateImportButtons);
0136         QObject::connect(mFinalModel, &QAbstractItemModel::modelReset, q, &ThumbnailPage::updateImportButtons);
0137     }
0138 
0139     void setupIcons()
0140     {
0141         const int size = KIconLoader::SizeHuge;
0142         mSrcIconLabel->setPixmap(QIcon::fromTheme(QStringLiteral("camera-photo")).pixmap(size));
0143         mDstIconLabel->setPixmap(QIcon::fromTheme(QStringLiteral("computer")).pixmap(size));
0144     }
0145 
0146     void setupSrcUrlWidgets()
0147     {
0148         mSrcUrlModelProxyMapper = nullptr;
0149         QObject::connect(mSrcUrlButton, &QAbstractButton::clicked, q, &ThumbnailPage::setupSrcUrlTreeView);
0150         QObject::connect(mSrcUrlButton, &QAbstractButton::clicked, q, &ThumbnailPage::toggleSrcUrlTreeView);
0151         mSrcUrlTreeView->hide();
0152         KAcceleratorManager::setNoAccel(mSrcUrlButton);
0153     }
0154 
0155     void setupDstUrlRequester()
0156     {
0157         mDstUrlRequester->setMode(KFile::Directory | KFile::LocalOnly);
0158     }
0159 
0160     void setupThumbnailView()
0161     {
0162         mThumbnailView->setModel(mFinalModel);
0163 
0164         mThumbnailView->setSelectionMode(QAbstractItemView::ExtendedSelection);
0165         mThumbnailView->setThumbnailViewHelper(new ImporterThumbnailViewHelper(q));
0166 
0167         auto delegate = new PreviewItemDelegate(mThumbnailView);
0168         delegate->setThumbnailDetails(PreviewItemDelegate::FileNameDetail);
0169         PreviewItemDelegate::ContextBarActions actions;
0170         switch (GwenviewConfig::thumbnailActions()) {
0171         case ThumbnailActions::None:
0172             actions = PreviewItemDelegate::NoAction;
0173             break;
0174         case ThumbnailActions::ShowSelectionButtonOnly:
0175         case ThumbnailActions::AllButtons:
0176             actions = PreviewItemDelegate::SelectionAction;
0177             break;
0178         }
0179         delegate->setContextBarActions(actions);
0180         mThumbnailView->setItemDelegate(delegate);
0181 
0182         QObject::connect(mSlider, &ZoomSlider::valueChanged, mThumbnailView, &ThumbnailView::setThumbnailWidth);
0183         QObject::connect(mThumbnailView, &ThumbnailView::thumbnailWidthChanged, mSlider, &ZoomSlider::setValue);
0184         int thumbnailSize = DEFAULT_THUMBNAIL_SIZE;
0185         mSlider->setValue(thumbnailSize);
0186         mSlider->updateToolTip();
0187         mThumbnailView->setThumbnailAspectRatio(DEFAULT_THUMBNAIL_ASPECT_RATIO);
0188         mThumbnailView->setThumbnailWidth(thumbnailSize);
0189         mThumbnailView->setThumbnailProvider(&mThumbnailProvider);
0190 
0191         QObject::connect(mThumbnailView->selectionModel(), &QItemSelectionModel::selectionChanged, q, &ThumbnailPage::updateImportButtons);
0192     }
0193 
0194     void setupPlaceHolderView(const QString &errorText)
0195     {
0196         mPlaceHolderWidget = new QWidget(q);
0197         // Use QSizePolicy::MinimumExpanding to avoid clipping
0198         mPlaceHolderWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
0199 
0200         // Icon
0201         mPlaceHolderIconLabel = new QLabel(mPlaceHolderWidget);
0202         const QSize iconSize(KIconLoader::SizeHuge, KIconLoader::SizeHuge);
0203         mPlaceHolderIconLabel->setMinimumSize(iconSize);
0204         mPlaceHolderIconLabel->setPixmap(QIcon::fromTheme(QStringLiteral("edit-none")).pixmap(iconSize));
0205         auto iconEffect = new QGraphicsOpacityEffect(mPlaceHolderIconLabel);
0206         iconEffect->setOpacity(0.5);
0207         mPlaceHolderIconLabel->setGraphicsEffect(iconEffect);
0208 
0209         // Label: see dolphin/src/views/dolphinview.cpp
0210         const QSizePolicy labelSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred, QSizePolicy::Label);
0211         mPlaceHolderLabel = new QLabel(mPlaceHolderWidget);
0212         mPlaceHolderLabel->setSizePolicy(labelSizePolicy);
0213         QFont placeholderLabelFont;
0214         // To match the size of a level 2 Heading/KTitleWidget
0215         placeholderLabelFont.setPointSize(qRound(placeholderLabelFont.pointSize() * 1.3));
0216         mPlaceHolderLabel->setFont(placeholderLabelFont);
0217         mPlaceHolderLabel->setTextInteractionFlags(Qt::NoTextInteraction);
0218         mPlaceHolderLabel->setWordWrap(true);
0219         mPlaceHolderLabel->setAlignment(Qt::AlignCenter);
0220         // Match opacity of QML placeholder label component
0221         auto effect = new QGraphicsOpacityEffect(mPlaceHolderLabel);
0222         effect->setOpacity(0.5);
0223         mPlaceHolderLabel->setGraphicsEffect(effect);
0224         // Show more friendly text when the protocol is "camera" (which is the usual case)
0225         const QString scheme(mSrcBaseUrl.scheme());
0226         // Truncate long protocol name
0227         const QString truncatedScheme(
0228             scheme.length() <= 10 ? scheme : QStringView(scheme).left(5).toString() + QStringLiteral("…") + QStringView(scheme).right(5).toString());
0229         // clang-format off
0230         if (scheme == QLatin1String("camera")) {
0231             mPlaceHolderLabel->setText(i18nc("@info above install button when Kamera is not installed", "Support for your camera is not installed."));
0232         } else if (!errorText.isEmpty()) {
0233             mPlaceHolderLabel->setText(i18nc("@info above install button, %1 protocol name %2 error text from KIO", "The protocol support library for \"%1\" is not installed. Error: %2", truncatedScheme, errorText));
0234         } else {
0235             mPlaceHolderLabel->setText(i18nc("@info above install button, %1 protocol name", "The protocol support library for \"%1\" is not installed.", truncatedScheme));
0236         }
0237 
0238         // Label to guide the user to restart the wizard after installing the protocol support library
0239         mRequireRestartLabel = new QLabel(mPlaceHolderWidget);
0240         mRequireRestartLabel->setSizePolicy(labelSizePolicy);
0241         mRequireRestartLabel->setTextInteractionFlags(Qt::NoTextInteraction);
0242         mRequireRestartLabel->setWordWrap(true);
0243         mRequireRestartLabel->setAlignment(Qt::AlignCenter);
0244         auto effect2 = new QGraphicsOpacityEffect(mRequireRestartLabel);
0245         effect2->setOpacity(0.5);
0246         mRequireRestartLabel->setGraphicsEffect(effect2);
0247         mRequireRestartLabel->setText(i18nc("@info:usagetip above install button", "After finishing the installation process, restart this Importer to continue."));
0248 
0249         // Button
0250         // Check if Discover is installed
0251         q->mDiscoverAvailable = !QStandardPaths::findExecutable("plasma-discover").isEmpty();
0252         QIcon buttonIcon(q->mDiscoverAvailable ? QIcon::fromTheme("plasmadiscover") : QIcon::fromTheme("install"));
0253         QString buttonText, whatsThisText;
0254         QString tooltipText(i18nc("@info:tooltip for a button, %1 protocol name", "Launch Discover to install the protocol support library for \"%1\"", scheme));
0255         if (scheme == QLatin1String("camera")) {
0256             buttonText = i18nc("@action:button", "Install Support for this Camera…");
0257             whatsThisText = i18nc("@info:whatsthis for a button when Kamera is not installed", "You need Kamera installed on your system to read from the camera. Click here to launch Discover to install Kamera to enable protocol support for \"camera:/\" on your system.");
0258         } else {
0259             if (q->mDiscoverAvailable) {
0260                 buttonText = i18nc("@action:button %1 protocol name", "Install Protocol Support for \"%1\"…", truncatedScheme);
0261                 whatsThisText = i18nc("@info:whatsthis for a button, %1 protocol name", "Click here to launch Discover to install the missing protocol support library to enable the protocol support for \"%1:/\" on your system.", scheme);
0262             } else {
0263                 // If Discover is not found on the system, guide the user to search the web.
0264                 buttonIcon = QIcon::fromTheme("internet-web-browser");
0265                 buttonText = i18nc("@action:button %1 protocol name", "Search the Web for How to Install Protocol Support for \"%1\"…", truncatedScheme);
0266                 tooltipText.clear();
0267             }
0268         }
0269         mInstallProtocolSupportButton = new QPushButton(buttonIcon, buttonText, mPlaceHolderWidget);
0270         mInstallProtocolSupportButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
0271         mInstallProtocolSupportButton->setToolTip(tooltipText);
0272         mInstallProtocolSupportButton->setWhatsThis(whatsThisText);
0273         // Highlight the button so the user can notice it more easily.
0274         mInstallProtocolSupportButton->setDefault(true);
0275         mInstallProtocolSupportButton->setFocus();
0276         // Button action
0277         if (q->mDiscoverAvailable || scheme == QLatin1String("camera")) {
0278             QObject::connect(mInstallProtocolSupportButton, &QAbstractButton::clicked, q, &ThumbnailPage::installProtocolSupport);
0279         } else {
0280             QObject::connect(mInstallProtocolSupportButton, &QAbstractButton::clicked, q, [scheme]() {
0281                 const QString searchKeyword(QUrl::toPercentEncoding(i18nc("@info this text will be used as a search term in an online search engine, %1 protocol name", "How to install protocol support for \"%1\" on Linux", scheme)).constData());
0282                 const QString searchEngineURL(i18nc("search engine URL, %1 search keyword, and translators can replace duckduckgo with other search engines", "https://duckduckgo.com/?q=%1", searchKeyword));
0283                 QDesktopServices::openUrl(QUrl(searchEngineURL));
0284             });
0285         }
0286         // clang-format on
0287 
0288         // VBoxLayout
0289         mPlaceHolderLayout = new QVBoxLayout(mPlaceHolderWidget);
0290         mPlaceHolderLayout->addStretch();
0291         mPlaceHolderLayout->addWidget(mPlaceHolderIconLabel, 0, Qt::AlignCenter);
0292         mPlaceHolderLayout->addWidget(mPlaceHolderLabel);
0293         mPlaceHolderLayout->addWidget(mRequireRestartLabel);
0294         mPlaceHolderLayout->addWidget(mInstallProtocolSupportButton, 0, Qt::AlignCenter); // Do not stretch the button
0295         mPlaceHolderLayout->addStretch();
0296 
0297         // Hide other controls
0298         gridLayout->removeItem(verticalLayout);
0299         gridLayout->removeItem(verticalLayout_2);
0300         gridLayout->removeWidget(label);
0301         gridLayout->removeWidget(label_2);
0302         gridLayout->removeWidget(widget);
0303         gridLayout->removeWidget(widget);
0304         gridLayout->removeWidget(mDstUrlRequester);
0305         mDstIconLabel->hide();
0306         mSrcIconLabel->hide();
0307         label->hide();
0308         label_2->hide();
0309         mDstUrlRequester->hide();
0310         widget->hide();
0311         widget_2->hide();
0312         mConfigureButton->hide();
0313         mImportSelectedButton->hide();
0314         mImportAllButton->hide();
0315 
0316         gridLayout->addWidget(mPlaceHolderWidget, 0, 0, 1, 0);
0317     }
0318 
0319     void setupButtonBox()
0320     {
0321         QObject::connect(mConfigureButton, &QAbstractButton::clicked, q, &ThumbnailPage::showConfigDialog);
0322 
0323         mImportSelectedButton = mButtonBox->addButton(i18n("Import Selected"), QDialogButtonBox::AcceptRole);
0324         QObject::connect(mImportSelectedButton, &QAbstractButton::clicked, q, &ThumbnailPage::slotImportSelected);
0325 
0326         mImportAllButton = mButtonBox->addButton(i18n("Import All"), QDialogButtonBox::AcceptRole);
0327         QObject::connect(mImportAllButton, &QAbstractButton::clicked, q, &ThumbnailPage::slotImportAll);
0328 
0329         QObject::connect(mButtonBox, &QDialogButtonBox::rejected, q, &ThumbnailPage::rejected);
0330     }
0331 
0332     QUrl urlForBaseUrl() const
0333     {
0334         QUrl url = mUrlMap.value(mSrcBaseUrl);
0335         if (!url.isValid()) {
0336             return {};
0337         }
0338 
0339         KIO::StatJob *job = KIO::stat(url);
0340         KJobWidgets::setWindow(job, q);
0341         if (!job->exec()) {
0342             return {};
0343         }
0344         KFileItem item(job->statResult(), url, true /* delayedMimeTypes */);
0345         return item.isDir() ? url : QUrl();
0346     }
0347 
0348     void rememberUrl(const QUrl &url)
0349     {
0350         mUrlMap.insert(mSrcBaseUrl, url);
0351     }
0352 };
0353 
0354 ThumbnailPage::ThumbnailPage()
0355     : d(new ThumbnailPagePrivate)
0356 {
0357     d->q = this;
0358     d->mUrlMap.setConfigGroup(KConfigGroup(KSharedConfig::openConfig(), URL_FOR_BASE_URL_GROUP));
0359     d->setupUi(this);
0360     d->setupIcons();
0361     d->setupDirModel();
0362     d->setupSrcUrlWidgets();
0363     d->setupDstUrlRequester();
0364     d->setupThumbnailView();
0365     d->setupButtonBox();
0366     updateImportButtons();
0367 }
0368 
0369 ThumbnailPage::~ThumbnailPage()
0370 {
0371     delete d;
0372 }
0373 
0374 void ThumbnailPage::setSourceUrl(const QUrl &srcBaseUrl, const QString &iconName, const QString &name)
0375 {
0376     d->mSrcBaseIcon = QIcon::fromTheme(iconName);
0377     d->mSrcBaseName = name;
0378 
0379     const int size = KIconLoader::SizeHuge;
0380     d->mSrcIconLabel->setPixmap(d->mSrcBaseIcon.pixmap(size));
0381 
0382     d->mSrcBaseUrl = srcBaseUrl;
0383     if (!d->mSrcBaseUrl.path().endsWith('/')) {
0384         d->mSrcBaseUrl.setPath(d->mSrcBaseUrl.path() + '/');
0385     }
0386     QUrl url = d->urlForBaseUrl();
0387 
0388     if (url.isValid()) {
0389         openUrl(url);
0390     } else {
0391         auto finder = new DocumentDirFinder(srcBaseUrl);
0392         connect(finder, &DocumentDirFinder::done, this, &ThumbnailPage::slotDocumentDirFinderDone);
0393         connect(finder, &DocumentDirFinder::protocollNotSupportedError, this, [this](const QString &errorText) {
0394             d->setupPlaceHolderView(errorText);
0395         });
0396         finder->start();
0397     }
0398 }
0399 
0400 void ThumbnailPage::slotDocumentDirFinderDone(const QUrl &url, DocumentDirFinder::Status /*status*/)
0401 {
0402     d->rememberUrl(url);
0403     openUrl(url);
0404 }
0405 
0406 void ThumbnailPage::openUrl(const QUrl &url)
0407 {
0408     d->mSrcUrl = url;
0409     QString path = QDir(d->mSrcBaseUrl.path()).relativeFilePath(d->mSrcUrl.path());
0410     QString text;
0411     if (path.isEmpty() || path == QLatin1String(".")) {
0412         text = d->mSrcBaseName;
0413     } else {
0414         path = QUrl::fromPercentEncoding(path.toUtf8());
0415         path.replace('/', QString::fromUtf8(" › "));
0416         text = QString::fromUtf8("%1 › %2").arg(d->mSrcBaseName, path);
0417     }
0418     d->mSrcUrlButton->setText(text);
0419     d->mRecursiveDirModel->setUrl(url);
0420 }
0421 
0422 QList<QUrl> ThumbnailPage::urlList() const
0423 {
0424     return d->mUrlList;
0425 }
0426 
0427 void ThumbnailPage::setDestinationUrl(const QUrl &url)
0428 {
0429     d->mDstUrlRequester->setUrl(url);
0430 }
0431 
0432 QUrl ThumbnailPage::destinationUrl() const
0433 {
0434     return d->mDstUrlRequester->url();
0435 }
0436 
0437 void ThumbnailPage::slotImportSelected()
0438 {
0439     importList(d->mThumbnailView->selectionModel()->selectedIndexes());
0440 }
0441 
0442 void ThumbnailPage::slotImportAll()
0443 {
0444     QModelIndexList list;
0445     QAbstractItemModel *model = d->mThumbnailView->model();
0446     for (int row = model->rowCount() - 1; row >= 0; --row) {
0447         list << model->index(row, 0);
0448     }
0449     importList(list);
0450 }
0451 
0452 void ThumbnailPage::importList(const QModelIndexList &list)
0453 {
0454     d->mUrlList.clear();
0455     for (const QModelIndex &index : list) {
0456         KFileItem item = itemForIndex(index);
0457         if (!ArchiveUtils::fileItemIsDirOrArchive(item)) {
0458             d->mUrlList << item.url();
0459         }
0460         // FIXME: Handle dirs (do we want to import recursively?)
0461     }
0462     Q_EMIT importRequested();
0463 }
0464 
0465 void ThumbnailPage::updateImportButtons()
0466 {
0467     d->mImportSelectedButton->setEnabled(d->mThumbnailView->selectionModel()->hasSelection());
0468     d->mImportAllButton->setEnabled(d->mThumbnailView->model()->rowCount(QModelIndex()) > 0);
0469 }
0470 
0471 void ThumbnailPage::showConfigDialog()
0472 {
0473     auto dialog = new ImporterConfigDialog(this);
0474     dialog->setAttribute(Qt::WA_DeleteOnClose);
0475     dialog->setModal(true);
0476     dialog->show();
0477 }
0478 
0479 void ThumbnailPage::installProtocolSupport() const
0480 {
0481     const QString scheme(d->mSrcBaseUrl.scheme());
0482     // clang-format off
0483     if (scheme == QLatin1String("camera")) {
0484         const QUrl kameraInstallUrl("appstream://org.kde.kamera");
0485         if (KIO::DesktopExecParser::hasSchemeHandler(kameraInstallUrl)) {
0486             QDesktopServices::openUrl(kameraInstallUrl);
0487         } else {
0488             KMessageBox::error(d->widget, xi18nc("@info when failing to open the appstream URL", "Opening Discover failed.<nl/>Please check if Discover is installed on your system, or use your system's package manager to install \"Kamera\" package."));
0489         }
0490     } else if (!QProcess::startDetached(QStringLiteral("plasma-discover"), QStringList({"--search", scheme}))) {
0491         // For other protocols, search for the protocol name in Discover.
0492         KMessageBox::error(d->widget, xi18nc("@info when failing to launch plasma-discover, %1 protocol name", "Opening Discover failed.<nl/>Please check if Discover is installed on your system, or use your system's package manager to install the protocol support library for \"%1\".", scheme));
0493     }
0494     // clang-format on
0495 }
0496 
0497 /**
0498  * This model allows only the url passed in the constructor to appear at the root
0499  * level. This makes it possible to select the url, but not its siblings.
0500  * It also provides custom role values for the root item.
0501  */
0502 class OnlyBaseUrlProxyModel : public QSortFilterProxyModel
0503 {
0504 public:
0505     OnlyBaseUrlProxyModel(const QUrl &url, const QIcon &icon, const QString &name, QObject *parent)
0506         : QSortFilterProxyModel(parent)
0507         , mUrl(url)
0508         , mIcon(icon)
0509         , mName(name)
0510     {
0511     }
0512 
0513     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
0514     {
0515         if (sourceParent.isValid()) {
0516             return true;
0517         }
0518         QModelIndex index = sourceModel()->index(sourceRow, 0);
0519         KFileItem item = itemForIndex(index);
0520         return item.url().matches(mUrl, QUrl::StripTrailingSlash);
0521     }
0522 
0523     QVariant data(const QModelIndex &index, int role) const override
0524     {
0525         if (index.parent().isValid()) {
0526             return QSortFilterProxyModel::data(index, role);
0527         }
0528         switch (role) {
0529         case Qt::DisplayRole:
0530             return mName;
0531         case Qt::DecorationRole:
0532             return mIcon;
0533         case Qt::ToolTipRole:
0534             return mUrl.toDisplayString(QUrl::PreferLocalFile);
0535         default:
0536             return QSortFilterProxyModel::data(index, role);
0537         }
0538     }
0539 
0540 private:
0541     QUrl mUrl;
0542     QIcon mIcon;
0543     QString mName;
0544 };
0545 
0546 void ThumbnailPage::setupSrcUrlTreeView()
0547 {
0548     if (d->mSrcUrlTreeView->model()) {
0549         // Already initialized
0550         return;
0551     }
0552     auto dirModel = new KDirModel(this);
0553     dirModel->dirLister()->setDirOnlyMode(true);
0554     dirModel->dirLister()->openUrl(KIO::upUrl(d->mSrcBaseUrl));
0555 
0556     auto onlyBaseUrlModel = new OnlyBaseUrlProxyModel(d->mSrcBaseUrl, d->mSrcBaseIcon, d->mSrcBaseName, this);
0557     onlyBaseUrlModel->setSourceModel(dirModel);
0558 
0559     auto sortModel = new QSortFilterProxyModel(this);
0560     sortModel->setDynamicSortFilter(true);
0561     sortModel->setSourceModel(onlyBaseUrlModel);
0562     sortModel->sort(0);
0563 
0564     d->mSrcUrlModelProxyMapper = new KModelIndexProxyMapper(dirModel, sortModel, this);
0565 
0566     d->mSrcUrlTreeView->setModel(sortModel);
0567     for (int i = 1; i < dirModel->columnCount(); ++i) {
0568         d->mSrcUrlTreeView->hideColumn(i);
0569     }
0570     connect(d->mSrcUrlTreeView, &QAbstractItemView::activated, this, &ThumbnailPage::openUrlFromIndex);
0571     connect(d->mSrcUrlTreeView, &QAbstractItemView::clicked, this, &ThumbnailPage::openUrlFromIndex);
0572 
0573     dirModel->expandToUrl(d->mSrcUrl);
0574     connect(dirModel, &KDirModel::expand, this, &ThumbnailPage::slotSrcUrlModelExpand);
0575 }
0576 
0577 void ThumbnailPage::slotSrcUrlModelExpand(const QModelIndex &index)
0578 {
0579     QModelIndex viewIndex = d->mSrcUrlModelProxyMapper->mapLeftToRight(index);
0580     d->mSrcUrlTreeView->expand(viewIndex);
0581     KFileItem item = itemForIndex(index);
0582     if (item.url() == d->mSrcUrl) {
0583         d->mSrcUrlTreeView->selectionModel()->select(viewIndex, QItemSelectionModel::ClearAndSelect);
0584     }
0585 }
0586 
0587 void ThumbnailPage::toggleSrcUrlTreeView()
0588 {
0589     d->mSrcUrlTreeView->setVisible(!d->mSrcUrlTreeView->isVisible());
0590 }
0591 
0592 void ThumbnailPage::openUrlFromIndex(const QModelIndex &index)
0593 {
0594     KFileItem item = itemForIndex(index);
0595     if (item.isNull()) {
0596         return;
0597     }
0598     QUrl url = item.url();
0599     d->rememberUrl(url);
0600     openUrl(url);
0601 }
0602 
0603 } // namespace
0604 
0605 #include "moc_thumbnailpage.cpp"