File indexing completed on 2024-05-26 12:48:13

0001 /*
0002  * This file is part of the KDE project
0003  * SPDX-FileCopyrightText: 2014 Arjen Hiemstra <ahiemstra@heimr.nl>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "Theme.h"
0009 
0010 #include <QStringList>
0011 #include <QUrl>
0012 #include <QDebug>
0013 #include <QFile>
0014 #include <QDir>
0015 #include <QColor>
0016 #include <QFont>
0017 #include <QFontDatabase>
0018 #include <QApplication>
0019 #include <QWidget>
0020 #include <QQmlComponent>
0021 #include <QStandardPaths>
0022 
0023 #include <KIconLoader>
0024 
0025 #include "QmlGlobalEngine.h"
0026 
0027 #ifdef Q_OS_WIN
0028 #include <windows.h>
0029 #endif
0030 
0031 class Theme::Private
0032 {
0033 public:
0034     Private()
0035         : inheritedTheme(0)
0036         , iconPath("icons/")
0037         , imagePath("images/")
0038         , fontPath("fonts/")
0039         , fontsAdded(false)
0040         , lineCountLandscape(40)
0041         , lineCountPortrait(70)
0042     { }
0043 
0044     void rebuildFontCache();
0045 
0046     QString id;
0047     QString name;
0048     QString inherits;
0049     Theme* inheritedTheme;
0050 
0051     QVariantMap colors;
0052     QVariantMap sizes;
0053     QVariantMap fonts;
0054 
0055     QString basePath;
0056     QString iconPath;
0057     QString imagePath;
0058     QString fontPath;
0059 
0060     QHash<QString, QColor> colorCache;
0061     QHash<QString, QFont> fontMap;
0062 
0063     bool fontsAdded;
0064     QList<int> addedFonts;
0065     int lineCountLandscape;
0066     int lineCountPortrait;
0067 };
0068 
0069 Theme::Theme(QObject* parent)
0070     : QObject(parent), d(new Private)
0071 {
0072     qApp->installEventFilter(this);
0073 }
0074 
0075 Theme::~Theme()
0076 {
0077     QFontDatabase db;
0078     Q_FOREACH(int id, d->addedFonts) {
0079         db.removeApplicationFont(id);
0080     }
0081 
0082     delete d;
0083 }
0084 
0085 QString Theme::id() const
0086 {
0087     return d->id;
0088 }
0089 
0090 void Theme::setId(const QString& newValue)
0091 {
0092     if(newValue != d->id) {
0093         d->id = newValue;
0094         const QString qmlFileSubPath = QStringLiteral("calligragemini/themes/") + d->id + QStringLiteral("/theme.qml");
0095         const QString qmlFileFullPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, qmlFileSubPath);
0096         d->basePath = QFileInfo(qmlFileFullPath).dir().absolutePath();
0097         emit idChanged();
0098     }
0099 }
0100 
0101 QString Theme::name() const
0102 {
0103     return d->name;
0104 }
0105 
0106 void Theme::setName(const QString& newValue)
0107 {
0108     if(newValue != d->name) {
0109         d->name = newValue;
0110         emit nameChanged();
0111     }
0112 }
0113 
0114 QString Theme::inherits() const
0115 {
0116     return d->inherits;
0117 }
0118 
0119 void Theme::setInherits(const QString& newValue)
0120 {
0121     if(newValue != d->inherits) {
0122         if(d->inheritedTheme) {
0123             delete d->inheritedTheme;
0124             d->inheritedTheme = 0;
0125         }
0126         d->inherits = newValue;
0127 
0128         if(!d->inherits.isEmpty()) {
0129             d->inheritedTheme = Theme::load(d->inherits, this);
0130             connect(d->inheritedTheme, &Theme::fontCacheRebuilt, this, &Theme::fontCacheRebuilt);
0131         }
0132 
0133         emit inheritsChanged();
0134     }
0135 }
0136 
0137 QVariantMap Theme::colors() const
0138 {
0139     return d->colors;
0140 }
0141 
0142 void Theme::setColors(const QVariantMap& newValue)
0143 {
0144     if(newValue != d->colors) {
0145         d->colors = newValue;
0146         emit colorsChanged();
0147     }
0148 }
0149 
0150 QColor Theme::color(const QString& name)
0151 {
0152     if(d->colorCache.contains(name))
0153         return d->colorCache.value(name);
0154 
0155     QStringList parts = name.split('/');
0156     QColor result;
0157 
0158     if(!parts.isEmpty())
0159     {
0160         QVariantMap map = d->colors;
0161         QString current = parts.takeFirst();
0162 
0163         while(map.contains(current))
0164         {
0165             QVariant value = map.value(current);
0166             if(value.type() == QVariant::Map)
0167             {
0168                 if(parts.isEmpty())
0169                     break;
0170 
0171                 map = value.toMap();
0172                 current = parts.takeFirst();
0173             }
0174             else
0175             {
0176                 result = value.value<QColor>();
0177                 map = QVariantMap();
0178             }
0179         }
0180     }
0181 
0182     if(!result.isValid() && d->inheritedTheme) {
0183         result = d->inheritedTheme->color(name);
0184     }
0185 
0186     if(!result.isValid()) {
0187         qWarning() << "Unable to find color" << name;
0188     } else {
0189         d->colorCache.insert(name, result);
0190     }
0191 
0192     return result;
0193 }
0194 
0195 QVariantMap Theme::sizes() const
0196 {
0197     return d->sizes;
0198 }
0199 
0200 void Theme::setSizes(const QVariantMap& newValue)
0201 {
0202     if(newValue != d->sizes) {
0203         d->sizes = newValue;
0204         emit sizesChanged();
0205     }
0206 }
0207 
0208 float Theme::size(const QString& name)
0209 {
0210     Q_UNUSED(name);
0211     return 0.f;
0212 }
0213 
0214 QVariantMap Theme::fonts() const
0215 {
0216     return d->fonts;
0217 }
0218 
0219 void Theme::setFonts(const QVariantMap& newValue)
0220 {
0221     if(newValue != d->fonts)
0222     {
0223         d->fonts = newValue;
0224 
0225         d->fontMap.clear();
0226 
0227         emit fontsChanged();
0228     }
0229 }
0230 
0231 QFont Theme::font(const QString& name)
0232 {
0233     if(!d->fontsAdded) {
0234         QDir fontDir(d->basePath + '/' + d->fontPath);
0235         QStringList entries = fontDir.entryList(QDir::Files);
0236         QFontDatabase db;
0237         Q_FOREACH(const QString &entry, entries) {
0238             d->addedFonts.append(db.addApplicationFont(fontDir.absoluteFilePath(entry)));
0239         }
0240         d->fontsAdded = true;
0241     }
0242 
0243     if(d->fontMap.isEmpty()) {
0244         d->rebuildFontCache();
0245     }
0246 
0247     if(d->fontMap.contains(name))
0248         return d->fontMap.value(name);
0249 
0250     if(d->inheritedTheme)
0251         return d->inheritedTheme->font(name);
0252 
0253     qWarning() << "Unable to find font" << name;
0254     return QFont();
0255 }
0256 
0257 QString Theme::fontPath() const
0258 {
0259     return d->fontPath;
0260 }
0261 
0262 void Theme::setFontPath(const QString& newValue)
0263 {
0264     if(newValue != d->fontPath) {
0265         if(!d->addedFonts.isEmpty()) {
0266             QFontDatabase db;
0267             Q_FOREACH(int id, d->addedFonts) {
0268                 db.removeApplicationFont(id);
0269             }
0270             d->addedFonts.clear();
0271         }
0272 
0273         d->fontPath = newValue;
0274         d->fontsAdded = false;
0275 
0276         emit fontPathChanged();
0277     }
0278 }
0279 
0280 
0281 QString Theme::iconPath() const
0282 {
0283     return d->iconPath;
0284 }
0285 
0286 void Theme::setIconPath(const QString& newValue)
0287 {
0288     if(newValue != d->iconPath) {
0289         d->iconPath = newValue;
0290         emit iconPathChanged();
0291     }
0292 }
0293 
0294 QUrl Theme::icon(const QString& name, bool useSystemFallback)
0295 {
0296     QString url = QString("%1/%2/%3.svg").arg(d->basePath, d->iconPath, name);
0297     if(!QFile::exists(url)) {
0298         if(d->inheritedTheme) {
0299             return d->inheritedTheme->icon(name);
0300         } else {
0301             if(useSystemFallback) {
0302                 url = KIconLoader::global()->iconPath(name, -128);
0303                 qWarning() << "Attempting to use a system fallback icon" << url;
0304             } else {
0305                 qWarning() << "Unable to find icon" << url;
0306             }
0307         }
0308     }
0309 
0310     return QUrl::fromLocalFile(url);
0311 }
0312 
0313 QIcon Theme::iconActual(const QString& name)
0314 {
0315     return QIcon(icon(name).toLocalFile());
0316 }
0317 
0318 QString Theme::imagePath() const
0319 {
0320     return d->imagePath;
0321 }
0322 
0323 void Theme::setImagePath(const QString& newValue)
0324 {
0325     if(newValue != d->imagePath) {
0326         d->imagePath = newValue;
0327         emit imagePathChanged();
0328     }
0329 }
0330 
0331 QUrl Theme::image(const QString& name)
0332 {
0333     QString url = QString("%1/%2/%3").arg(d->basePath, d->imagePath, name);
0334     if(!QFile::exists(url)) {
0335         if(d->inheritedTheme) {
0336             return d->inheritedTheme->image(name);
0337         } else {
0338             qWarning() << "Unable to find image" << url;
0339         }
0340     }
0341 
0342     return QUrl::fromLocalFile(url);
0343 }
0344 
0345 Theme* Theme::load(const QString& id, QObject* parent)
0346 {
0347     QString qml;
0348 
0349     //Ugly hacky stuff for making things work on Windows
0350 #ifdef Q_OS_WIN
0351     QDir appdir(qApp->applicationDirPath());
0352 
0353     // Corrects for mismatched case errors in path (qtdeclarative fails to load)
0354     wchar_t buffer[1024];
0355     QString absolute = appdir.absolutePath();
0356     DWORD rv = ::GetShortPathName((wchar_t*)absolute.utf16(), buffer, 1024);
0357     rv = ::GetLongPathName(buffer, buffer, 1024);
0358     QString correctedPath((QChar *)buffer);
0359     appdir.setPath(correctedPath);
0360 
0361     // for now, the app in bin/ and we still use the env.bat script
0362     appdir.cdUp();
0363     qml = QString("%1/bin/data/calligragemini/themes/%2/theme.qml").arg(appdir.canonicalPath(), id);
0364 #else
0365     const QString qmlFileSubPath = QStringLiteral("calligragemini/themes/") + id + QStringLiteral("/theme.qml");
0366     qml = QStandardPaths::locate(QStandardPaths::GenericDataLocation, qmlFileSubPath);
0367 #endif
0368 
0369     QQmlComponent themeComponent(QmlGlobalEngine::instance()->engine(), parent);
0370     themeComponent.loadUrl(QUrl::fromLocalFile(qml));
0371 
0372     if(themeComponent.isError()) {
0373         qWarning() << themeComponent.errorString();
0374         return 0;
0375     }
0376 
0377     Theme* theme = qobject_cast<Theme*>(themeComponent.create());
0378     if(!theme) {
0379         qWarning() << "Failed to create theme instance!";
0380         return 0;
0381     }
0382 
0383     return theme;
0384 }
0385 
0386 bool Theme::eventFilter(QObject* target, QEvent* event)
0387 {
0388     if(target == qApp->activeWindow() && target->inherits("QMainWindow") && event->type() == QEvent::Resize) {
0389         d->rebuildFontCache();
0390         emit fontCacheRebuilt();
0391     }
0392 
0393     return QObject::eventFilter(target, event);
0394 }
0395 
0396 int Theme::adjustedPixel(const int& pixel) const
0397 {
0398     if(!qApp->activeWindow())
0399         return 0;
0400 
0401     // If we are in portrait mode, we still assume 1080p for font size purposes
0402     int width = qApp->activeWindow()->height() > qApp->activeWindow()->width() ? qApp->activeWindow()->height() : qApp->activeWindow()->width();
0403     // The pixel size is based on a 1080p screen, and it is accepted that the window size
0404     // will vary slightly on there, depending on whether or not we are full screened (so
0405     // we accept up to 10 pixels less width)
0406     float sizeAdjustment = 1;
0407     if(width > 1920 || width < 1910)
0408         sizeAdjustment = width / 1920.f;
0409     return pixel * sizeAdjustment;
0410 }
0411 
0412 void Theme::Private::rebuildFontCache()
0413 {
0414     fontMap.clear();
0415     QFontDatabase db;
0416     for(QVariantMap::ConstIterator itr = fonts.constBegin(); itr != fonts.constEnd(); ++itr)
0417     {
0418         QVariantMap map = itr->toMap();
0419         if(map.isEmpty())
0420             continue;
0421 
0422         QFont font = db.font(map.value("family").toString(), map.value("style", "Regular").toString(), 10);
0423 
0424         if(font.isCopyOf(qApp->font()))
0425             qWarning() << "Could not find font" << map.value("family") << "with style" << map.value("style", "Regular");
0426 
0427         if(map.contains("pixelSize")) {
0428             // If we are in portrait mode, we still assume 1080p for font size purposes
0429             int width = qApp->activeWindow()->height() > qApp->activeWindow()->width() ? qApp->activeWindow()->height() : qApp->activeWindow()->width();
0430             // The pixel size is based on a 1080p screen, and it is accepted that the window size
0431             // will vary slightly on there, depending on whether or not we are full screened (so
0432             // we accept up to 10 pixels less width)
0433             float sizeAdjustment = 1;
0434             if(width > 1920 || width < 1910)
0435                 sizeAdjustment = width / 1920.f;
0436             font.setPixelSize(map.value("pixelSize").toInt() * sizeAdjustment);
0437         }
0438         else {
0439             float lineCount = qApp->activeWindow()->height() > qApp->activeWindow()->width() ? lineCountPortrait : lineCountLandscape;
0440             float lineHeight = qApp->activeWindow()->height() / lineCount;
0441             font.setPixelSize(lineHeight * map.value("size", 1).toFloat());
0442         }
0443 
0444         fontMap.insert(itr.key(), font);
0445     }
0446 }