File indexing completed on 2024-04-21 03:56:25
0001 /* 0002 SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk> 0003 0004 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0005 */ 0006 0007 #include "quickengine.h" 0008 #include "cache.h" 0009 #include "errorcode.h" 0010 #include "imageloader_p.h" 0011 #include "installation_p.h" 0012 #include "knewstuffquick_debug.h" 0013 #include "quicksettings.h" 0014 0015 #include <KLocalizedString> 0016 #include <QTimer> 0017 0018 #include "categoriesmodel.h" 0019 #include "quickquestionlistener.h" 0020 #include "searchpresetmodel.h" 0021 0022 class EnginePrivate 0023 { 0024 public: 0025 bool isValid = false; 0026 CategoriesModel *categoriesModel = nullptr; 0027 SearchPresetModel *searchPresetModel = nullptr; 0028 QString configFile; 0029 QTimer searchTimer; 0030 Engine::BusyState busyState; 0031 QString busyMessage; 0032 // the current request from providers 0033 KNSCore::Provider::SearchRequest currentRequest; 0034 KNSCore::Provider::SearchRequest storedRequest; 0035 // the page that is currently displayed, so it is not requested repeatedly 0036 int currentPage = -1; 0037 0038 // when requesting entries from a provider, how many to ask for 0039 int pageSize = 20; 0040 0041 int numDataJobs = 0; 0042 int numPictureJobs = 0; 0043 int numInstallJobs = 0; 0044 }; 0045 0046 Engine::Engine(QObject *parent) 0047 : KNSCore::EngineBase(parent) 0048 , d(new EnginePrivate) 0049 { 0050 const auto setBusy = [this](Engine::BusyState state, const QString &msg) { 0051 setBusyState(state); 0052 d->busyMessage = msg; 0053 }; 0054 setBusy(BusyOperation::Initializing, i18n("Loading data")); // For the user this should be the same as initializing 0055 0056 KNewStuffQuick::QuickQuestionListener::instance(); 0057 d->categoriesModel = new CategoriesModel(this); 0058 connect(d->categoriesModel, &QAbstractListModel::modelReset, this, &Engine::categoriesChanged); 0059 d->searchPresetModel = new SearchPresetModel(this); 0060 connect(d->searchPresetModel, &QAbstractListModel::modelReset, this, &Engine::searchPresetModelChanged); 0061 0062 d->searchTimer.setSingleShot(true); 0063 d->searchTimer.setInterval(1000); 0064 connect(&d->searchTimer, &QTimer::timeout, this, &Engine::reloadEntries); 0065 connect(installation(), &KNSCore::Installation::signalInstallationFinished, this, [this]() { 0066 --d->numInstallJobs; 0067 updateStatus(); 0068 }); 0069 connect(installation(), &KNSCore::Installation::signalInstallationFailed, this, [this](const QString &message) { 0070 --d->numInstallJobs; 0071 Q_EMIT signalErrorCode(KNSCore::ErrorCode::InstallationError, message, QVariant()); 0072 }); 0073 connect(this, &EngineBase::signalProvidersLoaded, this, &Engine::updateStatus); 0074 connect(this, &EngineBase::signalProvidersLoaded, this, [this]() { 0075 d->currentRequest.categories = EngineBase::categories(); 0076 }); 0077 0078 connect(this, 0079 &KNSCore::EngineBase::signalErrorCode, 0080 this, 0081 [setBusy, this](const KNSCore::ErrorCode::ErrorCode &error, const QString &message, const QVariant &metadata) { 0082 Q_EMIT errorCode(error, message, metadata); 0083 if (error == KNSCore::ErrorCode::ProviderError || error == KNSCore::ErrorCode::ConfigFileError) { 0084 // This means loading the config or providers file failed entirely and we cannot complete the 0085 // initialisation. It also means the engine is done loading, but that nothing will 0086 // work, and we need to inform the user of this. 0087 setBusy({}, QString()); 0088 } 0089 0090 // Emit the signal later, currently QML is not connected to the slot 0091 if (error == KNSCore::ErrorCode::ConfigFileError) { 0092 QTimer::singleShot(0, [this, error, message, metadata]() { 0093 Q_EMIT errorCode(error, message, metadata); 0094 }); 0095 } 0096 }); 0097 0098 connect(this, &Engine::signalEntryEvent, this, [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) { 0099 // Just forward the event but not do anything more 0100 if (event != KNSCore::Entry::StatusChangedEvent) { 0101 Q_EMIT entryEvent(entry, event); 0102 return; 0103 } 0104 0105 // We do not want to emit the entries changed signal for intermediate changed 0106 // this would cause the KCMs to reload their view unnecessarily, BUG: 431568 0107 if (entry.status() == KNSCore::Entry::Installing || entry.status() == KNSCore::Entry::Updating) { 0108 return; 0109 } 0110 Q_EMIT entryEvent(entry, event); 0111 }); 0112 // 0113 // And finally, let's just make sure we don't miss out the various things here getting changed 0114 // In other words, when we're asked to reset the view, actually do that 0115 connect(this, &Engine::signalResetView, this, &Engine::categoriesFilterChanged); 0116 connect(this, &Engine::signalResetView, this, &Engine::filterChanged); 0117 connect(this, &Engine::signalResetView, this, &Engine::sortOrderChanged); 0118 connect(this, &Engine::signalResetView, this, &Engine::searchTermChanged); 0119 } 0120 0121 bool Engine::init(const QString &configfile) 0122 { 0123 const bool valid = EngineBase::init(configfile); 0124 if (valid) { 0125 connect(this, &Engine::signalEntryEvent, cache().data(), [this](const KNSCore::Entry &entry, KNSCore::Entry::EntryEvent event) { 0126 if (event == KNSCore::Entry::StatusChangedEvent) { 0127 cache()->registerChangedEntry(entry); 0128 } 0129 }); 0130 const auto slotEntryChanged = [this](const KNSCore::Entry &entry) { 0131 Q_EMIT signalEntryEvent(entry, KNSCore::Entry::StatusChangedEvent); 0132 }; 0133 connect(installation(), &KNSCore::Installation::signalEntryChanged, this, slotEntryChanged); 0134 connect(cache().data(), &KNSCore::Cache::entryChanged, this, slotEntryChanged); 0135 } 0136 return valid; 0137 } 0138 void Engine::updateStatus() 0139 { 0140 QString busyMessage; 0141 BusyState state; 0142 if (d->numPictureJobs > 0) { 0143 // If it is loading previews or data is irrelevant for the user 0144 busyMessage = i18n("Loading data"); 0145 state |= BusyOperation::LoadingPreview; 0146 } 0147 if (d->numInstallJobs > 0) { 0148 busyMessage = i18n("Installing"); 0149 state |= BusyOperation::InstallingEntry; 0150 } 0151 if (d->numDataJobs > 0) { 0152 busyMessage = i18n("Loading data"); 0153 state |= BusyOperation::LoadingData; 0154 } 0155 d->busyMessage = busyMessage; 0156 setBusyState(state); 0157 } 0158 0159 bool Engine::needsLazyLoadSpinner() 0160 { 0161 return d->numDataJobs > 0 || d->numPictureJobs; 0162 } 0163 0164 Engine::~Engine() = default; 0165 0166 void Engine::setBusyState(BusyState state) 0167 { 0168 d->busyState = state; 0169 Q_EMIT busyStateChanged(); 0170 } 0171 Engine::BusyState Engine::busyState() const 0172 { 0173 return d->busyState; 0174 } 0175 QString Engine::busyMessage() const 0176 { 0177 return d->busyMessage; 0178 } 0179 0180 QString Engine::configFile() const 0181 { 0182 return d->configFile; 0183 } 0184 0185 void Engine::setConfigFile(const QString &newFile) 0186 { 0187 if (d->configFile != newFile) { 0188 d->configFile = newFile; 0189 Q_EMIT configFileChanged(); 0190 0191 if (KNewStuffQuick::Settings::instance()->allowedByKiosk()) { 0192 d->isValid = init(newFile); 0193 Q_EMIT categoriesFilterChanged(); 0194 Q_EMIT filterChanged(); 0195 Q_EMIT sortOrderChanged(); 0196 Q_EMIT searchTermChanged(); 0197 } else { 0198 // This is not an error message in the proper sense, and the message is not intended to look like an error (as there is really 0199 // nothing the user can do to fix it, and we just tell them so they're not wondering what's wrong) 0200 Q_EMIT errorCode( 0201 KNSCore::ErrorCode::ConfigFileError, 0202 i18nc("An informational message which is shown to inform the user they are not authorized to use GetHotNewStuff functionality", 0203 "You are not authorized to Get Hot New Stuff. If you think this is in error, please contact the person in charge of your permissions."), 0204 QVariant()); 0205 } 0206 } 0207 } 0208 0209 QObject *Engine::categories() const 0210 { 0211 return d->categoriesModel; 0212 } 0213 0214 QStringList Engine::categoriesFilter() const 0215 { 0216 return d->currentRequest.categories; 0217 } 0218 0219 void Engine::setCategoriesFilter(const QStringList &newCategoriesFilter) 0220 { 0221 if (d->currentRequest.categories != newCategoriesFilter) { 0222 d->currentRequest.categories = newCategoriesFilter; 0223 reloadEntries(); 0224 Q_EMIT categoriesFilterChanged(); 0225 } 0226 } 0227 0228 KNSCore::Provider::Filter Engine::filter() const 0229 { 0230 return d->currentRequest.filter; 0231 } 0232 0233 void Engine::setFilter(KNSCore::Provider::Filter newFilter) 0234 { 0235 if (d->currentRequest.filter != newFilter) { 0236 d->currentRequest.filter = newFilter; 0237 reloadEntries(); 0238 Q_EMIT filterChanged(); 0239 } 0240 } 0241 0242 KNSCore::Provider::SortMode Engine::sortOrder() const 0243 { 0244 return d->currentRequest.sortMode; 0245 } 0246 0247 void Engine::setSortOrder(KNSCore::Provider::SortMode mode) 0248 { 0249 if (d->currentRequest.sortMode != mode) { 0250 d->currentRequest.sortMode = mode; 0251 reloadEntries(); 0252 Q_EMIT sortOrderChanged(); 0253 } 0254 } 0255 0256 QString Engine::searchTerm() const 0257 { 0258 return d->currentRequest.searchTerm; 0259 } 0260 0261 void Engine::setSearchTerm(const QString &searchTerm) 0262 { 0263 if (d->isValid && d->currentRequest.searchTerm != searchTerm) { 0264 d->currentRequest.searchTerm = searchTerm; 0265 Q_EMIT searchTermChanged(); 0266 } 0267 KNSCore::Entry::List cacheEntries = cache()->requestFromCache(d->currentRequest); 0268 if (!cacheEntries.isEmpty()) { 0269 reloadEntries(); 0270 } else { 0271 d->searchTimer.start(); 0272 } 0273 } 0274 0275 QObject *Engine::searchPresetModel() const 0276 { 0277 return d->searchPresetModel; 0278 } 0279 0280 bool Engine::isValid() 0281 { 0282 return d->isValid; 0283 } 0284 0285 void Engine::updateEntryContents(const KNSCore::Entry &entry) 0286 { 0287 const auto provider = EngineBase::provider(entry.providerId()); 0288 if (provider.isNull() || !provider->isInitialized()) { 0289 qCWarning(KNEWSTUFFQUICK) << "Provider was not found or is not initialized" << provider << entry.providerId(); 0290 return; 0291 } 0292 provider->loadEntryDetails(entry); 0293 } 0294 0295 void Engine::reloadEntries() 0296 { 0297 Q_EMIT signalResetView(); 0298 d->currentPage = -1; 0299 d->currentRequest.page = 0; 0300 d->numDataJobs = 0; 0301 0302 const auto providersList = EngineBase::providers(); 0303 for (const QSharedPointer<KNSCore::Provider> &p : providersList) { 0304 if (p->isInitialized()) { 0305 if (d->currentRequest.filter == KNSCore::Provider::Installed) { 0306 // when asking for installed entries, never use the cache 0307 p->loadEntries(d->currentRequest); 0308 } else { 0309 // take entries from cache until there are no more 0310 KNSCore::Entry::List cacheEntries; 0311 KNSCore::Entry::List lastCache = cache()->requestFromCache(d->currentRequest); 0312 while (!lastCache.isEmpty()) { 0313 qCDebug(KNEWSTUFFQUICK) << "From cache"; 0314 cacheEntries << lastCache; 0315 0316 d->currentPage = d->currentRequest.page; 0317 ++d->currentRequest.page; 0318 lastCache = cache()->requestFromCache(d->currentRequest); 0319 } 0320 0321 // Since the cache has no more pages, reset the request's page 0322 if (d->currentPage >= 0) { 0323 d->currentRequest.page = d->currentPage; 0324 } 0325 0326 if (!cacheEntries.isEmpty()) { 0327 Q_EMIT signalEntriesLoaded(cacheEntries); 0328 } else { 0329 qCDebug(KNEWSTUFFQUICK) << "From provider"; 0330 p->loadEntries(d->currentRequest); 0331 0332 ++d->numDataJobs; 0333 updateStatus(); 0334 } 0335 } 0336 } 0337 } 0338 } 0339 void Engine::addProvider(QSharedPointer<KNSCore::Provider> provider) 0340 { 0341 EngineBase::addProvider(provider); 0342 connect(provider.data(), &KNSCore::Provider::loadingFinished, this, [this](const auto &request, const auto &entries) { 0343 d->currentPage = qMax<int>(request.page, d->currentPage); 0344 qCDebug(KNEWSTUFFQUICK) << "loaded page " << request.page << "current page" << d->currentPage << "count:" << entries.count(); 0345 0346 if (request.filter != KNSCore::Provider::Updates) { 0347 cache()->insertRequest(request, entries); 0348 } 0349 Q_EMIT signalEntriesLoaded(entries); 0350 0351 --d->numDataJobs; 0352 updateStatus(); 0353 }); 0354 connect(provider.data(), &KNSCore::Provider::entryDetailsLoaded, this, [this](const auto &entry) { 0355 --d->numDataJobs; 0356 updateStatus(); 0357 Q_EMIT signalEntryEvent(entry, KNSCore::Entry::DetailsLoadedEvent); 0358 }); 0359 } 0360 0361 void Engine::loadPreview(const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type) 0362 { 0363 qCDebug(KNEWSTUFFQUICK) << "START preview: " << entry.name() << type; 0364 auto l = new KNSCore::ImageLoader(entry, type, this); 0365 connect(l, &KNSCore::ImageLoader::signalPreviewLoaded, this, [this](const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type) { 0366 qCDebug(KNEWSTUFFQUICK) << "FINISH preview: " << entry.name() << type; 0367 Q_EMIT signalEntryPreviewLoaded(entry, type); 0368 --d->numPictureJobs; 0369 updateStatus(); 0370 }); 0371 connect(l, &KNSCore::ImageLoader::signalError, this, [this](const KNSCore::Entry &entry, KNSCore::Entry::PreviewType type, const QString &errorText) { 0372 Q_EMIT signalErrorCode(KNSCore::ErrorCode::ImageError, errorText, QVariantList() << entry.name() << type); 0373 qCDebug(KNEWSTUFFQUICK) << "ERROR preview: " << errorText << entry.name() << type; 0374 --d->numPictureJobs; 0375 updateStatus(); 0376 }); 0377 l->start(); 0378 ++d->numPictureJobs; 0379 updateStatus(); 0380 } 0381 0382 void Engine::adoptEntry(const KNSCore::Entry &entry) 0383 { 0384 registerTransaction(KNSCore::Transaction::adopt(this, entry)); 0385 } 0386 void Engine::install(const KNSCore::Entry &entry, int linkId) 0387 { 0388 auto transaction = KNSCore::Transaction::install(this, entry, linkId); 0389 registerTransaction(transaction); 0390 if (!transaction->isFinished()) { 0391 ++d->numInstallJobs; 0392 } 0393 } 0394 void Engine::uninstall(const KNSCore::Entry &entry) 0395 { 0396 registerTransaction(KNSCore::Transaction::uninstall(this, entry)); 0397 } 0398 void Engine::registerTransaction(KNSCore::Transaction *transaction) 0399 { 0400 connect(transaction, &KNSCore::Transaction::signalErrorCode, this, &EngineBase::signalErrorCode); 0401 connect(transaction, &KNSCore::Transaction::signalMessage, this, &EngineBase::signalMessage); 0402 connect(transaction, &KNSCore::Transaction::signalEntryEvent, this, &Engine::signalEntryEvent); 0403 } 0404 0405 void Engine::requestMoreData() 0406 { 0407 qCDebug(KNEWSTUFFQUICK) << "Get more data! current page: " << d->currentPage << " requested: " << d->currentRequest.page; 0408 0409 if (d->currentPage < d->currentRequest.page) { 0410 return; 0411 } 0412 0413 d->currentRequest.page++; 0414 doRequest(); 0415 } 0416 void Engine::doRequest() 0417 { 0418 const auto providersList = providers(); 0419 for (const QSharedPointer<KNSCore::Provider> &p : providersList) { 0420 if (p->isInitialized()) { 0421 p->loadEntries(d->currentRequest); 0422 ++d->numDataJobs; 0423 updateStatus(); 0424 } 0425 } 0426 } 0427 0428 void Engine::revalidateCacheEntries() 0429 { 0430 // This gets called from QML, because in QtQuick we reuse the engine, BUG: 417985 0431 // We can't handle this in the cache, because it can't access the configuration of the engine 0432 if (cache()) { 0433 const auto providersList = providers(); 0434 for (const auto &provider : providersList) { 0435 if (provider && provider->isInitialized()) { 0436 const KNSCore::Entry::List cacheBefore = cache()->registryForProvider(provider->id()); 0437 cache()->removeDeletedEntries(); 0438 const KNSCore::Entry::List cacheAfter = cache()->registryForProvider(provider->id()); 0439 // If the user has deleted them in the background we have to update the state to deleted 0440 for (const auto &oldCachedEntry : cacheBefore) { 0441 if (!cacheAfter.contains(oldCachedEntry)) { 0442 KNSCore::Entry removedEntry = oldCachedEntry; 0443 removedEntry.setEntryDeleted(); 0444 Q_EMIT signalEntryEvent(removedEntry, KNSCore::Entry::StatusChangedEvent); 0445 } 0446 } 0447 } 0448 } 0449 } 0450 } 0451 0452 void Engine::restoreSearch() 0453 { 0454 d->searchTimer.stop(); 0455 d->currentRequest = d->storedRequest; 0456 if (cache()) { 0457 KNSCore::Entry::List cacheEntries = cache()->requestFromCache(d->currentRequest); 0458 if (!cacheEntries.isEmpty()) { 0459 reloadEntries(); 0460 } else { 0461 d->searchTimer.start(); 0462 } 0463 } else { 0464 qCWarning(KNEWSTUFFQUICK) << "Attempted to call restoreSearch() without a correctly initialized engine. You will likely get unexpected behaviour."; 0465 } 0466 } 0467 0468 void Engine::storeSearch() 0469 { 0470 d->storedRequest = d->currentRequest; 0471 } 0472 0473 #include "moc_quickengine.cpp"