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 ¤tUrl) 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"