Files
zeal/src/libs/core/settings.cpp
T
2026-06-04 20:57:09 +03:00

450 lines
17 KiB
C++

// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "settings.h"
#include "application.h"
#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QLoggingCategory>
#include <QSettings>
#include <QStandardPaths>
#include <QSystemTrayIcon>
#include <QUrl>
#include <QUuid>
#include <QWebEngineProfile>
#include <QWebEngineSettings>
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
#include <QStyleHints>
#else
#include <QPalette>
#endif
namespace Zeal::Core {
namespace {
Q_LOGGING_CATEGORY(log, "zeal.core.settings")
using Qt::Literals::StringLiterals::operator""_L1;
// Configuration file groups
constexpr auto GroupUI = "ui"_L1;
constexpr auto GroupContent = "content"_L1;
constexpr auto GroupDocsets = "docsets"_L1;
constexpr auto GroupGlobalShortcuts = "global_shortcuts"_L1;
constexpr auto GroupSearch = "search"_L1;
constexpr auto GroupTabs = "tabs"_L1;
constexpr auto GroupInternal = "internal"_L1;
constexpr auto GroupProxy = "proxy"_L1;
} // namespace
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init,hicpp-member-init): initialized by load().
Settings::Settings(QObject *parent)
: QObject(parent)
{
qRegisterMetaType<ContentAppearance>("ContentAppearance");
qRegisterMetaType<ExternalLinkPolicy>("ExternalLinkPolicy");
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
// When the OS color scheme changes, reapply the color scheme.
connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this, [this]() {
if (contentAppearance == ContentAppearance::Automatic) {
applyColorScheme();
emit updated();
}
});
#endif
load();
}
Settings::~Settings()
{
save();
}
bool Settings::isDarkModeEnabled() const
{
if (contentAppearance == ContentAppearance::Dark) {
return true;
}
if (contentAppearance == ContentAppearance::Automatic && colorScheme() == ColorScheme::Dark) {
return true;
}
return false;
}
bool Settings::isTrayActive() const
{
return showSystrayIcon && QSystemTrayIcon::isSystemTrayAvailable();
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
void Settings::applyColorScheme() const
{
Qt::ColorScheme scheme = Qt::ColorScheme::Unknown;
switch (contentAppearance) {
case ContentAppearance::Light:
scheme = Qt::ColorScheme::Light;
break;
case ContentAppearance::Dark:
scheme = Qt::ColorScheme::Dark;
break;
default:
break;
}
// Setting the scheme updates the platform palette. The full widget refresh
// (re-deriving the palette and re-polishing existing widgets) is handled by
// re-installing the application style on QStyleHints::colorSchemeChanged; see
// app/main.cpp. Doing it there preserves the active QProxyStyle wrapper that
// restyles the tab close button, which re-creating the style here would drop.
qApp->styleHints()->setColorScheme(scheme);
}
#endif
Settings::ColorScheme Settings::colorScheme()
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
return QApplication::styleHints()->colorScheme();
#else
// Pre Qt 6.5 detection from https://www.qt.io/blog/dark-mode-on-windows-11-with-qt-6.5.
const QPalette p;
if (p.color(QPalette::WindowText).lightness() > p.color(QPalette::Window).lightness()) {
return ColorScheme::Dark;
}
return ColorScheme::Light;
#endif
}
void Settings::load()
{
auto settings = qsettings();
qCDebug(log, "Using settings file: %s", qPrintable(settings->fileName()));
migrate(settings.get());
// TODO: Put everything in groups
startMinimized = settings->value(QStringLiteral("start_minimized"), false).toBool();
#ifdef ZEAL_FEATURE_UPDATE_CHECK
checkForUpdate = settings->value(QStringLiteral("check_for_update"), true).toBool();
#else
checkForUpdate = settings->value(QStringLiteral("check_for_update"), false).toBool();
#endif
showSystrayIcon = settings->value(QStringLiteral("show_systray_icon"), true).toBool();
minimizeToSystray = settings->value(QStringLiteral("minimize_to_systray"), false).toBool();
hideOnClose = settings->value(QStringLiteral("hide_on_close"), false).toBool();
settings->beginGroup(GroupUI);
hideMenuBar = settings->value(QStringLiteral("hide_menu_bar"), false).toBool();
hideSidebar = settings->value(QStringLiteral("hide_sidebar"), false).toBool();
settings->endGroup();
settings->beginGroup(GroupGlobalShortcuts);
showShortcut = settings->value(QStringLiteral("show")).value<QKeySequence>();
settings->endGroup();
settings->beginGroup(GroupTabs);
openNewTabAfterActive = settings->value(QStringLiteral("open_new_tab_after_active"), false).toBool();
showTabCloseButton = settings->value(QStringLiteral("show_tab_close_button"), true).toBool();
settings->endGroup();
settings->beginGroup(GroupSearch);
isFuzzySearchEnabled = settings->value(QStringLiteral("fuzzy_search_enabled"), true).toBool();
settings->endGroup();
settings->beginGroup(GroupContent);
contentAppearance = settings->value(QStringLiteral("appearance"), QVariant::fromValue(ContentAppearance::Automatic))
.value<ContentAppearance>();
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
// Dark mode needs to be applied before Qt WebEngine is initialized.
if (isDarkModeEnabled()) {
QByteArray chromiumFlags = qgetenv("QTWEBENGINE_CHROMIUM_FLAGS");
if (!chromiumFlags.isEmpty()) {
chromiumFlags += ' ';
}
chromiumFlags += "--blink-settings=forceDarkModeEnabled=true,darkModeInversionAlgorithm=4";
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags);
}
#endif
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
applyColorScheme();
#endif
// Fonts
QWebEngineSettings *webSettings = QWebEngineProfile::defaultProfile()->settings();
serifFontFamily = settings
->value(QStringLiteral("serif_font_family"),
webSettings->fontFamily(QWebEngineSettings::SerifFont))
.toString();
sansSerifFontFamily = settings
->value(QStringLiteral("sans_serif_font_family"),
webSettings->fontFamily(QWebEngineSettings::SansSerifFont))
.toString();
fixedFontFamily = settings
->value(QStringLiteral("fixed_font_family"),
webSettings->fontFamily(QWebEngineSettings::FixedFont))
.toString();
static const QMap<QString, QWebEngineSettings::FontFamily> fontFamilies =
{{QStringLiteral("sans-serif"), QWebEngineSettings::SansSerifFont},
{QStringLiteral("serif"), QWebEngineSettings::SerifFont},
{QStringLiteral("monospace"), QWebEngineSettings::FixedFont}};
defaultFontFamily = settings->value(QStringLiteral("default_font_family"), QStringLiteral("serif")).toString();
// Fallback to the serif font family.
if (!fontFamilies.contains(defaultFontFamily)) {
defaultFontFamily = QStringLiteral("serif");
}
webSettings->setFontFamily(QWebEngineSettings::SansSerifFont, sansSerifFontFamily);
webSettings->setFontFamily(QWebEngineSettings::SerifFont, serifFontFamily);
webSettings->setFontFamily(QWebEngineSettings::FixedFont, fixedFontFamily);
const QString defaultFontFamilyResolved = webSettings->fontFamily(fontFamilies.value(defaultFontFamily));
webSettings->setFontFamily(QWebEngineSettings::StandardFont, defaultFontFamilyResolved);
defaultFontSize = settings
->value(QStringLiteral("default_font_size"),
webSettings->fontSize(QWebEngineSettings::DefaultFontSize))
.toInt();
defaultFixedFontSize = settings
->value(QStringLiteral("default_fixed_font_size"),
webSettings->fontSize(QWebEngineSettings::DefaultFixedFontSize))
.toInt();
minimumFontSize = settings
->value(QStringLiteral("minimum_font_size"),
webSettings->fontSize(QWebEngineSettings::MinimumFontSize))
.toInt();
webSettings->setFontSize(QWebEngineSettings::DefaultFontSize, defaultFontSize);
webSettings->setFontSize(QWebEngineSettings::DefaultFixedFontSize, defaultFixedFontSize);
webSettings->setFontSize(QWebEngineSettings::MinimumFontSize, minimumFontSize);
isHighlightOnNavigateEnabled = settings->value(QStringLiteral("highlight_on_navigate"), true).toBool();
customCssFile = settings->value(QStringLiteral("custom_css_file")).toString();
externalLinkPolicy = settings
->value(QStringLiteral("external_link_policy"),
QVariant::fromValue(ExternalLinkPolicy::Ask))
.value<ExternalLinkPolicy>();
isSmoothScrollingEnabled = settings->value(QStringLiteral("smooth_scrolling"), true).toBool();
settings->endGroup();
settings->beginGroup(GroupProxy);
proxyType = settings->value(QStringLiteral("type"), QVariant::fromValue(ProxyType::System)).value<ProxyType>();
proxyHost = settings->value(QStringLiteral("host")).toString();
proxyPort = static_cast<quint16>(settings->value(QStringLiteral("port"), 0).toUInt());
proxyAuthenticate = settings->value(QStringLiteral("authenticate"), false).toBool();
proxyUserName = settings->value(QStringLiteral("username")).toString();
proxyPassword = settings->value(QStringLiteral("password")).toString();
isIgnoreSslErrorsEnabled = settings->value(QStringLiteral("ignore_ssl_errors"), false).toBool();
settings->endGroup();
settings->beginGroup(GroupDocsets);
if (settings->contains(QStringLiteral("path"))) {
docsetPath = settings->value(QStringLiteral("path")).toString();
} else {
#ifndef PORTABLE_BUILD
docsetPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QLatin1String("/docsets");
#else
docsetPath = QStringLiteral("docsets");
#endif
}
settings->endGroup();
// Create the docset storage directory if it doesn't exist.
const QFileInfo fi(docsetPath);
if (!fi.exists()) {
const QString path = fi.isRelative() ? QCoreApplication::applicationDirPath() + QLatin1String("/") + docsetPath
: docsetPath;
if (!QDir().mkpath(path)) {
qCWarning(log, "Failed to create docset storage directory '%s'.", qPrintable(path));
}
}
settings->beginGroup(GroupInternal);
installId = settings
->value(QStringLiteral("install_id"),
// Avoid curly braces (QTBUG-885)
QUuid::createUuid().toString().mid(1, 36))
.toString();
settings->endGroup();
}
void Settings::save()
{
auto settings = qsettings();
// TODO: Put everything in groups
settings->setValue(QStringLiteral("start_minimized"), startMinimized);
settings->setValue(QStringLiteral("check_for_update"), checkForUpdate);
settings->setValue(QStringLiteral("show_systray_icon"), showSystrayIcon);
settings->setValue(QStringLiteral("minimize_to_systray"), minimizeToSystray);
settings->setValue(QStringLiteral("hide_on_close"), hideOnClose);
settings->beginGroup(GroupUI);
settings->setValue(QStringLiteral("hide_menu_bar"), hideMenuBar);
settings->setValue(QStringLiteral("hide_sidebar"), hideSidebar);
settings->endGroup();
settings->beginGroup(GroupGlobalShortcuts);
settings->setValue(QStringLiteral("show"), showShortcut);
settings->endGroup();
settings->beginGroup(GroupTabs);
settings->setValue(QStringLiteral("open_new_tab_after_active"), openNewTabAfterActive);
settings->setValue(QStringLiteral("show_tab_close_button"), showTabCloseButton);
settings->endGroup();
settings->beginGroup(GroupSearch);
settings->setValue(QStringLiteral("fuzzy_search_enabled"), isFuzzySearchEnabled);
settings->endGroup();
settings->beginGroup(GroupContent);
settings->setValue(QStringLiteral("default_font_family"), defaultFontFamily);
settings->setValue(QStringLiteral("serif_font_family"), serifFontFamily);
settings->setValue(QStringLiteral("sans_serif_font_family"), sansSerifFontFamily);
settings->setValue(QStringLiteral("fixed_font_family"), fixedFontFamily);
settings->setValue(QStringLiteral("default_font_size"), defaultFontSize);
settings->setValue(QStringLiteral("default_fixed_font_size"), defaultFixedFontSize);
settings->setValue(QStringLiteral("minimum_font_size"), minimumFontSize);
settings->setValue(QStringLiteral("appearance"), QVariant::fromValue(contentAppearance));
settings->setValue(QStringLiteral("highlight_on_navigate"), isHighlightOnNavigateEnabled);
settings->setValue(QStringLiteral("custom_css_file"), customCssFile);
settings->setValue(QStringLiteral("external_link_policy"), QVariant::fromValue(externalLinkPolicy));
settings->setValue(QStringLiteral("smooth_scrolling"), isSmoothScrollingEnabled);
settings->endGroup();
settings->beginGroup(GroupProxy);
settings->setValue(QStringLiteral("type"), QVariant::fromValue(proxyType));
settings->setValue(QStringLiteral("host"), proxyHost);
settings->setValue(QStringLiteral("port"), proxyPort);
settings->setValue(QStringLiteral("authenticate"), proxyAuthenticate);
settings->setValue(QStringLiteral("username"), proxyUserName);
settings->setValue(QStringLiteral("password"), proxyPassword);
settings->setValue(QStringLiteral("ignore_ssl_errors"), isIgnoreSslErrorsEnabled);
settings->endGroup();
settings->beginGroup(GroupDocsets);
settings->setValue(QStringLiteral("path"), docsetPath);
settings->endGroup();
settings->beginGroup(GroupInternal);
settings->setValue(QStringLiteral("install_id"), installId);
// Version of configuration file format, should match Zeal version. Used for migration rules.
settings->setValue(QStringLiteral("version"), Application::version().toString());
settings->endGroup();
settings->sync();
emit updated();
}
/*!
* \internal
* \brief Migrates settings from older application versions.
* \param settings QSettings object to update.
*
* The settings migration process relies on 'internal/version' option, that was introduced in the
* release 0.2.0, so a missing option indicates pre-0.2 release.
*/
void Settings::migrate(QSettings *settings)
{
settings->beginGroup(GroupInternal);
const auto version = QVersionNumber::fromString(settings->value(QStringLiteral("version")).toString());
settings->endGroup();
//
// 0.6.0
//
// Unset content.default_fixed_font_size.
// The causing bug was 0.6.1 (#903), but the incorrect setting still comes to haunt us (#1054).
if (version == QVersionNumber(0, 6, 0)) {
settings->beginGroup(GroupContent);
settings->remove(QStringLiteral("default_fixed_font_size"));
settings->endGroup();
}
//
// Pre 0.4
//
// Rename 'browser' group into 'content'.
if (version < QVersionNumber(0, 4, 0)) {
settings->beginGroup(QStringLiteral("browser"));
const QVariant tmpMinimumFontSize = settings->value(QStringLiteral("minimum_font_size"));
settings->endGroup();
settings->remove(QStringLiteral("browser"));
if (tmpMinimumFontSize.isValid()) {
settings->beginGroup(GroupContent);
settings->setValue(QStringLiteral("minimum_font_size"), tmpMinimumFontSize);
settings->endGroup();
}
}
}
/*!
* \internal
* \brief Returns an initialized QSettings object.
* \param parent Optional parent object.
* \return QSettings object.
*
* QSettings is initialized according to build options, e.g. standard vs portable.
* Caller is responsible for deleting the returned object.
*/
std::unique_ptr<QSettings> Settings::qsettings()
{
#ifndef PORTABLE_BUILD
return std::make_unique<QSettings>();
#else
return std::make_unique<QSettings>(QCoreApplication::applicationDirPath() + QLatin1String("/zeal.ini"),
QSettings::IniFormat);
#endif
}
} // namespace Zeal::Core
QDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::ContentAppearance policy)
{
out << static_cast<std::underlying_type_t<Zeal::Core::Settings::ContentAppearance>>(policy);
return out;
}
QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ContentAppearance &policy)
{
std::underlying_type_t<Zeal::Core::Settings::ContentAppearance> value = 0;
in >> value;
policy = static_cast<Zeal::Core::Settings::ContentAppearance>(value);
return in;
}
QDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::ExternalLinkPolicy policy)
{
out << static_cast<std::underlying_type_t<Zeal::Core::Settings::ExternalLinkPolicy>>(policy);
return out;
}
QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ExternalLinkPolicy &policy)
{
std::underlying_type_t<Zeal::Core::Settings::ExternalLinkPolicy> value = 0;
in >> value;
policy = static_cast<Zeal::Core::Settings::ExternalLinkPolicy>(value);
return in;
}