valentina/src/libs/vmisc/theme/vtheme.cpp

613 lines
19 KiB
C++

/************************************************************************
**
** @file vtheme.cpp
** @author Roman Telezhynskyi <dismine(at)gmail.com>
** @date 17 7, 2023
**
** @brief
** @copyright
** This source code is part of the Valentina project, a pattern making
** program, whose allow create and modeling patterns of clothing.
** Copyright (C) 2023 Valentina project
** <https://gitlab.com/smart-pattern/valentina> All Rights Reserved.
**
** Valentina 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.
**
** Valentina 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 Valentina. If not, see <http://www.gnu.org/licenses/>.
**
*************************************************************************/
#include "vtheme.h"
#include "../vcommonsettings.h"
#include <QIcon>
#include <QOperatingSystemVersion>
#include <QPainter>
#include <QPalette>
#include <QPixmap>
#include <QStyle>
#include <QStyleFactory>
#include <QTextStream>
#include <QtDebug>
#include <QtGlobal>
#include <QtSvg/QSvgRenderer>
#if defined(Q_OS_MACX)
#include "macutils.h"
#endif
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
#include <QStyleHints>
#endif
#include "../vabstractapplication.h"
#include "vapplicationstyle.h"
#include "vscenestylesheet.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
#include "../compatibility.h"
#endif
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
namespace
{
//---------------------------------------------------------------------------------------------------------------------
#ifdef Q_OS_WIN
auto NativeWindowsDarkThemeAvailable() -> bool
{
// dark mode supported Windows 10 1809 10.0.17763 onward
// https://stackoverflow.com/questions/53501268/win10-dark-theme-how-to-use-in-winapi
if (QOperatingSystemVersion::current().majorVersion() > 10)
{
return true;
}
if (QOperatingSystemVersion::current().majorVersion() == 10)
{
return QOperatingSystemVersion::current().microVersion() >= 17763;
}
return false;
}
#endif // Q_OS_WIN
//---------------------------------------------------------------------------------------------------------------------
#if defined(Q_OS_MACX)
inline auto NativeMacDarkThemeAvailable() -> bool
{
return NSNativeMacDarkThemeAvailable();
}
#endif
//---------------------------------------------------------------------------------------------------------------------
#if defined(Q_OS_LINUX)
inline auto NativeLinuxDarkThemeAvailable() -> bool
{
// There is no way to check native support. Assume always available.
return true;
}
#endif
//---------------------------------------------------------------------------------------------------------------------
void ActivateCustomLightTheme()
{
QFile f(QStringLiteral(":/light/stylesheet.qss"));
if (!f.exists())
{
qDebug() << "Unable to set stylesheet, file not found\n";
}
else
{
f.open(QFile::ReadOnly | QFile::Text);
QTextStream ts(&f);
qApp->setStyleSheet(ts.readAll()); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
}
}
//---------------------------------------------------------------------------------------------------------------------
void ActivateCustomDarkTheme()
{
QFile f(QStringLiteral(":/dark/stylesheet.qss"));
if (!f.exists())
{
qDebug() << "Unable to set stylesheet, file not found\n";
}
else
{
f.open(QFile::ReadOnly | QFile::Text);
QTextStream ts(&f);
qApp->setStyleSheet(ts.readAll()); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
}
}
//---------------------------------------------------------------------------------------------------------------------
#if defined(Q_OS_WIN)
void ActivateDefaultThemeWin()
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
qApp->setStyleSheet(QString()); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
#else
if (VTheme::IsInDarkTheme())
{
ActivateCustomDarkTheme();
}
else
{
qApp->setStyleSheet(QString()); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
}
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
}
#endif // defined(Q_OS_WIN)
//---------------------------------------------------------------------------------------------------------------------
#if defined(Q_OS_MACX)
void ActivateDefaultThemeMac()
{
qApp->setStyleSheet(QString()); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
}
#endif // defined(Q_OS_MACX)
//---------------------------------------------------------------------------------------------------------------------
void ActivateDefaultTheme()
{
#if defined(Q_OS_WIN)
ActivateDefaultThemeWin();
#elif defined(Q_OS_MACX)
ActivateDefaultThemeMac();
#else
if (VTheme::IsInDarkTheme())
{
ActivateCustomDarkTheme();
}
else
{
qApp->setStyleSheet(QString()); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
}
#endif
}
//---------------------------------------------------------------------------------------------------------------------
auto GetResourceName(const QString &root, const QString &iconName, bool dark) -> QString
{
return QStringLiteral(":/%1/%2/%3").arg(root, dark ? "dark"_L1 : "light"_L1, iconName);
}
//---------------------------------------------------------------------------------------------------------------------
auto GetPixmapResource(const QString &root, const QString &iconName, bool dark) -> QPixmap
{
QString const resourceName = GetResourceName(root, iconName, dark);
QPixmap pixmap = QPixmap(resourceName);
Q_ASSERT(!pixmap.isNull());
return pixmap;
}
} // namespace
//---------------------------------------------------------------------------------------------------------------------
auto VTheme::Instance() -> VTheme *
{
static VTheme *instance = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
if (instance == nullptr)
{
instance = new VTheme();
}
return instance;
}
//---------------------------------------------------------------------------------------------------------------------
void VTheme::StoreDefaultThemeName(const QString &themeName)
{
m_defaultThemeName = themeName;
}
//---------------------------------------------------------------------------------------------------------------------
auto VTheme::NativeDarkThemeAvailable() -> bool
{
#if defined(Q_OS_MACX)
return NativeMacDarkThemeAvailable();
#elif defined(Q_OS_WIN)
return NativeWindowsDarkThemeAvailable();
#elif defined(Q_OS_LINUX)
return NativeLinuxDarkThemeAvailable();
#else
return false;
#endif
}
//---------------------------------------------------------------------------------------------------------------------
auto VTheme::IsInDarkTheme() -> bool
{
if (NativeDarkThemeAvailable())
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
QStyleHints *hints = QGuiApplication::styleHints();
return hints->colorScheme() == Qt::ColorScheme::Dark;
#else
#if defined(Q_OS_MACX)
return NSMacIsInDarkTheme();
#elif defined(Q_OS_WIN)
QSettings settings("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
QSettings::NativeFormat);
return settings.value("AppsUseLightTheme", 1).toInt() == 0;
#elif defined(Q_OS_LINUX)
return ShouldApplyDarkTheme();
#endif
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
}
return false;
}
//---------------------------------------------------------------------------------------------------------------------
auto VTheme::ShouldApplyDarkTheme() -> bool
{
QPalette const palette = qApp->palette(); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
return palette.color(QPalette::WindowText).lightness() > palette.color(QPalette::Window).lightness();
}
//---------------------------------------------------------------------------------------------------------------------
auto VTheme::ColorSheme() -> VColorSheme
{
VThemeMode const themeMode = VAbstractApplication::VApp()->Settings()->GetThemeMode();
if (themeMode == VThemeMode::Light)
{
return VColorSheme::Light;
}
if (themeMode == VThemeMode::Dark)
{
return VColorSheme::Dark;
}
if (NativeDarkThemeAvailable())
{
if (IsInDarkTheme())
{
return VColorSheme::Dark;
}
return VColorSheme::Light;
}
if (ShouldApplyDarkTheme())
{
return VColorSheme::Dark;
}
return VColorSheme::Light;
}
//---------------------------------------------------------------------------------------------------------------------
auto VTheme::DefaultThemeName() -> QString
{
VColorSheme const colorScheme = ColorSheme();
QString const themePrefix = (colorScheme == VColorSheme::Light ? QStringLiteral("Light") : QStringLiteral("Dark"));
#if defined(Q_OS_MACX)
return QStringLiteral("La-Sierra-%1").arg(themePrefix);
#else
return QStringLiteral("Eleven-%1").arg(themePrefix);
#endif
}
//---------------------------------------------------------------------------------------------------------------------
void VTheme::InitApplicationStyle()
{
VThemeMode const themeMode = VAbstractApplication::VApp()->Settings()->GetThemeMode();
if (themeMode == VThemeMode::Light || themeMode == VThemeMode::Dark)
{
QStyle *style = QStyleFactory::create(QStringLiteral("fusion"));
if (style != nullptr)
{
style = new VApplicationStyle(style);
QApplication::setStyle(style);
}
return;
}
#if defined(Q_OS_WIN)
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
if (NativeDarkThemeAvailable())
{
if (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark)
{
QApplication::setStyle(QStyleFactory::create(QStringLiteral("fusion")));
}
}
#endif
#endif
}
//---------------------------------------------------------------------------------------------------------------------
void VTheme::SetIconTheme()
{
if (not HasThemeIcon(VThemeIcon::DocumentOpen))
{
// If there is no default working icon theme then we should
// use an icon theme that we provide via a .qrc file
// This case happens under Windows and Mac OS X
// This does not happen under GNOME or KDE
QIcon::setThemeName(DefaultThemeName());
}
else
{
VThemeMode const themeMode = VAbstractApplication::VApp()->Settings()->GetThemeMode();
if ((themeMode == VThemeMode::Dark && !ShouldApplyDarkTheme()) ||
(themeMode == VThemeMode::Light && ShouldApplyDarkTheme()))
{
QIcon::setThemeName(DefaultThemeName());
}
}
}
//---------------------------------------------------------------------------------------------------------------------
void VTheme::SetToAutoTheme() const
{
qApp->setStyleSheet(QString()); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
QIcon::setThemeName(m_defaultThemeName);
}
//---------------------------------------------------------------------------------------------------------------------
void VTheme::InitThemeMode()
{
VThemeMode const themeMode = VAbstractApplication::VApp()->Settings()->GetThemeMode();
if (themeMode == VThemeMode::Light)
{
if (NativeDarkThemeAvailable())
{
if (IsInDarkTheme())
{
#if defined(Q_OS_MACX)
NSMacSetToLightTheme();
#else
ActivateCustomLightTheme();
#endif
}
}
else if (ShouldApplyDarkTheme())
{
ActivateCustomLightTheme();
}
}
else if (themeMode == VThemeMode::Dark)
{
if (NativeDarkThemeAvailable())
{
if (!IsInDarkTheme())
{
#if defined(Q_OS_MACX)
NSMacSetToDarkTheme();
#else
ActivateCustomDarkTheme();
#endif
}
}
else
{
ActivateCustomDarkTheme();
}
}
else
{
if (NativeDarkThemeAvailable())
{
ActivateDefaultTheme();
}
else
{
if (ShouldApplyDarkTheme())
{
ActivateCustomDarkTheme();
}
}
}
}
//---------------------------------------------------------------------------------------------------------------------
auto VTheme::ThemeStylesheet() -> QString
{
VThemeMode const themeMode = VAbstractApplication::VApp()->Settings()->GetThemeMode();
if (themeMode == VThemeMode::Light)
{
if (NativeDarkThemeAvailable())
{
if (IsInDarkTheme())
{
#if defined(Q_OS_MACX)
return QStringLiteral("native");
#else
return QStringLiteral("light");
#endif
}
}
else if (ShouldApplyDarkTheme())
{
return QStringLiteral("light");
}
return QStringLiteral("native");
}
if (themeMode == VThemeMode::Dark)
{
if (NativeDarkThemeAvailable())
{
if (!IsInDarkTheme())
{
#if defined(Q_OS_MACX)
return QStringLiteral("native");
#else
return QStringLiteral("dark");
#endif
}
}
else
{
return QStringLiteral("dark");
}
return QStringLiteral("native");
}
if (!NativeDarkThemeAvailable())
{
if (ShouldApplyDarkTheme())
{
return QStringLiteral("dark");
}
return QStringLiteral("light");
}
return QStringLiteral("native");
}
//---------------------------------------------------------------------------------------------------------------------
void VTheme::ResetThemeSettings() const
{
InitApplicationStyle();
SetToAutoTheme();
SetIconTheme();
InitThemeMode();
VSceneStylesheet::ResetStyles();
emit Instance()->ThemeSettingsChanged();
}
//---------------------------------------------------------------------------------------------------------------------
auto VTheme::GetFallbackThemeIcon(const QString &iconName, QSize iconSize) -> QIcon
{
const QString themePrefix = (ColorSheme() == VColorSheme::Light ? QStringLiteral("Light") : QStringLiteral("Dark"));
const QString themeName = QStringLiteral("Eleven-%1").arg(themePrefix);
const QString filePath = QStringLiteral(":icons/%1/%2.svg").arg(themeName, iconName);
QIcon icon;
icon.addFile(filePath, iconSize, QIcon::Normal, QIcon::On);
iconSize *= 2;
icon.addFile(filePath, iconSize, QIcon::Normal, QIcon::On);
return icon;
}
//---------------------------------------------------------------------------------------------------------------------
auto VTheme::GetIconResource(const QString &root, const QString &iconName) -> QIcon
{
QIcon icon;
bool const dark = (ColorSheme() == VColorSheme::Dark);
QPixmap pixmap = ::GetPixmapResource(root, iconName, dark);
icon.addPixmap(pixmap);
if (dark)
{
// automatic disabled icon is no good for dark
// paint transparent black to get disabled look
QPainter p(&pixmap);
p.fillRect(pixmap.rect(), QColor(48, 47, 47, 128));
icon.addPixmap(pixmap, QIcon::Disabled);
}
return icon;
}
//---------------------------------------------------------------------------------------------------------------------
auto VTheme::GetPixmapResource(const QString &root, const QString &iconName) -> QPixmap
{
bool const dark = (ColorSheme() == VColorSheme::Dark);
return ::GetPixmapResource(root, iconName, dark);
}
//---------------------------------------------------------------------------------------------------------------------
auto VTheme::GetResourceName(const QString &root, const QString &iconName) -> QString
{
return ::GetResourceName(root, iconName, ColorSheme() == VColorSheme::Dark);
}
//---------------------------------------------------------------------------------------------------------------------
VTheme::VTheme(QObject *parent)
: QObject(parent)
{
bool isProcessingColorSchemeChange = false;
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
auto colorSchemeChangedSlot = [this, &isProcessingColorSchemeChange]()
{
if (isProcessingColorSchemeChange)
{
return; // Already processing, avoid recursion
}
isProcessingColorSchemeChange = true;
QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
VCommonSettings *settings = VAbstractApplication::VApp()->Settings();
VThemeMode const themeMode = settings->GetThemeMode();
if (themeMode == VThemeMode::System && VTheme::NativeDarkThemeAvailable())
{
if (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark)
{
settings->SetThemeMode(VThemeMode::Light);
}
else
{
settings->SetThemeMode(VThemeMode::Dark);
}
ResetThemeSettings();
QCoreApplication::processEvents();
settings->SetThemeMode(themeMode);
}
ResetThemeSettings();
QGuiApplication::restoreOverrideCursor();
isProcessingColorSchemeChange = false;
};
QStyleHints *hints = QGuiApplication::styleHints();
connect(hints, &QStyleHints::colorSchemeChanged, this, colorSchemeChangedSlot);
#else
if (VTheme::NativeDarkThemeAvailable())
{
m_darkTheme = IsInDarkTheme();
m_themeTimer = new QTimer(this);
m_themeTimer->setTimerType(Qt::VeryCoarseTimer);
auto colorSchemeTimeoutCheck = [this, &isProcessingColorSchemeChange]()
{
if (isProcessingColorSchemeChange)
{
return; // Already processing, avoid recursion
}
isProcessingColorSchemeChange = true;
QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
if (bool darkTheme = IsInDarkTheme(); m_darkTheme != darkTheme)
{
m_darkTheme = darkTheme;
ResetThemeSettings();
}
QGuiApplication::restoreOverrideCursor();
isProcessingColorSchemeChange = false;
};
connect(m_themeTimer, &QTimer::timeout, this, colorSchemeTimeoutCheck);
m_themeTimer->start(5s);
}
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
}