/****************************************************************************** Copyright (C) 2023 by Dennis Sädtler This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ******************************************************************************/ #include #include #include #include #include #include #include #include #include #include "qt-wrappers.hpp" #include "obs-app.hpp" #include "obs-app-theming.hpp" #include "obs-proxy-style.hpp" #include "platform.hpp" #include "ui-config.h" using namespace std; struct CFParser { cf_parser cfp = {}; ~CFParser() { cf_parser_free(&cfp); } operator cf_parser *() { return &cfp; } cf_parser *operator->() { return &cfp; } }; static OBSTheme *ParseThemeMeta(const QString &path) { QFile themeFile(path); if (!themeFile.open(QIODeviceBase::ReadOnly)) return nullptr; OBSTheme *meta = nullptr; const QByteArray data = themeFile.readAll(); CFParser cfp; int ret; if (!cf_parser_parse(cfp, data.constData(), QT_TO_UTF8(path))) return nullptr; if (cf_token_is(cfp, "@") || cf_go_to_token(cfp, "@", nullptr)) { while (cf_next_token(cfp)) { if (cf_token_is(cfp, "OBSThemeMeta")) break; if (!cf_go_to_token(cfp, "@", nullptr)) return nullptr; } if (!cf_next_token(cfp)) return nullptr; if (!cf_token_is(cfp, "{")) return nullptr; meta = new OBSTheme(); for (;;) { if (!cf_next_token(cfp)) { delete meta; return nullptr; } ret = cf_token_is_type(cfp, CFTOKEN_NAME, "name", nullptr); if (ret != PARSE_SUCCESS) break; string name(cfp->cur_token->str.array, cfp->cur_token->str.len); ret = cf_next_token_should_be(cfp, ":", ";", nullptr); if (ret != PARSE_SUCCESS) continue; if (!cf_next_token(cfp)) { delete meta; return nullptr; } ret = cf_token_is_type(cfp, CFTOKEN_STRING, "value", ";"); if (ret != PARSE_SUCCESS) continue; BPtr str = cf_literal_to_str(cfp->cur_token->str.array, cfp->cur_token->str.len); if (str) { if (name == "dark") meta->isDark = strcmp(str, "true") == 0; else if (name == "extends") meta->extends = str; else if (name == "author") meta->author = str; else if (name == "id") meta->id = str; else if (name == "name") meta->name = str; } if (!cf_go_to_token(cfp, ";", nullptr)) { delete meta; return nullptr; } } } if (meta) { auto filepath = filesystem::u8path(path.toStdString()); meta->isBaseTheme = filepath.extension() == ".obt"; meta->filename = filepath.stem(); if (meta->id.isEmpty() || meta->name.isEmpty() || (!meta->isBaseTheme && meta->extends.isEmpty())) { /* Theme is invalid */ delete meta; meta = nullptr; } else { meta->location = absolute(filepath); meta->isHighContrast = path.endsWith(".oha"); meta->isVisible = !path.contains("System"); } } return meta; } static bool ParseVarName(CFParser &cfp, QString &value) { int ret; ret = cf_next_token_should_be(cfp, "(", ";", nullptr); if (ret != PARSE_SUCCESS) return false; ret = cf_next_token_should_be(cfp, "-", ";", nullptr); if (ret != PARSE_SUCCESS) return false; ret = cf_next_token_should_be(cfp, "-", ";", nullptr); if (ret != PARSE_SUCCESS) return false; if (!cf_next_token(cfp)) return false; value = QString::fromUtf8(cfp->cur_token->str.array, cfp->cur_token->str.len); ret = cf_next_token_should_be(cfp, ")", ";", nullptr); if (ret != PARSE_SUCCESS) return false; return !value.isEmpty(); } static QColor ParseColor(CFParser &cfp) { const char *array; uint32_t color = 0; QColor res(QColor::Invalid); if (cf_token_is(cfp, "#")) { if (!cf_next_token(cfp)) return res; color = strtol(cfp->cur_token->str.array, nullptr, 16); } else if (cf_token_is(cfp, "rgb")) { int ret = cf_next_token_should_be(cfp, "(", ";", nullptr); if (ret != PARSE_SUCCESS || !cf_next_token(cfp)) return res; array = cfp->cur_token->str.array; color |= strtol(array, nullptr, 10) << 16; ret = cf_next_token_should_be(cfp, ",", ";", nullptr); if (ret != PARSE_SUCCESS || !cf_next_token(cfp)) return res; array = cfp->cur_token->str.array; color |= strtol(array, nullptr, 10) << 8; ret = cf_next_token_should_be(cfp, ",", ";", nullptr); if (ret != PARSE_SUCCESS || !cf_next_token(cfp)) return res; array = cfp->cur_token->str.array; color |= strtol(array, nullptr, 10); ret = cf_next_token_should_be(cfp, ")", ";", nullptr); if (ret != PARSE_SUCCESS) return res; } else if (cf_token_is(cfp, "bikeshed")) { color |= QRandomGenerator::global()->bounded(INT8_MAX) << 16; color |= QRandomGenerator::global()->bounded(INT8_MAX) << 8; color |= QRandomGenerator::global()->bounded(INT8_MAX); } res = color; return res; } static bool ParseCalc(CFParser &cfp, QStringList &calc, vector &vars) { int ret = cf_next_token_should_be(cfp, "(", ";", nullptr); if (ret != PARSE_SUCCESS) return false; if (!cf_next_token(cfp)) return false; while (!cf_token_is(cfp, ")")) { if (cf_token_is(cfp, ";")) break; if (cf_token_is(cfp, "calc")) { /* Internal calc's do not have proper names. * They are anonymous variables */ OBSThemeVariable var; QStringList subcalc; var.name = QString("__unnamed_%1") .arg(QRandomGenerator::global() ->generate64()); if (!ParseCalc(cfp, subcalc, vars)) return false; var.type = OBSThemeVariable::Calc; var.value = subcalc; calc << var.name; vars.push_back(std::move(var)); } else if (cf_token_is(cfp, "var")) { QString value; if (!ParseVarName(cfp, value)) return false; calc << value; } else { calc << QString::fromUtf8(cfp->cur_token->str.array, cfp->cur_token->str.len); } if (!cf_next_token(cfp)) return false; } return !calc.isEmpty(); } static vector ParseThemeVariables(const char *themeData) { CFParser cfp; int ret; std::vector vars; if (!cf_parser_parse(cfp, themeData, nullptr)) return vars; if (!cf_token_is(cfp, "@") && !cf_go_to_token(cfp, "@", nullptr)) return vars; while (cf_next_token(cfp)) { if (cf_token_is(cfp, "OBSThemeVars")) break; if (!cf_go_to_token(cfp, "@", nullptr)) return vars; } if (!cf_next_token(cfp)) return {}; if (!cf_token_is(cfp, "{")) return {}; for (;;) { if (!cf_next_token(cfp)) return vars; if (!cf_token_is(cfp, "-")) return vars; ret = cf_next_token_should_be(cfp, "-", ";", nullptr); if (ret != PARSE_SUCCESS) continue; if (!cf_next_token(cfp)) return vars; ret = cf_token_is_type(cfp, CFTOKEN_NAME, "key", nullptr); if (ret != PARSE_SUCCESS) break; QString key = QString::fromUtf8(cfp->cur_token->str.array, cfp->cur_token->str.len); OBSThemeVariable var; var.name = key; #ifdef _WIN32 const QString osPrefix = "os_win_"; #elif __APPLE__ const QString osPrefix = "os_mac_"; #else const QString osPrefix = "os_lin_"; #endif if (key.startsWith(osPrefix) && key.length() > osPrefix.length()) { var.name = key.sliced(osPrefix.length()); } ret = cf_next_token_should_be(cfp, ":", ";", nullptr); if (ret != PARSE_SUCCESS) continue; if (!cf_next_token(cfp)) return vars; if (cfp->cur_token->type == CFTOKEN_NUM) { const char *ch = cfp->cur_token->str.array; const char *end = ch + cfp->cur_token->str.len; double f = os_strtod(ch); var.value = f; var.type = OBSThemeVariable::Number; /* Look for a suffix and mark variable as size if it exists */ while (ch < end) { if (!isdigit(*ch) && !isspace(*ch) && *ch != '.') { var.suffix = QString::fromUtf8(ch, end - ch); var.type = OBSThemeVariable::Size; break; } ch++; } } else if (cf_token_is(cfp, "rgb") || cf_token_is(cfp, "#") || cf_token_is(cfp, "bikeshed")) { QColor color = ParseColor(cfp); if (!color.isValid()) continue; var.value = color; var.type = OBSThemeVariable::Color; } else if (cf_token_is(cfp, "var")) { QString value; if (!ParseVarName(cfp, value)) continue; var.value = value; var.type = OBSThemeVariable::Alias; } else if (cf_token_is(cfp, "calc")) { QStringList calc; if (!ParseCalc(cfp, calc, vars)) continue; var.type = OBSThemeVariable::Calc; var.value = calc; } else { var.type = OBSThemeVariable::String; BPtr strVal = cf_literal_to_str(cfp->cur_token->str.array, cfp->cur_token->str.len); var.value = QString::fromUtf8(strVal.Get()); } if (!cf_next_token(cfp)) return vars; if (cf_token_is(cfp, "!") && cf_next_token_should_be(cfp, "editable", nullptr, nullptr) == PARSE_SUCCESS) { if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Alias) { blog(LOG_WARNING, "Variable of calc/alias type cannot be editable: %s", QT_TO_UTF8(var.name)); } else { var.editable = true; } } vars.push_back(std::move(var)); if (!cf_token_is(cfp, ";") && !cf_go_to_token(cfp, ";", nullptr)) return vars; } return vars; } void OBSApp::FindThemes() { string themeDir; themeDir.resize(512); QStringList filters; filters << "*.obt" // OBS Base Theme << "*.ovt" // OBS Variant Theme << "*.oha" // OBS High-contrast Adjustment layer ; if (GetConfigPath(themeDir.data(), themeDir.capacity(), "obs-studio/themes/") > 0) { QDirIterator it(QT_UTF8(themeDir.c_str()), filters, QDir::Files); while (it.hasNext()) { OBSTheme *theme = ParseThemeMeta(it.next()); if (theme && !themes.contains(theme->id)) themes[theme->id] = std::move(*theme); else delete theme; } } GetDataFilePath("themes/", themeDir); QDirIterator it(QString::fromStdString(themeDir), filters, QDir::Files); while (it.hasNext()) { OBSTheme *theme = ParseThemeMeta(it.next()); if (theme && !themes.contains(theme->id)) themes[theme->id] = std::move(*theme); else delete theme; } /* Build dependency tree for all themes, removing ones that have items missing. */ QSet invalid; for (OBSTheme &theme : themes) { if (theme.extends.isEmpty()) { if (!theme.isBaseTheme) { blog(LOG_ERROR, R"(Theme "%s" is not base, but does not specify parent!)", QT_TO_UTF8(theme.id)); invalid.insert(theme.id); } continue; } QString parentId = theme.extends; while (!parentId.isEmpty()) { OBSTheme *parent = GetTheme(parentId); if (!parent) { blog(LOG_ERROR, R"(Theme "%s" is missing ancestor "%s"!)", QT_TO_UTF8(theme.id), QT_TO_UTF8(parentId)); invalid.insert(theme.id); break; } if (theme.isBaseTheme && !parent->isBaseTheme) { blog(LOG_ERROR, R"(Ancestor "%s" of base theme "%s" is not a base theme!)", QT_TO_UTF8(parent->id), QT_TO_UTF8(theme.id)); invalid.insert(theme.id); break; } /* Mark this theme as a variant of first parent that is a base theme. */ if (!theme.isBaseTheme && parent->isBaseTheme && theme.parent.isEmpty()) theme.parent = parent->id; theme.dependencies.push_front(parent->id); parentId = parent->extends; if (parentId.isEmpty() && !parent->isBaseTheme) { blog(LOG_ERROR, R"(Final ancestor of "%s" ("%s") is not a base theme!)", QT_TO_UTF8(theme.id), QT_TO_UTF8(parent->id)); invalid.insert(theme.id); break; } } } for (const QString &name : invalid) { themes.remove(name); } } static bool ResolveVariable(const QHash &vars, OBSThemeVariable &var) { const OBSThemeVariable *varPtr = &var; const OBSThemeVariable *realVar = varPtr; while (realVar->type == OBSThemeVariable::Alias) { QString newKey = realVar->value.toString(); if (!vars.contains(newKey)) { blog(LOG_ERROR, R"(Variable "%s" (aliased by "%s") does not exist!)", QT_TO_UTF8(newKey), QT_TO_UTF8(var.name)); return false; } const OBSThemeVariable &newVar = vars[newKey]; realVar = &newVar; } if (realVar != varPtr) var = *realVar; return true; } static QString EvalCalc(const QHash &vars, const OBSThemeVariable &var, const int recursion = 0); static OBSThemeVariable ParseCalcVariable(const QHash &vars, const QString &value, const int recursion = 0) { OBSThemeVariable var; const QByteArray utf8 = value.toUtf8(); const char *data = utf8.constData(); if (isdigit(*data)) { double f = os_strtod(data); var.type = OBSThemeVariable::Number; var.value = f; const char *dataEnd = data + utf8.size(); while (data < dataEnd) { if (*data && !isdigit(*data) && *data != '.') { var.suffix = QString::fromUtf8(data, dataEnd - data); var.type = OBSThemeVariable::Size; break; } data++; } } else { /* Treat value as an alias/key and resolve it */ var.type = OBSThemeVariable::Alias; var.value = value; ResolveVariable(vars, var); /* Handle nested calc()s */ if (var.type == OBSThemeVariable::Calc) { QString val = EvalCalc(vars, var, recursion + 1); var = ParseCalcVariable(vars, val); } /* Only number or size would be valid here */ if (var.type != OBSThemeVariable::Number && var.type != OBSThemeVariable::Size) { blog(LOG_ERROR, "calc() operand is not a size or number: %s", QT_TO_UTF8(var.value.toString())); throw invalid_argument("Operand not of numeric type"); } } return var; } static QString EvalCalc(const QHash &vars, const OBSThemeVariable &var, const int recursion) { if (recursion >= 10) { /* Abort after 10 levels of recursion */ blog(LOG_ERROR, "Maximum calc() recursion levels hit!"); return "'Invalid expression'"; } QStringList args = var.value.toStringList(); if (args.length() != 3) { blog(LOG_ERROR, "calc() had invalid number of arguments: %lld (%s)", args.length(), QT_TO_UTF8(args.join(", "))); return "'Invalid expression'"; } QString &opt = args[1]; if (opt != '*' && opt != '+' && opt != '-' && opt != '/') { blog(LOG_ERROR, "Unknown/invalid calc() operator: %s", QT_TO_UTF8(opt)); return "'Invalid expression'"; } OBSThemeVariable val1, val2; try { val1 = ParseCalcVariable(vars, args[0], recursion); val2 = ParseCalcVariable(vars, args[2], recursion); } catch (...) { return "'Invalid expression'"; } /* Ensure that suffixes match (if any) */ if (!val1.suffix.isEmpty() && !val2.suffix.isEmpty() && val1.suffix != val2.suffix) { blog(LOG_ERROR, "calc() requires suffixes to match or only one to be present! %s != %s", QT_TO_UTF8(val1.suffix), QT_TO_UTF8(val2.suffix)); return "'Invalid expression'"; } double val = numeric_limits::quiet_NaN(); double d1 = val1.userValue.isValid() ? val1.userValue.toDouble() : val1.value.toDouble(); double d2 = val2.userValue.isValid() ? val2.userValue.toDouble() : val2.value.toDouble(); if (!isfinite(d1) || !isfinite(d2)) { blog(LOG_ERROR, "calc() received at least one invalid value:" " op1: %f, op2: %f", d1, d2); return "'Invalid expression'"; } if (opt == "+") val = d1 + d2; else if (opt == "-") val = d1 - d2; else if (opt == "*") val = d1 * d2; else if (opt == "/") val = d1 / d2; if (!isnormal(val)) { blog(LOG_ERROR, "Invalid calc() math resulted in non-normal number:" " %f %s %f = %f", d1, QT_TO_UTF8(opt), d2, val); return "'Invalid expression'"; } bool isInteger = ceill(val) == val; QString result = QString::number(val, 'f', isInteger ? 0 : -1); /* Carry-over suffix */ if (!val1.suffix.isEmpty()) result += val1.suffix; else if (!val2.suffix.isEmpty()) result += val2.suffix; return result; } static qsizetype FindEndOfOBSMetadata(const QString &content) { /* Find end of last OBS-specific section and strip it, kinda jank but should work */ qsizetype end = 0; for (auto section : {"OBSThemeMeta", "OBSThemeVars", "OBSTheme"}) { qsizetype idx = content.indexOf(section, 0); if (idx > end) { end = content.indexOf('}', idx) + 1; } } return end; } static QString PrepareQSS(const QHash &vars, const QStringList &contents) { QString stylesheet; QString needleTemplate("var(--%1)"); for (const QString &content : contents) { qsizetype offset = FindEndOfOBSMetadata(content); if (offset >= 0) { stylesheet += "\n"; stylesheet += content.sliced(offset); } } for (const OBSThemeVariable &var_ : vars) { OBSThemeVariable var(var_); if (!ResolveVariable(vars, var)) continue; QString needle = needleTemplate.arg(var_.name); QString replace; QVariant value = var.userValue.isValid() ? var.userValue : var.value; if (var.type == OBSThemeVariable::Color) { replace = value.value().name(QColor::HexRgb); } else if (var.type == OBSThemeVariable::Calc) { replace = EvalCalc(vars, var); } else if (var.type == OBSThemeVariable::Size || var.type == OBSThemeVariable::Number) { double val = value.toDouble(); bool isInteger = ceill(val) == val; replace = QString::number(val, 'f', isInteger ? 0 : -1); if (!var.suffix.isEmpty()) replace += var.suffix; } else { replace = value.toString(); } stylesheet = stylesheet.replace(needle, replace); } return stylesheet; } template static void FillEnumMap(QHash &map) { QMetaEnum meta = QMetaEnum::fromType(); int numKeys = meta.keyCount(); for (int i = 0; i < numKeys; i++) { const char *key = meta.key(i); QString keyName(key); map[keyName.toLower()] = static_cast(meta.keyToValue(key)); } } static QPalette PreparePalette(const QHash &vars, const QPalette &defaultPalette) { static QHash roleMap; static QHash groupMap; if (roleMap.empty()) FillEnumMap(roleMap); if (groupMap.empty()) FillEnumMap(groupMap); QPalette pal(defaultPalette); for (const OBSThemeVariable &var_ : vars) { if (!var_.name.startsWith("palette_")) continue; if (var_.name.count("_") < 1 || var_.name.count("_") > 2) continue; OBSThemeVariable var(var_); if (!ResolveVariable(vars, var) || var.type != OBSThemeVariable::Color) continue; /* Determine role and optionally group based on name. * Format is: palette_[_] */ QPalette::ColorRole role = QPalette::NoRole; QPalette::ColorGroup group = QPalette::All; QStringList parts = var_.name.split("_"); if (parts.length() >= 2) { QString key = parts[1].toLower(); if (!roleMap.contains(key)) { blog(LOG_WARNING, "Palette role \"%s\" is not valid!", QT_TO_UTF8(parts[1])); continue; } role = roleMap[key]; } if (parts.length() == 3) { QString key = parts[2].toLower(); if (!groupMap.contains(key)) { blog(LOG_WARNING, "Palette group \"%s\" is not valid!", QT_TO_UTF8(parts[2])); continue; } group = groupMap[key]; } QVariant value = var.userValue.isValid() ? var.userValue : var.value; QColor color = value.value().name(QColor::HexRgb); pal.setColor(group, role, color); } return pal; } OBSTheme *OBSApp::GetTheme(const QString &name) { if (!themes.contains(name)) return nullptr; return &themes[name]; } bool OBSApp::SetTheme(const QString &name) { OBSTheme *theme = GetTheme(name); if (!theme) return false; if (themeWatcher) { themeWatcher->blockSignals(true); themeWatcher->removePaths(themeWatcher->files()); } setStyleSheet(""); currentTheme = theme; QStringList contents; QHash vars; /* Build list of themes to load (in order) */ QStringList themeIds(theme->dependencies); themeIds << theme->id; /* Find and add high contrast adjustment layer if available */ if (HighContrastEnabled()) { for (const OBSTheme &theme_ : themes) { if (!theme_.isHighContrast) continue; if (theme_.parent != theme->id) continue; themeIds << theme_.id; break; } } QStringList filenames; for (const QString &themeId : themeIds) { OBSTheme *cur = GetTheme(themeId); QFile file(cur->location); filenames << file.fileName(); if (!file.open(QIODeviceBase::ReadOnly)) return false; const QByteArray content = file.readAll(); for (OBSThemeVariable &var : ParseThemeVariables(content.constData())) { vars[var.name] = std::move(var); } contents.emplaceBack(content.constData()); } const QString stylesheet = PrepareQSS(vars, contents); const QPalette palette = PreparePalette(vars, defaultPalette); setPalette(palette); setStyleSheet(stylesheet); #ifdef _DEBUG /* Write resulting QSS to file in config dir "themes" folder. */ string filename("obs-studio/themes/"); filename += theme->id.toStdString(); filename += ".out"; filesystem::path debugOut; char configPath[512]; if (GetConfigPath(configPath, sizeof(configPath), filename.c_str())) { debugOut = absolute(filesystem::u8path(configPath)); filesystem::create_directories(debugOut.parent_path()); } QFile debugFile(debugOut); if (debugFile.open(QIODeviceBase::WriteOnly)) { debugFile.write(stylesheet.toUtf8()); debugFile.flush(); } #endif #ifdef __APPLE__ SetMacOSDarkMode(theme->isDark); #endif emit StyleChanged(); if (themeWatcher) { themeWatcher->addPaths(filenames); /* Give it 250 ms before re-enabling the watcher to prevent too * many reloads when edited with an auto-saving IDE. */ QTimer::singleShot(250, this, [&] { themeWatcher->blockSignals(false); }); } return true; } void OBSApp::themeFileChanged(const QString &path) { themeWatcher->blockSignals(true); blog(LOG_INFO, "Theme file \"%s\" changed, reloading...", QT_TO_UTF8(path)); SetTheme(currentTheme->id); } static map themeMigrations = { {"Yami", DEFAULT_THEME}, {"Grey", "com.obsproject.Yami.Grey"}, {"Rachni", "com.obsproject.Yami.Rachni"}, {"Light", "com.obsproject.Yami.Light"}, {"Dark", "com.obsproject.Yami.Classic"}, {"Acri", "com.obsproject.Yami.Acri"}, {"System", "com.obsproject.System"}, }; bool OBSApp::InitTheme() { defaultPalette = palette(); #if !defined(_WIN32) && !defined(__APPLE__) setStyle(new OBSProxyStyle("Fusion")); #else setStyle(new OBSProxyStyle()); #endif /* Set search paths for custom 'theme:' URI prefix */ string searchDir; if (GetDataFilePath("themes", searchDir)) { auto installSearchDir = filesystem::u8path(searchDir); QDir::addSearchPath("theme", absolute(installSearchDir)); } char userDir[512]; if (GetConfigPath(userDir, sizeof(userDir), "obs-studio/themes")) { auto configSearchDir = filesystem::u8path(userDir); QDir::addSearchPath("theme", absolute(configSearchDir)); } /* Load list of themes and read their metadata */ FindThemes(); if (config_get_bool(globalConfig, "Appearance", "AutoReload")) { /* Set up Qt file watcher to automatically reload themes */ themeWatcher = new QFileSystemWatcher(this); connect(themeWatcher.get(), &QFileSystemWatcher::fileChanged, this, &OBSApp::themeFileChanged); } /* Migrate old theme config key */ if (config_has_user_value(globalConfig, "General", "CurrentTheme3") && !config_has_user_value(globalConfig, "Appearance", "Theme")) { const char *old = config_get_string(globalConfig, "General", "CurrentTheme3"); if (themeMigrations.count(old)) { config_set_string(globalConfig, "Appearance", "Theme", themeMigrations[old].c_str()); } } QString themeName = config_get_string(globalConfig, "Appearance", "Theme"); if (themeName.isEmpty() || !GetTheme(themeName)) { if (!themeName.isEmpty()) { blog(LOG_WARNING, "Loading theme \"%s\" failed, falling back to " "default theme (\"%s\").", QT_TO_UTF8(themeName), DEFAULT_THEME); } #ifdef _WIN32 themeName = HighContrastEnabled() ? "com.obsproject.System" : DEFAULT_THEME; #else themeName = DEFAULT_THEME; #endif } if (!SetTheme(themeName)) { blog(LOG_ERROR, "Loading default theme \"%s\" failed, falling back to " "system theme as last resort.", QT_TO_UTF8(themeName)); return SetTheme("com.obsproject.System"); } return true; }