/* 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;
}