File indexing completed on 2024-05-05 17:04:27

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