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 }