File indexing completed on 2024-04-28 04:05:04

0001 /*
0002     SPDX-FileCopyrightText: 2012 Stefan Majewsky <majewsky@gmx.net>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-only
0005 */
0006 
0007 #include "kgamethemeprovider.h"
0008 #include "kgameimageprovider_p.h"
0009 
0010 // own
0011 #include <kdegames_logging.h>
0012 // KF
0013 #include <KConfig>
0014 #include <KConfigGroup>
0015 #include <KSharedConfig>
0016 // Qt
0017 #include <QDir>
0018 #include <QFileInfo>
0019 #include <QGuiApplication>
0020 #include <QQmlContext>
0021 #include <QStandardPaths>
0022 // Std
0023 #include <utility>
0024 
0025 class KGameThemeProviderPrivate
0026 {
0027 public:
0028     KGameThemeProvider *const q;
0029 
0030     QString m_name;
0031     QList<const KGameTheme *> m_themes;
0032     const QByteArray m_configKey;
0033     mutable const KGameTheme *m_currentTheme = nullptr;
0034     const KGameTheme *m_defaultTheme = nullptr;
0035     // this stores the arguments which were passed to discoverThemes()
0036     QString m_dtDirectory;
0037     QString m_dtDefaultThemeName;
0038     const QMetaObject *m_dtThemeClass = nullptr;
0039     // this remembers which themes were already discovered
0040     QStringList m_discoveredThemes;
0041     // this disables the addTheme() lock during rediscoverThemes()
0042     bool m_inRediscover = false;
0043 
0044 public:
0045     KGameThemeProviderPrivate(KGameThemeProvider *parent, const QByteArray &key)
0046         : q(parent)
0047         , m_configKey(key)
0048     {
0049     }
0050 
0051     void updateThemeName()
0052     {
0053         Q_EMIT q->currentThemeNameChanged(q->currentThemeName());
0054     }
0055 };
0056 
0057 KGameThemeProvider::KGameThemeProvider(const QByteArray &configKey, QObject *parent)
0058     : QObject(parent)
0059     , d_ptr(new KGameThemeProviderPrivate(this, configKey))
0060 {
0061     qRegisterMetaType<const KGameTheme *>();
0062     qRegisterMetaType<KGameThemeProvider *>();
0063     connect(this, &KGameThemeProvider::currentThemeChanged, this, [this]() {
0064         Q_D(KGameThemeProvider);
0065         d->updateThemeName();
0066     });
0067 }
0068 
0069 KGameThemeProvider::~KGameThemeProvider()
0070 {
0071     Q_D(KGameThemeProvider);
0072 
0073     if (!d->m_themes.isEmpty()) {
0074         // save current theme in config file (no sync() call here; this will most
0075         // likely be called at application shutdown when others are also writing to
0076         // KGlobal::config(); also KConfig's dtor will sync automatically)
0077         // but do not save if there is no choice; this is esp. helpful for the
0078         // KGameRenderer constructor overload that uses a single KGameTheme instance
0079         if (d->m_themes.size() > 1 && !d->m_configKey.isEmpty()) {
0080             KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KgTheme"));
0081             cg.writeEntry(d->m_configKey.data(), currentTheme()->identifier());
0082         }
0083         // cleanup
0084         while (!d->m_themes.isEmpty()) {
0085             delete const_cast<KGameTheme *>(d->m_themes.takeFirst());
0086         }
0087     }
0088 }
0089 
0090 QString KGameThemeProvider::name() const
0091 {
0092     Q_D(const KGameThemeProvider);
0093 
0094     return d->m_name;
0095 }
0096 
0097 QList<const KGameTheme *> KGameThemeProvider::themes() const
0098 {
0099     Q_D(const KGameThemeProvider);
0100 
0101     return d->m_themes;
0102 }
0103 
0104 void KGameThemeProvider::addTheme(KGameTheme *theme)
0105 {
0106     Q_D(KGameThemeProvider);
0107 
0108     // The intended use is to create the KGameThemeProvider object, add themes,
0109     //*then* start to work with the currentLevel(). The first call to
0110     // currentTheme() will load the previous selection from the config, and the
0111     // level list will be considered immutable from this point.
0112     Q_ASSERT_X(d->m_currentTheme == nullptr || d->m_inRediscover, "KGameThemeProvider::addTheme", "Only allowed before currentTheme() is called.");
0113     // add theme
0114     d->m_themes.append(theme);
0115     theme->setParent(this);
0116 }
0117 
0118 const KGameTheme *KGameThemeProvider::defaultTheme() const
0119 {
0120     Q_D(const KGameThemeProvider);
0121 
0122     return d->m_defaultTheme;
0123 }
0124 
0125 void KGameThemeProvider::setDefaultTheme(const KGameTheme *theme)
0126 {
0127     Q_D(KGameThemeProvider);
0128 
0129     if (d->m_currentTheme) {
0130         qCDebug(KDEGAMES_LOG) << "You're calling setDefaultTheme after the current "
0131                                  "theme has already been determined. That's not gonna work.";
0132         return;
0133     }
0134     Q_ASSERT(d->m_themes.contains(theme));
0135     d->m_defaultTheme = theme;
0136 }
0137 
0138 const KGameTheme *KGameThemeProvider::currentTheme() const
0139 {
0140     Q_D(const KGameThemeProvider);
0141 
0142     if (d->m_currentTheme) {
0143         return d->m_currentTheme;
0144     }
0145     Q_ASSERT(!d->m_themes.isEmpty());
0146     // check configuration file for saved theme
0147     if (!d->m_configKey.isEmpty()) {
0148         KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KgTheme"));
0149         const QByteArray id = cg.readEntry(d->m_configKey.data(), QByteArray());
0150         // look for a theme with this id
0151         for (const KGameTheme *theme : std::as_const(d->m_themes)) {
0152             if (theme->identifier() == id) {
0153                 return d->m_currentTheme = theme;
0154             }
0155         }
0156     }
0157     // fall back to default theme (or first theme if no default specified)
0158     return d->m_currentTheme = (d->m_defaultTheme ? d->m_defaultTheme : d->m_themes.first());
0159 }
0160 
0161 void KGameThemeProvider::setCurrentTheme(const KGameTheme *theme)
0162 {
0163     Q_D(KGameThemeProvider);
0164 
0165     Q_ASSERT(d->m_themes.contains(theme));
0166     if (d->m_currentTheme != theme) {
0167         d->m_currentTheme = theme;
0168         Q_EMIT currentThemeChanged(theme);
0169     }
0170 }
0171 
0172 QString KGameThemeProvider::currentThemeName() const
0173 {
0174     Q_D(const KGameThemeProvider);
0175 
0176     return currentTheme()->name();
0177 }
0178 
0179 void KGameThemeProvider::discoverThemes(const QString &directory, const QString &defaultThemeName, const QMetaObject *themeClass)
0180 {
0181     Q_D(KGameThemeProvider);
0182 
0183     d->m_dtDirectory = directory;
0184     d->m_dtDefaultThemeName = defaultThemeName;
0185     d->m_dtThemeClass = themeClass;
0186     rediscoverThemes();
0187 }
0188 
0189 static QStringList findSubdirectories(const QStringList &dirs)
0190 {
0191     QStringList result;
0192 
0193     for (const QString &dir : dirs) {
0194         const QStringList subdirNames = QDir(dir).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
0195         result.reserve(result.size() + subdirNames.size());
0196         for (const QString &subdirName : subdirNames) {
0197             const QString subdir = dir + QLatin1Char('/') + subdirName;
0198             result << subdir;
0199         }
0200     }
0201     if (!result.isEmpty()) {
0202         result += findSubdirectories(result);
0203     }
0204 
0205     return result;
0206 }
0207 
0208 void KGameThemeProvider::rediscoverThemes()
0209 {
0210     Q_D(KGameThemeProvider);
0211 
0212     if (d->m_dtDirectory.isEmpty()) {
0213         return; // discoverThemes() was never called
0214     }
0215 
0216     KGameTheme *defaultTheme = nullptr;
0217 
0218     d->m_inRediscover = true;
0219     const QString defaultFileName = d->m_dtDefaultThemeName + QLatin1String(".desktop");
0220 
0221     QStringList themePaths;
0222     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, d->m_dtDirectory, QStandardPaths::LocateDirectory);
0223     const QStringList allDirs = dirs + findSubdirectories(dirs);
0224     for (const QString &dir : allDirs) {
0225         const QStringList fileNames = QDir(dir).entryList({QStringLiteral("*.desktop")});
0226         for (const QString &file : fileNames) {
0227             if (!themePaths.contains(file)) {
0228                 themePaths.append(dir + QLatin1Char('/') + file);
0229             }
0230         }
0231     }
0232 
0233     // create themes from result, order default theme at the front (that's not
0234     // needed by KGameThemeProvider, but nice for the theme selector)
0235     QList<KGameTheme *> themes;
0236     themes.reserve(themePaths.size());
0237     if (d->m_discoveredThemes.isEmpty()) {
0238         d->m_discoveredThemes.reserve(themePaths.size());
0239     }
0240     for (const QString &themePath : std::as_const(themePaths)) {
0241         const QFileInfo fi(themePath);
0242         if (d->m_discoveredThemes.contains(fi.fileName())) {
0243             continue;
0244         }
0245         d->m_discoveredThemes << fi.fileName();
0246         // the identifier is constructed such that it is compatible with
0247         // KGameTheme (e.g. "themes/default.desktop")
0248         const QByteArray id = QString(d->m_dtDirectory + QLatin1Char('/') + fi.fileName()).toUtf8();
0249 
0250         // create theme
0251         KGameTheme *theme;
0252         if (d->m_dtThemeClass) {
0253             theme = qobject_cast<KGameTheme *>(d->m_dtThemeClass->newInstance(Q_ARG(QByteArray, id), Q_ARG(QObject *, this)));
0254             Q_ASSERT_X(theme, "KGameThemeProvider::discoverThemes", "Could not create theme instance. Is your constructor Q_INVOKABLE?");
0255         } else {
0256             theme = new KGameTheme(id, this);
0257         }
0258         // silently discard invalid theme files
0259         if (!theme->readFromDesktopFile(themePath)) {
0260             delete theme;
0261             continue;
0262         }
0263         // order default theme at the front (that's not necessarily needed by
0264         // KGameThemeProvider, but nice for the theme selector)
0265         if (fi.fileName() == defaultFileName) {
0266             themes.prepend(theme);
0267             defaultTheme = theme;
0268         } else {
0269             themes.append(theme);
0270         }
0271     }
0272     // add themes in the determined order
0273     for (KGameTheme *theme : std::as_const(themes)) {
0274         addTheme(theme);
0275     }
0276 
0277     if (defaultTheme != nullptr) {
0278         setDefaultTheme(defaultTheme);
0279     } else if (d->m_defaultTheme == nullptr && themes.count() != 0) {
0280         setDefaultTheme(themes.value(0));
0281     }
0282 
0283     d->m_inRediscover = false;
0284 }
0285 
0286 QPixmap KGameThemeProvider::generatePreview(const KGameTheme *theme, QSize size)
0287 {
0288     const qreal dpr = qApp->devicePixelRatio();
0289     QPixmap pixmap = QPixmap(theme->previewPath()).scaled(size * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0290     pixmap.setDevicePixelRatio(dpr);
0291     return pixmap;
0292 }
0293 
0294 void KGameThemeProvider::setDeclarativeEngine(const QString &name, QQmlEngine *engine)
0295 {
0296     Q_D(KGameThemeProvider);
0297 
0298     if (d->m_name != name) { // prevent multiple declarations
0299         d->m_name = name;
0300         engine->addImageProvider(name, new KGameImageProvider(this));
0301         engine->rootContext()->setContextProperty(name, this);
0302     }
0303 }
0304 
0305 #include "moc_kgamethemeprovider.cpp"