File indexing completed on 2024-04-28 04:18:50

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 "contextmanager.h"
0021 
0022 // Qt
0023 #include <QItemSelectionModel>
0024 #include <QTimer>
0025 #include <QUndoGroup>
0026 
0027 // KF
0028 #include <KDirLister>
0029 #include <KProtocolManager>
0030 
0031 // Local
0032 #include <lib/document/documentfactory.h>
0033 #include <lib/gvdebug.h>
0034 #include <lib/gwenviewconfig.h>
0035 #include <lib/semanticinfo/sorteddirmodel.h>
0036 
0037 namespace Gwenview
0038 {
0039 struct ContextManagerPrivate {
0040     SortedDirModel *mDirModel = nullptr;
0041     QItemSelectionModel *mSelectionModel = nullptr;
0042     QUrl mCurrentDirUrl;
0043     QUrl mCurrentUrl;
0044 
0045     QUrl mUrlToSelect;
0046     QUrl mTargetDirUrl;
0047 
0048     bool mSelectedFileItemListNeedsUpdate;
0049     using Signal = void (ContextManager::*)();
0050     QVector<Signal> mQueuedSignals;
0051     KFileItemList mSelectedFileItemList;
0052 
0053     bool mDirListerFinished = false;
0054     QTimer *mQueuedSignalsTimer = nullptr;
0055 
0056     void queueSignal(Signal signal)
0057     {
0058         if (!mQueuedSignals.contains(signal)) {
0059             mQueuedSignals << signal;
0060         }
0061         mQueuedSignalsTimer->start();
0062     }
0063 
0064     void updateSelectedFileItemList()
0065     {
0066         if (!mSelectedFileItemListNeedsUpdate) {
0067             return;
0068         }
0069         mSelectedFileItemList.clear();
0070         const QItemSelection selection = mSelectionModel->selection();
0071         for (const QModelIndex &index : selection.indexes()) {
0072             mSelectedFileItemList << mDirModel->itemForIndex(index);
0073         }
0074 
0075         // At least add current url if it's valid (it may not be in
0076         // the list if we are viewing a non-browsable url, for example
0077         // using http protocol)
0078         if (mSelectedFileItemList.isEmpty() && mCurrentUrl.isValid()) {
0079             KFileItem item(mCurrentUrl);
0080             mSelectedFileItemList << item;
0081         }
0082 
0083         mSelectedFileItemListNeedsUpdate = false;
0084     }
0085 };
0086 
0087 ContextManager::ContextManager(SortedDirModel *dirModel, QObject *parent)
0088     : QObject(parent)
0089     , d(new ContextManagerPrivate)
0090 {
0091     d->mQueuedSignalsTimer = new QTimer(this);
0092     d->mQueuedSignalsTimer->setInterval(100);
0093     d->mQueuedSignalsTimer->setSingleShot(true);
0094     connect(d->mQueuedSignalsTimer, &QTimer::timeout, this, &ContextManager::emitQueuedSignals);
0095 
0096     d->mDirModel = dirModel;
0097     connect(d->mDirModel, &SortedDirModel::dataChanged, this, &ContextManager::slotDirModelDataChanged);
0098 
0099     /* HACK! In extended-selection mode, when the current index is removed,
0100      * QItemSelectionModel selects the previous index if there is one, if not it
0101      * selects the next index. This is not what we want: when the user removes
0102      * an image, he expects to go to the next one, not the previous one.
0103      *
0104      * To overcome this, we must connect to the mDirModel.rowsAboutToBeRemoved()
0105      * signal *before* QItemSelectionModel connects to it, so that our slot is
0106      * called before QItemSelectionModel slot. This allows us to pick a new
0107      * current index ourself, leaving QItemSelectionModel slot with nothing to
0108      * do.
0109      *
0110      * This is the reason ContextManager creates a QItemSelectionModel itself:
0111      * doing so ensures QItemSelectionModel cannot be connected to the
0112      * mDirModel.rowsAboutToBeRemoved() signal before us.
0113      */
0114     connect(d->mDirModel, &SortedDirModel::rowsAboutToBeRemoved, this, &ContextManager::slotRowsAboutToBeRemoved);
0115 
0116     connect(d->mDirModel, &SortedDirModel::rowsInserted, this, &ContextManager::slotRowsInserted);
0117 
0118     connect(d->mDirModel->dirLister(), QOverload<const QUrl &, const QUrl &>::of(&KDirLister::redirection), this, [this](const QUrl &, const QUrl &newUrl) {
0119         setCurrentDirUrl(newUrl);
0120     });
0121 
0122     connect(d->mDirModel->dirLister(), QOverload<>::of(&KDirLister::completed), this, &ContextManager::slotDirListerCompleted);
0123 
0124     d->mSelectionModel = new QItemSelectionModel(d->mDirModel);
0125 
0126     connect(d->mSelectionModel, &QItemSelectionModel::selectionChanged, this, &ContextManager::slotSelectionChanged);
0127     connect(d->mSelectionModel, &QItemSelectionModel::currentChanged, this, &ContextManager::slotCurrentChanged);
0128 
0129     d->mSelectedFileItemListNeedsUpdate = false;
0130 
0131     connect(DocumentFactory::instance(), &DocumentFactory::readyForDirListerStart, this, [this](const QUrl &urlReady) {
0132         setCurrentDirUrl(urlReady.adjusted(QUrl::RemoveFilename));
0133     });
0134 }
0135 
0136 ContextManager::~ContextManager()
0137 {
0138     delete d;
0139 }
0140 
0141 void ContextManager::loadConfig()
0142 {
0143     setTargetDirUrl(QUrl(GwenviewConfig::lastTargetDir()));
0144 }
0145 
0146 void ContextManager::saveConfig() const
0147 {
0148     GwenviewConfig::setLastTargetDir(targetDirUrl().toString());
0149 }
0150 
0151 QItemSelectionModel *ContextManager::selectionModel() const
0152 {
0153     return d->mSelectionModel;
0154 }
0155 
0156 void ContextManager::setCurrentUrl(const QUrl &currentUrl)
0157 {
0158     if (d->mCurrentUrl == currentUrl) {
0159         return;
0160     }
0161 
0162     d->mCurrentUrl = currentUrl;
0163     if (!d->mCurrentUrl.isEmpty()) {
0164         Document::Ptr doc = DocumentFactory::instance()->load(currentUrl);
0165         QUndoGroup *undoGroup = DocumentFactory::instance()->undoGroup();
0166         undoGroup->addStack(doc->undoStack());
0167         undoGroup->setActiveStack(doc->undoStack());
0168     }
0169 
0170     d->mSelectedFileItemListNeedsUpdate = true;
0171     Q_EMIT currentUrlChanged(currentUrl);
0172 }
0173 
0174 KFileItemList ContextManager::selectedFileItemList() const
0175 {
0176     d->updateSelectedFileItemList();
0177     return d->mSelectedFileItemList;
0178 }
0179 
0180 void ContextManager::setCurrentDirUrl(const QUrl &_url)
0181 {
0182     const QUrl url = _url.adjusted(QUrl::StripTrailingSlash);
0183     if (url == d->mCurrentDirUrl) {
0184         return;
0185     }
0186 
0187     if (url.isValid() && KProtocolManager::supportsListing(url)) {
0188         d->mCurrentDirUrl = url;
0189         d->mDirModel->dirLister()->openUrl(url);
0190         d->mDirListerFinished = false;
0191     } else {
0192         d->mCurrentDirUrl.clear();
0193         Q_EMIT d->mDirModel->dirLister()->clear();
0194     }
0195     Q_EMIT currentDirUrlChanged(d->mCurrentDirUrl);
0196 }
0197 
0198 QUrl ContextManager::currentDirUrl() const
0199 {
0200     return d->mCurrentDirUrl;
0201 }
0202 
0203 QUrl ContextManager::currentUrl() const
0204 {
0205     return d->mCurrentUrl;
0206 }
0207 
0208 SortedDirModel *ContextManager::dirModel() const
0209 {
0210     return d->mDirModel;
0211 }
0212 
0213 void ContextManager::slotDirModelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
0214 {
0215     // Data change can happen in the following cases:
0216     // - items have been renamed
0217     // - item bytes have been modified
0218     // - item meta info has been retrieved or modified
0219     //
0220     // If a selected item is affected, schedule emission of a
0221     // selectionDataChanged() signal. Don't emit it directly to avoid spamming
0222     // the context items in case of a mass change.
0223     QModelIndexList selectionList = d->mSelectionModel->selectedIndexes();
0224     if (selectionList.isEmpty()) {
0225         return;
0226     }
0227 
0228     QModelIndexList changedList;
0229     for (int row = topLeft.row(); row <= bottomRight.row(); ++row) {
0230         changedList << d->mDirModel->index(row, 0);
0231     }
0232 
0233     QModelIndexList &shortList = selectionList;
0234     QModelIndexList &longList = changedList;
0235     if (shortList.length() > longList.length()) {
0236         std::swap(shortList, longList);
0237     }
0238     for (const QModelIndex &index : qAsConst(shortList)) {
0239         if (longList.contains(index)) {
0240             d->mSelectedFileItemListNeedsUpdate = true;
0241             d->queueSignal(&ContextManager::selectionDataChanged);
0242             return;
0243         }
0244     }
0245 }
0246 
0247 void ContextManager::slotSelectionChanged()
0248 {
0249     d->mSelectedFileItemListNeedsUpdate = true;
0250     if (!d->mSelectionModel->hasSelection()) {
0251         // There is a chance that the URL that has been passed in from the command
0252         // line is not shown by the thumbnail view. In that case, we will not have
0253         // a selection but we also do not want to clear the current URL, as that
0254         // would hide the image that was requested to be shown. So check to see if
0255         // the current URL is in the thumbnail view, and only if it is, deselect
0256         // it.
0257         if (d->mDirModel->indexForUrl(d->mCurrentUrl).isValid()) {
0258             setCurrentUrl(QUrl());
0259         }
0260     }
0261     d->queueSignal(&ContextManager::selectionChanged);
0262 }
0263 
0264 void Gwenview::ContextManager::slotCurrentChanged(const QModelIndex &index)
0265 {
0266     const QUrl url = d->mDirModel->urlForIndex(index);
0267     setCurrentUrl(url);
0268 }
0269 
0270 void ContextManager::emitQueuedSignals()
0271 {
0272     for (ContextManagerPrivate::Signal signal : qAsConst(d->mQueuedSignals)) {
0273         Q_EMIT(this->*signal)();
0274     }
0275     d->mQueuedSignals.clear();
0276 }
0277 
0278 void Gwenview::ContextManager::slotRowsAboutToBeRemoved(const QModelIndex & /*parent*/, int start, int end)
0279 {
0280     const QModelIndex oldCurrent = d->mSelectionModel->currentIndex();
0281     if (oldCurrent.row() < start || oldCurrent.row() > end) {
0282         // currentIndex has not been removed
0283         return;
0284     }
0285     QModelIndex newCurrent;
0286     if (end + 1 < d->mDirModel->rowCount()) {
0287         newCurrent = d->mDirModel->index(end + 1, 0);
0288     } else if (start > 0) {
0289         newCurrent = d->mDirModel->index(start - 1, 0);
0290     } else {
0291         // No index we can select, nothing to do
0292         return;
0293     }
0294     d->mSelectionModel->select(oldCurrent, QItemSelectionModel::Deselect);
0295     d->mSelectionModel->setCurrentIndex(newCurrent, QItemSelectionModel::Select);
0296 }
0297 
0298 bool ContextManager::currentUrlIsRasterImage() const
0299 {
0300     return MimeTypeUtils::urlKind(currentUrl()) == MimeTypeUtils::KIND_RASTER_IMAGE;
0301 }
0302 
0303 QUrl ContextManager::urlToSelect() const
0304 {
0305     return d->mUrlToSelect;
0306 }
0307 
0308 void ContextManager::setUrlToSelect(const QUrl &url)
0309 {
0310     GV_RETURN_IF_FAIL(url.isValid());
0311     d->mUrlToSelect = url;
0312 
0313     setCurrentUrl(url);
0314     selectUrlToSelect();
0315 }
0316 
0317 QUrl ContextManager::targetDirUrl() const
0318 {
0319     return d->mTargetDirUrl;
0320 }
0321 
0322 void ContextManager::setTargetDirUrl(const QUrl &url)
0323 {
0324     GV_RETURN_IF_FAIL(url.isEmpty() || url.isValid());
0325     d->mTargetDirUrl = GwenviewConfig::historyEnabled() ? url : QUrl();
0326 }
0327 
0328 void ContextManager::slotRowsInserted()
0329 {
0330     // We reach this method when rows have been inserted in the model, but views
0331     // may not have been updated yet and thus do not have the matching items.
0332     // Delay the selection of mUrlToSelect so that the view items exist.
0333     //
0334     // Without this, when Gwenview is started with an image as argument and the
0335     // thumbnail bar is visible, the image will not be selected in the thumbnail
0336     // bar.
0337     if (d->mUrlToSelect.isValid()) {
0338         QMetaObject::invokeMethod(this, &ContextManager::selectUrlToSelect, Qt::QueuedConnection);
0339     }
0340 }
0341 
0342 void ContextManager::selectUrlToSelect()
0343 {
0344     // Because of the queued connection above we might be called several times in a row
0345     // In this case we don't want the warning below
0346     if (d->mUrlToSelect.isEmpty()) {
0347         return;
0348     }
0349 
0350     GV_RETURN_IF_FAIL(d->mUrlToSelect.isValid());
0351     QModelIndex index = d->mDirModel->indexForUrl(d->mUrlToSelect);
0352     if (index.isValid()) {
0353         d->mSelectionModel->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
0354         d->mUrlToSelect = QUrl();
0355     } else if (d->mDirListerFinished) {
0356         // Desired URL cannot be found in the directory
0357         // Clear the selection to avoid dragging any local files into context
0358         // and manually set current URL
0359         d->mSelectionModel->clearSelection();
0360         setCurrentUrl(d->mUrlToSelect);
0361     }
0362 }
0363 
0364 void ContextManager::slotDirListerCompleted()
0365 {
0366     d->mDirListerFinished = true;
0367 }
0368 
0369 } // namespace
0370 
0371 #include "moc_contextmanager.cpp"