Files
zeal/src/libs/core/settings.cpp
T
2026-03-22 15:30:20 +02:00

460 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 <QUrl>
#include <QUuid>
#include <QWebEngineProfile>
#include <QWebEngineSettings>
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
#include <QStyle>
#include <QStyleFactory>
#include <QStyleHints>
#else
#include <QPalette>
#endif
namespace {
// Configuration file groups
constexpr char GroupUI[] = "ui";
constexpr char GroupContent[] = "content";
constexpr char GroupDocsets[] = "docsets";
constexpr char GroupGlobalShortcuts[] = "global_shortcuts";
constexpr char GroupSearch[] = "search";
constexpr char GroupTabs[] = "tabs";
constexpr char GroupInternal[] = "internal";
constexpr char GroupState[] = "state";
constexpr char GroupProxy[] = "proxy";
} // namespace
using namespace Zeal::Core;
static Q_LOGGING_CATEGORY(log, "zeal.core.settings")
Settings::Settings(QObject *parent)
: QObject(parent)
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qRegisterMetaTypeStreamOperators<ContentAppearance>("ContentAppearance");
qRegisterMetaTypeStreamOperators<ExternalLinkPolicy>("ExternalLinkPolicy");
#else
qRegisterMetaType<ContentAppearance>("ContentAppearance");
qRegisterMetaType<ExternalLinkPolicy>("ExternalLinkPolicy");
#endif
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
// When the OS color scheme changes, reapply the color scheme.
connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged,
this, [this]() {
if (contentAppearance == ContentAppearance::Automatic) {
applyColorScheme();
}
});
#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;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
void Settings::applyColorScheme()
{
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;
}
qApp->styleHints()->setColorScheme(scheme);
// setColorScheme() alone doesn't reliably update existing widgets:
// - Widgets with stylesheets (QStyleSheetStyle) only update on polish().
// - Direct palette consumers (QTreeView, QLineEdit) only update when the
// application palette changes.
// Re-instantiating the style triggers the full unpolish-polish cycle for
// QStyleSheetStyle widgets and also updates the application palette via
// the new style's standardPalette().
QStyle *newStyle = QStyleFactory::create(qApp->style()->name());
if (newStyle != nullptr) {
qApp->setStyle(newStyle);
}
}
#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()
{
QScopedPointer<QSettings> settings(qsettings());
migrate(settings.data());
// TODO: Put everything in groups
startMinimized = settings->value(QStringLiteral("start_minimized"), false).toBool();
checkForUpdate = settings->value(QStringLiteral("check_for_update"), true).toBool();
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();
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();
settings->endGroup();
settings->beginGroup(GroupSearch);
isFuzzySearchEnabled = settings->value(QStringLiteral("fuzzy_search_enabled"), false).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, 5, 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 = static_cast<ProxyType>(settings->value(QStringLiteral("type"),
ProxyType::System).toUInt());
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(GroupState);
windowGeometry = settings->value(QStringLiteral("window_geometry")).toByteArray();
verticalSplitterGeometry = settings->value(QStringLiteral("splitter_geometry")).toByteArray();
tocSplitterState = settings->value(QStringLiteral("toc_splitter_state")).toByteArray();
settings->endGroup();
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()
{
QScopedPointer<QSettings> 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->endGroup();
settings->beginGroup(GroupGlobalShortcuts);
settings->setValue(QStringLiteral("show"), showShortcut);
settings->endGroup();
settings->beginGroup(GroupTabs);
settings->setValue(QStringLiteral("open_new_tab_after_active"), openNewTabAfterActive);
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"), 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(GroupState);
settings->setValue(QStringLiteral("window_geometry"), windowGeometry);
settings->setValue(QStringLiteral("splitter_geometry"), verticalSplitterGeometry);
settings->setValue(QStringLiteral("toc_splitter_state"), tocSplitterState);
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();
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
applyColorScheme();
#endif
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) const
{
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();
}
}
//
// Pre 0.3
//
// Unset 'state/splitter_geometry', because custom styles were removed.
if (version < QVersionNumber(0, 3, 0)) {
settings->beginGroup(GroupState);
settings->remove(QStringLiteral("splitter_geometry"));
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.
*/
QSettings *Settings::qsettings(QObject *parent)
{
#ifndef PORTABLE_BUILD
return new QSettings(parent);
#else
return new QSettings(QCoreApplication::applicationDirPath() + QLatin1String("/zeal.ini"),
QSettings::IniFormat, parent);
#endif
}
QDataStream &operator<<(QDataStream &out, Settings::ContentAppearance policy)
{
out << static_cast<std::underlying_type_t<Settings::ContentAppearance>>(policy);
return out;
}
QDataStream &operator>>(QDataStream &in, Settings::ContentAppearance &policy)
{
std::underlying_type_t<Settings::ContentAppearance> value;
in >> value;
policy = static_cast<Settings::ContentAppearance>(value);
return in;
}
QDataStream &operator<<(QDataStream &out, Settings::ExternalLinkPolicy policy)
{
out << static_cast<std::underlying_type_t<Settings::ExternalLinkPolicy>>(policy);
return out;
}
QDataStream &operator>>(QDataStream &in, Settings::ExternalLinkPolicy &policy)
{
std::underlying_type_t<Settings::ExternalLinkPolicy> value;
in >> value;
policy = static_cast<Settings::ExternalLinkPolicy>(value);
return in;
}