/* SPDX-FileCopyrightText: 2026 Project Tick * SPDX-FileContributor: Project Tick * SPDX-License-Identifier: GPL-3.0-or-later * * MeshMC - A Custom Launcher for Minecraft * Copyright (C) 2026 Project Tick * * 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 3 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 "ThemeManager.h" #include "ITheme.h" #include "SystemTheme.h" #include "DarkTheme.h" #include "BrightTheme.h" #include "CustomTheme.h" #include "CatPack.h" #include "Application.h" #include "Exception.h" #include #include #include #include #include #include #include #include #include ThemeManager::ThemeManager() { const auto& style = QApplication::style(); m_defaultStyle = style->objectName(); m_defaultPalette = QApplication::palette(); // Default "System" theme addTheme( std::make_unique(m_defaultStyle, m_defaultPalette, true)); // Built-in Fusion themes auto darkTheme = new DarkTheme(); addTheme(std::unique_ptr(darkTheme)); addTheme(std::make_unique()); // System widget themes from QStyleFactory QStringList styles = QStyleFactory::keys(); for (auto& st : styles) { #ifdef Q_OS_WINDOWS if (QSysInfo::productVersion() != "11" && st == "windows11") { continue; } #endif addTheme(std::make_unique(st, m_defaultPalette, false)); } // Custom theme addTheme(std::make_unique(darkTheme, "custom")); initIconThemes(); initializeCatPacks(); } void ThemeManager::addTheme(std::unique_ptr theme) { QString id = theme->id(); m_themes.insert(std::make_pair(id, std::move(theme))); } ITheme* ThemeManager::getTheme(const QString& id) { auto it = m_themes.find(id); if (it != m_themes.end()) { return it->second.get(); } return nullptr; } void ThemeManager::setApplicationTheme(const QString& id, bool initial) { auto theme = getTheme(id); if (theme) { theme->apply(initial); } else { qWarning() << "Tried to set invalid theme:" << id; } } void ThemeManager::setIconTheme(const QString& name) { XdgIcon::setThemeName(name); } void ThemeManager::applyCurrentlySelectedTheme(bool initial) { auto settings = APPLICATION->settings(); // Apply widget theme first (sets palette) auto applicationTheme = settings->get("ApplicationTheme").toString(); if (applicationTheme.isEmpty()) { applicationTheme = "system"; } setApplicationTheme(applicationTheme, initial); // Auto-resolve icon variant based on the now-active palette brightness auto iconTheme = settings->get("IconTheme").toString(); if (!iconTheme.isEmpty()) { auto resolved = bestIconThemeForPalette(iconTheme); if (resolved != iconTheme) { settings->set("IconTheme", resolved); } setIconTheme(resolved); } } std::vector ThemeManager::allThemes() { std::vector ret; for (auto& pair : m_themes) { ret.push_back(pair.second.get()); } return ret; } QStringList ThemeManager::families() { QStringList ret; QSet seen; for (auto& pair : m_themes) { QString fam = pair.second->family(); if (!seen.contains(fam)) { seen.insert(fam); ret.append(fam); } } return ret; } std::vector ThemeManager::themesInFamily(const QString& family) { std::vector ret; for (auto& pair : m_themes) { if (pair.second->family() == family) { ret.push_back(pair.second.get()); } } return ret; } QList ThemeManager::iconThemes() const { return m_iconThemes; } QStringList ThemeManager::iconThemeFamilies() const { QStringList ret; QSet seen; for (const auto& entry : m_iconThemes) { const QString& fam = entry.family; if (!seen.contains(fam)) { seen.insert(fam); ret.append(fam); } } return ret; } QList ThemeManager::iconThemesInFamily(const QString& family) const { QList ret; for (const auto& entry : m_iconThemes) { if (entry.family == family) { ret.append(entry); } } return ret; } QString ThemeManager::resolveIconTheme(const QString& family) const { auto entries = iconThemesInFamily(family); if (entries.size() <= 1) { return entries.isEmpty() ? QString() : entries[0].id; } // Check if family has variants bool hasVariants = false; for (const auto& entry : entries) { if (!entry.variant.isEmpty()) { hasVariants = true; break; } } if (!hasVariants) { return entries[0].id; } // Auto-detect based on current palette brightness auto windowColor = QApplication::palette().color(QPalette::Window); bool isDark = windowColor.lightnessF() < 0.5; for (const auto& entry : entries) { QString v = entry.variant.toLower(); if (isDark && v == "dark") return entry.id; if (!isDark && v == "light") return entry.id; } return entries[0].id; } QString ThemeManager::bestIconThemeForPalette(const QString& currentIconId) const { // Find the family of the current icon theme QString family; for (const auto& entry : m_iconThemes) { if (entry.id == currentIconId) { family = entry.family; break; } } if (family.isEmpty()) { return currentIconId; } // Resolve the best variant for that family based on current palette QString resolved = resolveIconTheme(family); return resolved.isEmpty() ? currentIconId : resolved; } void ThemeManager::initIconThemes() { m_iconThemes = { {"pe_colored", QObject::tr("Default"), QObject::tr("Default"), QString()}, {"multimc", QStringLiteral("MultiMC"), QStringLiteral("MultiMC"), QString()}, {"pe_dark", QObject::tr("Simple (Dark Icons)"), QObject::tr("Simple (Dark Icons)"), QString()}, {"pe_light", QObject::tr("Simple (Light Icons)"), QObject::tr("Simple (Light Icons)"), QString()}, {"pe_blue", QObject::tr("Simple (Blue Icons)"), QObject::tr("Simple (Blue Icons)"), QString()}, {"pe_colored", QObject::tr("Simple (Colored Icons)"), QObject::tr("Simple (Colored Icons)"), QString()}, {"OSX", QStringLiteral("OSX"), QStringLiteral("OSX"), QString()}, {"iOS", QStringLiteral("iOS"), QStringLiteral("iOS"), QString()}, {"flat", QObject::tr("Flat (Light)"), QStringLiteral("Flat"), QObject::tr("Light")}, {"flat_white", QObject::tr("Flat (Dark)"), QStringLiteral("Flat"), QObject::tr("Dark")}, {"breeze_dark", QObject::tr("Breeze (Dark)"), QStringLiteral("Breeze"), QObject::tr("Dark")}, {"breeze_light", QObject::tr("Breeze (Light)"), QStringLiteral("Breeze"), QObject::tr("Light")}, {"custom", QObject::tr("Custom"), QObject::tr("Custom"), QString()}, }; } void ThemeManager::initializeCatPacks() { QList> defaultCats{ {"kitteh", QObject::tr("Background Cat (from MultiMC)")}, {"rory", QObject::tr("Rory ID 11 (drawn by Ashtaka)")}, {"rory-flat", QObject::tr("Rory ID 11 (flat edition, drawn by Ashtaka)")}, {"teawie", QObject::tr("Teawie (drawn by SympathyTea)")}}; for (const auto& [id, name] : defaultCats) { addCatPack(std::make_unique(id, name)); } // Create catpacks folder in data directory m_catPacksFolder = QDir("catpacks"); if (!m_catPacksFolder.mkpath(".")) qWarning() << "Couldn't create catpacks folder"; QStringList supportedImageFormats; for (const auto& format : QImageReader::supportedImageFormats()) { supportedImageFormats.append("*." + format); } auto loadFiles = [this, &supportedImageFormats](const QDir& dir) { QDirIterator it(dir.absolutePath(), supportedImageFormats, QDir::Files); while (it.hasNext()) { QFileInfo info(it.next()); addCatPack(std::make_unique(info)); } }; // Load image files in catpacks folder root loadFiles(m_catPacksFolder); // Load subdirectories QDirIterator directoryIterator(m_catPacksFolder.path(), QDir::Dirs | QDir::NoDotAndDotDot); while (directoryIterator.hasNext()) { QDir dir(directoryIterator.next()); QFileInfo manifest(dir.absoluteFilePath("catpack.json")); if (manifest.isFile()) { try { addCatPack(std::make_unique(manifest)); } catch (const Exception& e) { qWarning() << "Couldn't load catpack json:" << e.cause(); } } else { loadFiles(dir); } } } void ThemeManager::addCatPack(std::unique_ptr catPack) { QString id = catPack->id(); if (m_catPacks.find(id) == m_catPacks.end()) m_catPacks.emplace(id, std::move(catPack)); else qWarning() << "CatPack" << id << "not added to prevent id duplication"; } QString ThemeManager::getCatPack(const QString& catName) { QString id = catName.isEmpty() ? APPLICATION->settings()->get("BackgroundCat").toString() : catName; auto it = m_catPacks.find(id); if (it != m_catPacks.end()) return it->second->path(); // Fallback to first available if (!m_catPacks.empty()) return m_catPacks.begin()->second->path(); return QString(); } QList ThemeManager::getValidCatPacks() { QList ret; ret.reserve(m_catPacks.size()); for (auto& [id, pack] : m_catPacks) { ret.append(pack.get()); } return ret; } QDir ThemeManager::getCatPacksFolder() { return m_catPacksFolder; }