File indexing completed on 2024-05-05 05:29:11

0001 /*
0002  *   SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0003  *
0004  *   SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "DiscoverObject.h"
0008 #include "CachedNetworkAccessManager.h"
0009 #include "DiscoverBackendsFactory.h"
0010 #include "DiscoverDeclarativePlugin.h"
0011 #include "FeaturedModel.h"
0012 #include "OdrsAppsModel.h"
0013 #include "PaginateModel.h"
0014 #include "UnityLauncher.h"
0015 #include <Transaction/TransactionModel.h>
0016 
0017 // Qt includes
0018 #include "discover_debug.h"
0019 #include <QClipboard>
0020 #include <QDBusConnection>
0021 #include <QDBusMessage>
0022 #include <QDBusPendingCall>
0023 #include <QDesktopServices>
0024 #include <QFile>
0025 #include <QGuiApplication>
0026 #include <QPointer>
0027 #include <QQmlApplicationEngine>
0028 #include <QQmlContext>
0029 #include <QQmlEngine>
0030 #include <QSessionManager>
0031 #include <QSortFilterProxyModel>
0032 #include <QTimer>
0033 #include <QtQuick/QQuickItem>
0034 #include <qqml.h>
0035 
0036 // KDE includes
0037 #include <KAboutData>
0038 #include <KAuthorized>
0039 #include <KConfigGroup>
0040 #include <KCrash>
0041 #include <KLocalizedContext>
0042 #include <KLocalizedString>
0043 #include <KOSRelease>
0044 #include <KSharedConfig>
0045 #include <KStatusNotifierItem>
0046 #include <KUiServerV2JobTracker>
0047 #include <kcoreaddons_version.h>
0048 // #include <KSwitchLanguageDialog>
0049 
0050 // DiscoverCommon includes
0051 #include <Category/Category.h>
0052 #include <Category/CategoryModel.h>
0053 #include <resources/AbstractResource.h>
0054 #include <resources/ResourcesModel.h>
0055 #include <resources/ResourcesProxyModel.h>
0056 
0057 #include <QMimeDatabase>
0058 #include <cmath>
0059 #include <functional>
0060 #include <resources/StoredResultsStream.h>
0061 #include <unistd.h>
0062 #include <utils.h>
0063 
0064 #ifdef WITH_FEEDBACK
0065 #include "plasmauserfeedback.h"
0066 #endif
0067 #include "PowerManagementInterface.h"
0068 #include "discoversettings.h"
0069 
0070 using namespace Qt::StringLiterals;
0071 
0072 class CachedNetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
0073 {
0074     virtual QNetworkAccessManager *create(QObject *parent) override
0075     {
0076         return new CachedNetworkAccessManager(QStringLiteral("images"), parent);
0077     }
0078 };
0079 
0080 class OurSortFilterProxyModel : public QSortFilterProxyModel, public QQmlParserStatus
0081 {
0082     Q_OBJECT
0083     Q_INTERFACES(QQmlParserStatus)
0084 public:
0085     void classBegin() override
0086     {
0087     }
0088     void componentComplete() override
0089     {
0090         if (dynamicSortFilter())
0091             sort(0);
0092     }
0093 };
0094 
0095 static void scheduleShutdownWithErrorCode()
0096 {
0097     QTimer::singleShot(0, QCoreApplication::instance(), []() {
0098         QCoreApplication::instance()->exit(1);
0099     });
0100 }
0101 
0102 DiscoverObject::DiscoverObject(const QVariantMap &initialProperties)
0103     : QObject()
0104     , m_engine(new QQmlApplicationEngine)
0105     , m_networkAccessManagerFactory(new CachedNetworkAccessManagerFactory)
0106 {
0107     setObjectName(QStringLiteral("DiscoverMain"));
0108     m_engine->rootContext()->setContextObject(new KLocalizedContext(m_engine));
0109     auto factory = m_engine->networkAccessManagerFactory();
0110     m_engine->setNetworkAccessManagerFactory(nullptr);
0111     delete factory;
0112     m_engine->setNetworkAccessManagerFactory(m_networkAccessManagerFactory.data());
0113 
0114     const auto uriApp = "org.kde.discover.app";
0115 
0116     qmlRegisterType<UnityLauncher>(uriApp, 1, 0, "UnityLauncher");
0117     qmlRegisterType<PaginateModel>(uriApp, 1, 0, "PaginateModel");
0118     qmlRegisterType<FeaturedModel>(uriApp, 1, 0, "FeaturedModel");
0119     qmlRegisterType<OdrsAppsModel>(uriApp, 1, 0, "OdrsAppsModel");
0120     qmlRegisterType<PowerManagementInterface>(uriApp, 1, 0, "PowerManagementInterface");
0121     qmlRegisterType<OurSortFilterProxyModel>(uriApp, 1, 0, "QSortFilterProxyModel");
0122 #ifdef WITH_FEEDBACK
0123     qmlRegisterSingletonType<PlasmaUserFeedback>(uriApp, 1, 0, "UserFeedbackSettings", [](QQmlEngine *, QJSEngine *) -> QObject * {
0124         return new PlasmaUserFeedback(KSharedConfig::openConfig(QStringLiteral("PlasmaUserFeedback"), KConfig::NoGlobals));
0125     });
0126 #endif
0127     qmlRegisterSingletonType<DiscoverSettings>(uriApp, 1, 0, "DiscoverSettings", [](QQmlEngine *engine, QJSEngine *) -> QObject * {
0128         auto r = new DiscoverSettings;
0129         r->setParent(engine);
0130         connect(r, &DiscoverSettings::installedPageSortingChanged, r, &DiscoverSettings::save);
0131         connect(r, &DiscoverSettings::appsListPageSortingChanged, r, &DiscoverSettings::save);
0132         return r;
0133     });
0134     qmlRegisterAnonymousType<QQuickView>(uriApp, 1);
0135 
0136     qmlRegisterAnonymousType<KAboutData>(uriApp, 1);
0137     qmlRegisterAnonymousType<KAboutLicense>(uriApp, 1);
0138     qmlRegisterAnonymousType<KAboutPerson>(uriApp, 1);
0139     qmlRegisterAnonymousType<DiscoverObject>(uriApp, 1);
0140 
0141     auto uri = "org.kde.discover";
0142     DiscoverDeclarativePlugin *plugin = new DiscoverDeclarativePlugin;
0143     plugin->setParent(this);
0144     plugin->initializeEngine(m_engine, uri);
0145     plugin->registerTypes(uri);
0146 
0147     m_engine->rootContext()->setContextProperty(QStringLiteral("app"), this);
0148     m_engine->rootContext()->setContextProperty(QStringLiteral("discoverAboutData"), QVariant::fromValue(KAboutData::applicationData()));
0149 
0150     const auto discoverQmlUri = u"org.kde.discover.qml"_s;
0151 
0152     auto navigation = m_engine->singletonInstance<QObject *>(discoverQmlUri, u"Navigation"_s);
0153     if (!navigation) {
0154         qCWarning(DISCOVER_LOG) << "Failed to import Navigation singleton";
0155         scheduleShutdownWithErrorCode();
0156         return;
0157     }
0158 
0159     {
0160         QQmlComponent component(m_engine, discoverQmlUri, u"DiscoverWindow"_s);
0161         auto object = component.beginCreate(m_engine->rootContext());
0162         auto window = qobject_cast<QQuickWindow *>(object);
0163         if (!object || !window) {
0164             qCWarning(DISCOVER_LOG) << "Failed to create main window:" << component.errorString();
0165             scheduleShutdownWithErrorCode();
0166             component.completeCreate();
0167             if (object) {
0168                 delete object;
0169             }
0170             return;
0171         }
0172         navigation->setProperty("window", QVariant::fromValue(object));
0173         component.setInitialProperties(object, initialProperties);
0174         component.completeCreate();
0175         initMainWindow(window);
0176     }
0177 
0178     auto action = new OneTimeAction(
0179         [this]() {
0180             bool found = DiscoverBackendsFactory::hasRequestedBackends();
0181             const auto backends = ResourcesModel::global()->backends();
0182             for (auto b : backends) {
0183                 found |= b->hasApplications();
0184             }
0185 
0186             if (!found) {
0187                 const QString distroName = KOSRelease().name();
0188 
0189                 QString errorText =
0190                     i18nc("@title %1 is the distro name", "%1 is not configured for installing apps through Discover—only app add-ons", distroName);
0191                 QString errorExplanation = xi18nc("@info:usagetip %1 is the distro name",
0192                                                   "To use Discover for apps, install your preferred module on the <interface>Settings"
0193                                                   "</interface> page, under <interface>Missing Backends</interface>.");
0194                 QString buttonIcon = QStringLiteral("tools-report-bug");
0195                 QString buttonText = i18nc("@action:button %1 is the distro name", "Report This Issue to %1", distroName);
0196                 QString buttonUrl = KOSRelease().bugReportUrl();
0197 
0198                 if (distroName.contains(QStringLiteral("Arch Linux"))) {
0199                     errorExplanation = xi18nc("@info:usagetip %1 is the distro name; in this case it always contains 'Arch Linux'",
0200                                               "To use Discover for apps, install"
0201                                               " <link url='https://wiki.archlinux.org/title/Flatpak#Installation'>Flatpak</link> or"
0202                                               " <link url='https://wiki.archlinux.org/title/KDE#Discover_does_not_show_any_applications'>PackageKit</link>"
0203                                               " using the <command>pacman</command> package manager.<nl/><nl/>"
0204                                               " Review <link url='https://archlinux.org/packages/extra/x86_64/discover/'>%1's packaging for Discover</link>",
0205                                               distroName);
0206                     buttonIcon = QString();
0207                     buttonText = QString();
0208                     buttonUrl = QString();
0209                 }
0210 
0211                 Q_EMIT openErrorPage(errorText, errorExplanation, buttonText, buttonIcon, buttonUrl);
0212             }
0213         },
0214         this);
0215 
0216     if (ResourcesModel::global()->backends().isEmpty()) {
0217         connect(ResourcesModel::global(), &ResourcesModel::allInitialized, action, &OneTimeAction::trigger);
0218     } else {
0219         action->trigger();
0220     }
0221 }
0222 
0223 DiscoverObject::~DiscoverObject()
0224 {
0225     m_engine->deleteLater();
0226 }
0227 
0228 bool DiscoverObject::isRoot()
0229 {
0230     return ::getuid() == 0;
0231 }
0232 
0233 QStringList DiscoverObject::modes() const
0234 {
0235     QStringList ret;
0236     QObject *obj = mainWindow();
0237     for (int i = obj->metaObject()->propertyOffset(); i < obj->metaObject()->propertyCount(); i++) {
0238         QMetaProperty p = obj->metaObject()->property(i);
0239         QByteArray compName = p.name();
0240         if (compName.startsWith("top") && compName.endsWith("Comp")) {
0241             ret += QString::fromLatin1(compName.mid(3, compName.length() - 7));
0242         }
0243     }
0244     return ret;
0245 }
0246 
0247 void DiscoverObject::openMode(const QString &_mode)
0248 {
0249     if (!m_mainWindow) {
0250         qCWarning(DISCOVER_LOG) << "could not get the main object";
0251         return;
0252     }
0253 
0254     if (!modes().contains(_mode, Qt::CaseInsensitive))
0255         qCWarning(DISCOVER_LOG) << "unknown mode" << _mode << modes();
0256 
0257     QString mode = _mode;
0258     mode[0] = mode[0].toUpper();
0259 
0260     const QByteArray propertyName = "top" + mode.toLatin1() + "Comp";
0261     const QVariant modeComp = m_mainWindow->property(propertyName.constData());
0262     if (!modeComp.isValid()) {
0263         qCWarning(DISCOVER_LOG) << "couldn't open mode" << _mode;
0264     } else {
0265         m_mainWindow->setProperty("currentTopLevel", modeComp);
0266     }
0267 }
0268 
0269 void DiscoverObject::openMimeType(const QString &mime)
0270 {
0271     Q_EMIT listMimeInternal(mime);
0272 }
0273 
0274 void DiscoverObject::showLoadingPage()
0275 {
0276     if (m_mainWindow) {
0277         m_mainWindow->setProperty("currentTopLevel", QStringLiteral(DISCOVER_BASE_URL "/LoadingPage.qml"));
0278     }
0279 }
0280 
0281 void DiscoverObject::openCategory(const QString &category)
0282 {
0283     showLoadingPage();
0284     auto action = new OneTimeAction(
0285         [this, category]() {
0286             Category *cat = CategoryModel::global()->findCategoryByName(category);
0287             if (cat) {
0288                 Q_EMIT listCategoryInternal(cat);
0289             } else {
0290                 openMode(QStringLiteral("Browsing"));
0291                 showError(i18n("Could not find category '%1'", category));
0292             }
0293         },
0294         this);
0295 
0296     if (CategoryModel::global()->rootCategories().isEmpty()) {
0297         connect(CategoryModel::global(), &CategoryModel::rootCategoriesChanged, action, &OneTimeAction::trigger);
0298     } else {
0299         action->trigger();
0300     }
0301 }
0302 
0303 void DiscoverObject::openLocalPackage(const QUrl &localfile)
0304 {
0305     if (!QFile::exists(localfile.toLocalFile())) {
0306         showError(i18n("Trying to open inexisting file '%1'", localfile.toString()));
0307         openMode(QStringLiteral("Browsing"));
0308         return;
0309     }
0310     showLoadingPage();
0311     auto action = new OneTimeAction(
0312         [this, localfile]() {
0313             AbstractResourcesBackend::Filters f;
0314             f.resourceUrl = localfile;
0315             auto stream = new StoredResultsStream({ResourcesModel::global()->search(f)});
0316             connect(stream, &StoredResultsStream::finishedResources, this, [this, localfile](const QVector<StreamResult> &res) {
0317                 if (res.count() == 1) {
0318                     Q_EMIT openApplicationInternal(res.first().resource);
0319                 } else {
0320                     QMimeDatabase db;
0321                     auto mime = db.mimeTypeForUrl(localfile);
0322                     auto fIsFlatpakBackend = [](AbstractResourcesBackend *backend) {
0323                         return backend->metaObject()->className() == QByteArray("FlatpakBackend");
0324                     };
0325                     if (mime.name().startsWith(QLatin1String("application/vnd.flatpak"))
0326                         && !kContains(ResourcesModel::global()->backends(), fIsFlatpakBackend)) {
0327                         openApplication(QUrl(QStringLiteral("appstream://org.kde.discover.flatpak")));
0328                         showError(i18n("Cannot interact with flatpak resources without the flatpak backend %1. Please install it first.",
0329                                        localfile.toDisplayString()));
0330                     } else {
0331                         openMode(QStringLiteral("Browsing"));
0332                         showError(i18n("Could not open %1", localfile.toDisplayString()));
0333                     }
0334                 }
0335             });
0336         },
0337         this);
0338 
0339     if (ResourcesModel::global()->backends().isEmpty()) {
0340         connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, action, &OneTimeAction::trigger);
0341     } else {
0342         action->trigger();
0343     }
0344 }
0345 
0346 void DiscoverObject::openApplication(const QUrl &url)
0347 {
0348     Q_ASSERT(!url.isEmpty());
0349     showLoadingPage();
0350     auto action = new OneTimeAction(
0351         [this, url]() {
0352             AbstractResourcesBackend::Filters f;
0353             f.resourceUrl = url;
0354             auto stream = new StoredResultsStream({ResourcesModel::global()->search(f)});
0355             connect(stream, &StoredResultsStream::finishedResources, this, [this, url](const QVector<StreamResult> &res) {
0356                 if (res.count() >= 1) {
0357                     QPointer<QTimer> timeout = new QTimer(this);
0358                     timeout->setSingleShot(true);
0359                     timeout->setInterval(20000);
0360                     connect(timeout, &QTimer::timeout, timeout, &QTimer::deleteLater);
0361 
0362                     auto openResourceOrWait = [this, res, timeout] {
0363                         auto idx = kIndexOf(res, [](auto res) {
0364                             return res.resource->isInstalled();
0365                         });
0366                         if (idx < 0) {
0367                             bool oneBroken = kContains(res, [](auto res) {
0368                                 return res.resource->state() == AbstractResource::Broken;
0369                             });
0370                             if (oneBroken && timeout) {
0371                                 return false;
0372                             }
0373 
0374                             idx = 0;
0375                         }
0376                         Q_EMIT openApplicationInternal(res[idx].resource);
0377                         return true;
0378                     };
0379 
0380                     if (!openResourceOrWait()) {
0381                         auto f = new OneTimeAction(0, openResourceOrWait, this);
0382                         for (auto r : res) {
0383                             if (r.resource->state() == AbstractResource::Broken) {
0384                                 connect(r.resource, &AbstractResource::stateChanged, f, &OneTimeAction::trigger);
0385                             }
0386                         }
0387                         timeout->start();
0388                         connect(timeout, &QTimer::destroyed, f, &OneTimeAction::trigger);
0389                     } else {
0390                         delete timeout;
0391                     }
0392                 } else if (url.scheme() == QLatin1String("snap")) {
0393                     openApplication(QUrl(QStringLiteral("appstream://org.kde.discover.snap")));
0394                     showError(i18n("Please make sure Snap support is installed"));
0395                 } else {
0396                     const QString errorText = i18n("Could not open %1 because it "
0397                     "was not found in any available software repositories.",
0398                     url.toDisplayString());
0399                     const QString errorExplanation = i18n("Please report this "
0400                     "issue to the packagers of your distribution.");
0401                     QString buttonIcon = QStringLiteral("tools-report-bug");
0402                     QString buttonText = i18n("Report This Issue");
0403                     QString buttonUrl = KOSRelease().bugReportUrl();
0404                     Q_EMIT openErrorPage(errorText, errorExplanation, buttonText, buttonIcon, buttonUrl);
0405                 }
0406             });
0407         },
0408         this);
0409 
0410     if (ResourcesModel::global()->backends().isEmpty()) {
0411         connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, action, &OneTimeAction::trigger);
0412     } else {
0413         action->trigger();
0414     }
0415 }
0416 
0417 class TransactionsJob : public KJob
0418 {
0419 public:
0420     void start() override
0421     {
0422         // no-op, this is just observing
0423 
0424         setTotalAmount(Items, TransactionModel::global()->rowCount());
0425         setPercent(TransactionModel::global()->progress());
0426         connect(TransactionModel::global(), &TransactionModel::lastTransactionFinished, this, &TransactionsJob::emitResult);
0427         connect(TransactionModel::global(), &TransactionModel::transactionRemoved, this, &TransactionsJob::refreshInfo);
0428         connect(TransactionModel::global(), &TransactionModel::progressChanged, this, [this] {
0429             setPercent(TransactionModel::global()->progress());
0430         });
0431         refreshInfo();
0432     }
0433 
0434     void refreshInfo()
0435     {
0436         if (TransactionModel::global()->rowCount() == 0) {
0437             return;
0438         }
0439 
0440         setProcessedAmount(Items, totalAmount(Items) - TransactionModel::global()->rowCount() + 1);
0441         auto firstTransaction = TransactionModel::global()->transactions().constFirst();
0442         Q_EMIT description(this, firstTransaction->name());
0443     }
0444 
0445     void cancel()
0446     {
0447         setError(KJob::KilledJobError /*KIO::ERR_USER_CANCELED*/);
0448         deleteLater();
0449     }
0450 };
0451 
0452 bool DiscoverObject::quitWhenIdle()
0453 {
0454     if (!ResourcesModel::global()->isBusy()) {
0455         return true;
0456     }
0457 
0458     if (!m_sni) {
0459         auto tracker = new KUiServerV2JobTracker(m_sni);
0460 
0461         m_sni = new KStatusNotifierItem(this);
0462         m_sni->setStatus(KStatusNotifierItem::Active);
0463         m_sni->setIconByName(QStringLiteral("plasmadiscover"));
0464         m_sni->setTitle(i18n("Discover"));
0465         m_sni->setToolTip(QStringLiteral("process-working-symbolic"),
0466                           i18n("Discover"),
0467                           i18n("Discover was closed before certain tasks were done, waiting for it to finish."));
0468         m_sni->setStandardActionsEnabled(false);
0469 
0470         connect(TransactionModel::global(), &TransactionModel::countChanged, this, &DiscoverObject::reconsiderQuit);
0471         connect(m_sni, &KStatusNotifierItem::activateRequested, this, &DiscoverObject::restore);
0472 
0473         auto job = new TransactionsJob;
0474         job->setParent(this);
0475         tracker->registerJob(job);
0476         job->start();
0477         connect(m_sni, &KStatusNotifierItem::activateRequested, job, &TransactionsJob::cancel);
0478 
0479         m_mainWindow->hide();
0480     }
0481     return false;
0482 }
0483 
0484 void DiscoverObject::restore()
0485 {
0486     if (!m_sni || !m_mainWindow) {
0487         return;
0488     }
0489 
0490     disconnect(TransactionModel::global(), &TransactionModel::countChanged, this, &DiscoverObject::reconsiderQuit);
0491     disconnect(m_sni, &KStatusNotifierItem::activateRequested, this, &DiscoverObject::restore);
0492 
0493     m_mainWindow->show();
0494     m_sni->deleteLater();
0495     m_sni = nullptr;
0496 }
0497 
0498 void DiscoverObject::reconsiderQuit()
0499 {
0500     if (ResourcesModel::global()->isBusy()) {
0501         return;
0502     }
0503 
0504     m_sni->deleteLater();
0505     // Let the job UI to finalise properly
0506     QTimer::singleShot(20, qGuiApp, &QCoreApplication::quit);
0507 }
0508 
0509 void DiscoverObject::initMainWindow(QQuickWindow *mainWindow)
0510 {
0511     Q_ASSERT(mainWindow);
0512 
0513     m_mainWindow = std::unique_ptr<QQuickWindow>(mainWindow);
0514 
0515     KConfigGroup window(KSharedConfig::openConfig(), u"Window"_s);
0516     if (window.hasKey("geometry"))
0517         m_mainWindow->setGeometry(window.readEntry("geometry", QRect()));
0518     if (window.hasKey("visibility")) {
0519         QWindow::Visibility visibility(QWindow::Visibility(window.readEntry<int>("visibility", QWindow::Windowed)));
0520         m_mainWindow->setVisibility(qMax(visibility, QQuickView::AutomaticVisibility));
0521     }
0522     connect(m_mainWindow.get(), &QQuickView::sceneGraphError, this, [](QQuickWindow::SceneGraphError /*error*/, const QString &message) {
0523         KCrash::setErrorMessage(message);
0524         qFatal("%s", qPrintable(message));
0525     });
0526 
0527     m_mainWindow->installEventFilter(this);
0528 
0529     connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
0530         m_mainWindow.reset();
0531     });
0532 
0533     connect(qGuiApp, &QGuiApplication::commitDataRequest, this, [this](QSessionManager &sessionManager) {
0534         if (!quitWhenIdle()) {
0535             sessionManager.cancel();
0536         }
0537     });
0538 }
0539 
0540 bool DiscoverObject::eventFilter(QObject *object, QEvent *event)
0541 {
0542     if (object != m_mainWindow.get()) {
0543         return false;
0544     }
0545 
0546     if (event->type() == QEvent::Close) {
0547         if (!quitWhenIdle()) {
0548             return true;
0549         }
0550 
0551         KConfigGroup window(KSharedConfig::openConfig(), u"Window"_s);
0552         window.writeEntry("geometry", m_mainWindow->geometry());
0553         window.writeEntry<int>("visibility", m_mainWindow->visibility());
0554     }
0555     // } else if (event->type() == QEvent::ShortcutOverride) {
0556     //     qCWarning(DISCOVER_LOG) << "Action conflict" << event;
0557     // }
0558     return false;
0559 }
0560 
0561 QString DiscoverObject::iconName(const QIcon &icon)
0562 {
0563     return icon.name();
0564 }
0565 
0566 void DiscoverObject::switchApplicationLanguage()
0567 {
0568     // auto langDialog = new KSwitchLanguageDialog(nullptr);
0569     // connect(langDialog, SIGNAL(finished(int)), this, SLOT(dialogFinished()));
0570     // langDialog->show();
0571 }
0572 
0573 class DiscoverTestExecutor : public QObject
0574 {
0575 public:
0576     DiscoverTestExecutor(QQuickWindow *mainWindow, QQmlEngine *engine, const QUrl &url)
0577         : QObject(engine)
0578     {
0579         connect(engine, &QQmlEngine::quit, this, &DiscoverTestExecutor::finish, Qt::QueuedConnection);
0580 
0581         QQmlComponent *component = new QQmlComponent(engine, url, engine);
0582         m_testObject = component->create(engine->rootContext());
0583 
0584         if (!m_testObject) {
0585             qCWarning(DISCOVER_LOG) << "error loading test" << url << m_testObject << component->errors();
0586             Q_ASSERT(false);
0587         }
0588 
0589         m_testObject->setProperty("appRoot", QVariant::fromValue(mainWindow));
0590         connect(engine, &QQmlEngine::warnings, this, &DiscoverTestExecutor::processWarnings);
0591     }
0592 
0593     void processWarnings(const QList<QQmlError> &warnings)
0594     {
0595         for (const QQmlError &warning : warnings) {
0596             if (warning.url().path().endsWith(QLatin1String("DiscoverTest.qml"))) {
0597                 qCWarning(DISCOVER_LOG) << "Test failed!" << warnings;
0598                 qGuiApp->exit(1);
0599             }
0600         }
0601         m_warnings << warnings;
0602     }
0603 
0604     void finish()
0605     {
0606         if (m_warnings.isEmpty())
0607             qCDebug(DISCOVER_LOG) << "cool no warnings!";
0608         else
0609             qCDebug(DISCOVER_LOG) << "test finished successfully despite" << m_warnings;
0610         qGuiApp->exit(m_warnings.count());
0611     }
0612 
0613 private:
0614     QObject *m_testObject;
0615     QList<QQmlError> m_warnings;
0616 };
0617 
0618 void DiscoverObject::loadTest(const QUrl &url)
0619 {
0620     new DiscoverTestExecutor(m_mainWindow.get(), engine(), url);
0621 }
0622 
0623 QQuickWindow *DiscoverObject::mainWindow() const
0624 {
0625     return m_mainWindow.get();
0626 }
0627 
0628 void DiscoverObject::showError(const QString &msg)
0629 {
0630     QTimer::singleShot(100, this, [msg]() {
0631         Q_EMIT ResourcesModel::global()->passiveMessage(msg);
0632     });
0633 }
0634 
0635 void DiscoverObject::copyTextToClipboard(const QString text)
0636 {
0637     QClipboard *clipboard = QGuiApplication::clipboard();
0638     clipboard->setText(text);
0639 }
0640 
0641 void DiscoverObject::reboot()
0642 {
0643     auto method = QDBusMessage::createMethodCall(QStringLiteral("org.kde.LogoutPrompt"),
0644                                                  QStringLiteral("/LogoutPrompt"),
0645                                                  QStringLiteral("org.kde.LogoutPrompt"),
0646                                                  QStringLiteral("promptReboot"));
0647     QDBusConnection::sessionBus().asyncCall(method);
0648 }
0649 
0650 void DiscoverObject::rebootNow()
0651 {
0652     auto method = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.login1"),
0653                                                  QStringLiteral("/org/freedesktop/login1"),
0654                                                  QStringLiteral("org.freedesktop.login1.Manager"),
0655                                                  QStringLiteral("Reboot"));
0656     method.setArguments({true /*interactive*/});
0657     QDBusConnection::systemBus().asyncCall(method);
0658 }
0659 
0660 QRect DiscoverObject::initialGeometry() const
0661 {
0662     KConfigGroup window(KSharedConfig::openConfig(), u"Window"_s);
0663     return window.readEntry("geometry", QRect());
0664 }
0665 
0666 QString DiscoverObject::describeSources() const
0667 {
0668     return mainWindow()->property("describeSources").toString();
0669 }
0670 
0671 #include "DiscoverObject.moc"