主题 - 新增「按时段自动」模式:6:00–18:00 浅色,其余时间深色 - 主题菜单改为四种模式,默认仍为跟随系统 侧栏 - 左侧文件夹侧栏(Ctrl+B 开关),递归扫描 *.md / *.markdown / *.mdown / *.mkd - 文件 → 打开文件夹…(Ctrl+Shift+O),记住上次打开的目录 - 点击文件名即打开,当前文件在列表中高亮,支持手动刷新 编辑 - 左下角 <src> 按钮 / Ctrl+Shift+M 切换源代码模式(关高亮、行号、当前行) - Ctrl+1~6 设置标题级别,再按一次取消 - Ctrl+/ 把选中内容包成围栏代码块,再按一次拆掉(含光标在块内的情况) 其他 - 模板回归脚本验证了标题与代码块的文本变换 - README 与截图更新
430 lines
17 KiB
C++
430 lines
17 KiB
C++
#include "ThemeManager.h"
|
|
|
|
#include <QApplication>
|
|
#include <QDateTime>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QGuiApplication>
|
|
#include <QPalette>
|
|
#include <QProcess>
|
|
#include <QSettings>
|
|
#include <QStandardPaths>
|
|
#include <QTime>
|
|
#include <QTimer>
|
|
#include <QXmlStreamReader>
|
|
|
|
namespace {
|
|
|
|
QString runCommand(const QString &program, const QStringList &arguments, int timeoutMs)
|
|
{
|
|
QProcess process;
|
|
process.start(program, arguments);
|
|
if (!process.waitForFinished(timeoutMs)) {
|
|
process.kill();
|
|
process.waitForFinished(200);
|
|
return QString();
|
|
}
|
|
if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0)
|
|
return QString();
|
|
return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
|
|
}
|
|
|
|
bool looksDark(const QString &themeName)
|
|
{
|
|
return themeName.contains(QLatin1String("dark"), Qt::CaseInsensitive);
|
|
}
|
|
|
|
/// XFCE 把界面主题记录在 xfconf 的 xsettings.xml 里。
|
|
QString xfceThemeName()
|
|
{
|
|
QFile file(QDir::homePath() + QStringLiteral("/.config/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml"));
|
|
if (!file.open(QIODevice::ReadOnly))
|
|
return QString();
|
|
|
|
QXmlStreamReader xml(&file);
|
|
while (!xml.atEnd()) {
|
|
xml.readNext();
|
|
if (!xml.isStartElement() || xml.name().toString() != QLatin1String("property"))
|
|
continue;
|
|
const QXmlStreamAttributes attributes = xml.attributes();
|
|
if (attributes.value(QLatin1String("name")).toString() == QLatin1String("ThemeName"))
|
|
return attributes.value(QLatin1String("value")).toString();
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
QColor hex(const char *value)
|
|
{
|
|
return QColor(QString::fromLatin1(value));
|
|
}
|
|
|
|
ThemeColors lightColors()
|
|
{
|
|
ThemeColors c;
|
|
c.window = hex("#f2f3f5");
|
|
c.windowText = hex("#1f2328");
|
|
c.base = hex("#ffffff");
|
|
c.alternateBase = hex("#f6f7f9");
|
|
c.text = hex("#1f2328");
|
|
c.button = hex("#ffffff");
|
|
c.buttonText = hex("#1f2328");
|
|
c.border = hex("#dcdfe4");
|
|
c.accent = hex("#0969da");
|
|
c.highlight = hex("#cfe3ff");
|
|
c.highlightedText = hex("#0b1f38");
|
|
c.disabled = hex("#9aa2ab");
|
|
c.placeholder = hex("#6b7480");
|
|
c.menuBackground = hex("#ffffff");
|
|
c.scrollHandle = hex("#c9ced6");
|
|
c.scrollHandleHover = hex("#aeb5bf");
|
|
c.sidebarBackground = hex("#f8f9fb");
|
|
c.hoverBackground = hex("#eaedf1");
|
|
|
|
c.editorBase = hex("#ffffff");
|
|
c.editorText = hex("#24292f");
|
|
c.gutterBackground = hex("#fafbfc");
|
|
c.gutterText = hex("#b0b7c0");
|
|
c.gutterActiveText = hex("#5c6670");
|
|
c.currentLine = hex("#f4f6f9");
|
|
|
|
c.heading = hex("#0a52a3");
|
|
c.headingMarker = hex("#b3b9c2");
|
|
c.strong = hex("#11151a");
|
|
c.emphasis = hex("#4b535c");
|
|
c.code = hex("#b4295a");
|
|
c.codeBackground = hex("#f2f4f7");
|
|
c.link = hex("#0969da");
|
|
c.linkUrl = hex("#6b7480");
|
|
c.quote = hex("#6b7480");
|
|
c.quoteMarker = hex("#c3c9d2");
|
|
c.listMarker = hex("#0969da");
|
|
c.rule = hex("#d6dae0");
|
|
c.taskDone = hex("#1a7f37");
|
|
return c;
|
|
}
|
|
|
|
ThemeColors darkColors()
|
|
{
|
|
ThemeColors c;
|
|
c.window = hex("#1b1d21");
|
|
c.windowText = hex("#d6d9dd");
|
|
c.base = hex("#1e2024");
|
|
c.alternateBase = hex("#23262b");
|
|
c.text = hex("#d6d9dd");
|
|
c.button = hex("#26292f");
|
|
c.buttonText = hex("#d6d9dd");
|
|
c.border = hex("#2d3137");
|
|
c.accent = hex("#3b8eea");
|
|
c.highlight = hex("#26466b");
|
|
c.highlightedText = hex("#eef3f8");
|
|
c.disabled = hex("#5d646d");
|
|
c.placeholder = hex("#8b939d");
|
|
c.menuBackground = hex("#242730");
|
|
c.scrollHandle = hex("#3a3f47");
|
|
c.scrollHandleHover = hex("#4d535d");
|
|
c.sidebarBackground = hex("#1a1c20");
|
|
c.hoverBackground = hex("#272b31");
|
|
|
|
c.editorBase = hex("#1e2024");
|
|
c.editorText = hex("#d7dade");
|
|
c.gutterBackground = hex("#1a1c20");
|
|
c.gutterText = hex("#4f565f");
|
|
c.gutterActiveText = hex("#aeb5bf");
|
|
c.currentLine = hex("#23262c");
|
|
|
|
c.heading = hex("#79b8ff");
|
|
c.headingMarker = hex("#5d6570");
|
|
c.strong = hex("#f2f4f7");
|
|
c.emphasis = hex("#c3c9d1");
|
|
c.code = hex("#ff8fbf");
|
|
c.codeBackground = hex("#292c33");
|
|
c.link = hex("#62a8ff");
|
|
c.linkUrl = hex("#7f8791");
|
|
c.quote = hex("#8b939d");
|
|
c.quoteMarker = hex("#4f565f");
|
|
c.listMarker = hex("#62a8ff");
|
|
c.rule = hex("#33373e");
|
|
c.taskDone = hex("#5fbf7f");
|
|
return c;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ThemeManager *ThemeManager::instance()
|
|
{
|
|
static ThemeManager *manager = new ThemeManager();
|
|
return manager;
|
|
}
|
|
|
|
ThemeManager::ThemeManager(QObject *parent)
|
|
: QObject(parent)
|
|
{
|
|
if (qApp) {
|
|
const QColor window = qApp->palette().color(QPalette::Window);
|
|
m_paletteGuess = window.lightness() < 128 ? Scheme::Dark : Scheme::Light;
|
|
}
|
|
|
|
m_pollTimer = new QTimer(this);
|
|
m_pollTimer->setInterval(2000);
|
|
connect(m_pollTimer, &QTimer::timeout, this, &ThemeManager::pollSystemTheme);
|
|
}
|
|
|
|
void ThemeManager::initialize(Mode forcedMode, bool persist)
|
|
{
|
|
if (forcedMode != Mode::System) {
|
|
setMode(forcedMode, persist);
|
|
} else {
|
|
const QString saved = QSettings().value(QStringLiteral("theme/mode")).toString();
|
|
m_mode = modeFromString(saved, nullptr);
|
|
applyScheme(schemeForMode(m_mode), true);
|
|
}
|
|
m_pollTimer->start();
|
|
}
|
|
|
|
void ThemeManager::setMode(Mode mode, bool persist)
|
|
{
|
|
m_mode = mode;
|
|
if (persist)
|
|
QSettings().setValue(QStringLiteral("theme/mode"), modeToString(mode));
|
|
|
|
applyScheme(schemeForMode(m_mode), true);
|
|
}
|
|
|
|
void ThemeManager::pollSystemTheme()
|
|
{
|
|
if (m_mode == Mode::Light || m_mode == Mode::Dark)
|
|
return;
|
|
|
|
// 跟随系统:读桌面设置;按时段:到 6:00 / 18:00 自动翻转。
|
|
const Scheme target = schemeForMode(m_mode);
|
|
if (target != m_scheme)
|
|
applyScheme(target, false);
|
|
}
|
|
|
|
ThemeManager::Scheme ThemeManager::schemeForMode(Mode mode) const
|
|
{
|
|
switch (mode) {
|
|
case Mode::Light: return Scheme::Light;
|
|
case Mode::Dark: return Scheme::Dark;
|
|
case Mode::Schedule: return scheduleScheme();
|
|
case Mode::System: break;
|
|
}
|
|
return detectSystemScheme();
|
|
}
|
|
|
|
ThemeManager::Scheme ThemeManager::scheduleScheme() const
|
|
{
|
|
const int hour = QTime::currentTime().hour();
|
|
return (hour >= 6 && hour < 18) ? Scheme::Light : Scheme::Dark;
|
|
}
|
|
|
|
void ThemeManager::applyScheme(Scheme scheme, bool force)
|
|
{
|
|
const bool changed = (scheme != m_scheme) || force;
|
|
m_scheme = scheme;
|
|
buildColors(m_scheme);
|
|
applyToApplication();
|
|
if (changed)
|
|
emit schemeChanged(m_scheme);
|
|
}
|
|
|
|
void ThemeManager::buildColors(Scheme scheme)
|
|
{
|
|
m_colors = (scheme == Scheme::Dark) ? darkColors() : lightColors();
|
|
}
|
|
|
|
void ThemeManager::applyToApplication()
|
|
{
|
|
QPalette palette;
|
|
palette.setColor(QPalette::Window, m_colors.window);
|
|
palette.setColor(QPalette::WindowText, m_colors.windowText);
|
|
palette.setColor(QPalette::Base, m_colors.base);
|
|
palette.setColor(QPalette::AlternateBase, m_colors.alternateBase);
|
|
palette.setColor(QPalette::Text, m_colors.text);
|
|
palette.setColor(QPalette::Button, m_colors.button);
|
|
palette.setColor(QPalette::ButtonText, m_colors.buttonText);
|
|
palette.setColor(QPalette::BrightText, Qt::white);
|
|
palette.setColor(QPalette::Highlight, m_colors.highlight);
|
|
palette.setColor(QPalette::HighlightedText, m_colors.highlightedText);
|
|
palette.setColor(QPalette::ToolTipBase, m_colors.menuBackground);
|
|
palette.setColor(QPalette::ToolTipText, m_colors.text);
|
|
palette.setColor(QPalette::PlaceholderText, m_colors.placeholder);
|
|
palette.setColor(QPalette::Link, m_colors.link);
|
|
palette.setColor(QPalette::LinkVisited, m_colors.link);
|
|
palette.setColor(QPalette::Disabled, QPalette::WindowText, m_colors.disabled);
|
|
palette.setColor(QPalette::Disabled, QPalette::Text, m_colors.disabled);
|
|
palette.setColor(QPalette::Disabled, QPalette::ButtonText, m_colors.disabled);
|
|
palette.setColor(QPalette::Disabled, QPalette::HighlightedText, m_colors.disabled);
|
|
|
|
qApp->setPalette(palette);
|
|
qApp->setStyleSheet(styleSheet());
|
|
}
|
|
|
|
QString ThemeManager::styleSheet() const
|
|
{
|
|
QString qss = QString::fromLatin1(R"(
|
|
QMainWindow, QDialog { background-color: @window; }
|
|
QMenuBar { background-color: @window; color: @windowText; border-bottom: 1px solid @border; }
|
|
QMenuBar::item { background: transparent; padding: 5px 10px; border-radius: 5px; }
|
|
QMenuBar::item:selected { background-color: @highlight; color: @highlightedText; }
|
|
QMenu { background-color: @menuBackground; color: @text; border: 1px solid @border; padding: 5px; }
|
|
QMenu::item { padding: 6px 28px 6px 22px; border-radius: 5px; }
|
|
QMenu::item:selected { background-color: @highlight; color: @highlightedText; }
|
|
QMenu::item:disabled { color: @disabled; }
|
|
QMenu::separator { height: 1px; background-color: @border; margin: 5px 8px; }
|
|
QStatusBar { background-color: @window; color: @placeholder; border-top: 1px solid @border; }
|
|
QStatusBar::item { border: none; }
|
|
QPlainTextEdit { background-color: @editorBase; color: @editorText; border: none;
|
|
selection-background-color: @highlight; selection-color: @highlightedText; }
|
|
QScrollBar:vertical { background: transparent; width: 11px; margin: 0; }
|
|
QScrollBar::handle:vertical { background-color: @scrollHandle; border-radius: 4px;
|
|
min-height: 28px; margin: 2px 2px; }
|
|
QScrollBar::handle:vertical:hover { background-color: @scrollHandleHover; }
|
|
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }
|
|
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: transparent; }
|
|
QScrollBar:horizontal { background: transparent; height: 11px; margin: 0; }
|
|
QScrollBar::handle:horizontal { background-color: @scrollHandle; border-radius: 4px;
|
|
min-width: 28px; margin: 2px 2px; }
|
|
QScrollBar::handle:horizontal:hover { background-color: @scrollHandleHover; }
|
|
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; }
|
|
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: transparent; }
|
|
QPushButton { background-color: @button; color: @buttonText; border: 1px solid @border;
|
|
border-radius: 6px; padding: 5px 16px; min-width: 68px; }
|
|
QPushButton:hover { background-color: @highlight; color: @highlightedText; }
|
|
QPushButton:default { background-color: @accent; color: #ffffff; border-color: @accent; }
|
|
QToolTip { background-color: @menuBackground; color: @text; border: 1px solid @border; padding: 4px; }
|
|
QToolButton { border: none; border-radius: 5px; padding: 3px 8px; color: @text; background: transparent; }
|
|
QToolButton:hover { background-color: @hoverBackground; }
|
|
QToolButton:checked { background-color: @highlight; color: @highlightedText; }
|
|
QDockWidget { color: @text; }
|
|
QDockWidget::title { background-color: @window; padding: 6px 8px; }
|
|
QListWidget { background-color: @sidebarBackground; border: 1px solid @border; border-radius: 6px;
|
|
padding: 3px; outline: 0; }
|
|
QListWidget::item { padding: 4px 6px; border-radius: 4px; }
|
|
QListWidget::item:hover { background-color: @hoverBackground; }
|
|
QListWidget::item:selected { background-color: @highlight; color: @highlightedText; }
|
|
)");
|
|
|
|
// 先替换长 token,避免 @window 吃掉 @windowText 这类前缀冲突。
|
|
qss.replace(QLatin1String("@scrollHandleHover"), m_colors.scrollHandleHover.name());
|
|
qss.replace(QLatin1String("@highlightedText"), m_colors.highlightedText.name());
|
|
qss.replace(QLatin1String("@buttonText"), m_colors.buttonText.name());
|
|
qss.replace(QLatin1String("@windowText"), m_colors.windowText.name());
|
|
qss.replace(QLatin1String("@editorText"), m_colors.editorText.name());
|
|
qss.replace(QLatin1String("@menuBackground"), m_colors.menuBackground.name());
|
|
qss.replace(QLatin1String("@scrollHandle"), m_colors.scrollHandle.name());
|
|
qss.replace(QLatin1String("@editorBase"), m_colors.editorBase.name());
|
|
qss.replace(QLatin1String("@placeholder"), m_colors.placeholder.name());
|
|
qss.replace(QLatin1String("@disabled"), m_colors.disabled.name());
|
|
qss.replace(QLatin1String("@border"), m_colors.border.name());
|
|
qss.replace(QLatin1String("@hoverBackground"), m_colors.hoverBackground.name());
|
|
qss.replace(QLatin1String("@sidebarBackground"), m_colors.sidebarBackground.name());
|
|
qss.replace(QLatin1String("@accent"), m_colors.accent.name());
|
|
qss.replace(QLatin1String("@highlight"), m_colors.highlight.name());
|
|
qss.replace(QLatin1String("@button"), m_colors.button.name());
|
|
qss.replace(QLatin1String("@window"), m_colors.window.name());
|
|
qss.replace(QLatin1String("@text"), m_colors.text.name());
|
|
return qss;
|
|
}
|
|
|
|
QString ThemeManager::modeToString(Mode mode)
|
|
{
|
|
switch (mode) {
|
|
case Mode::Light: return QStringLiteral("light");
|
|
case Mode::Dark: return QStringLiteral("dark");
|
|
case Mode::Schedule: return QStringLiteral("schedule");
|
|
case Mode::System: break;
|
|
}
|
|
return QStringLiteral("system");
|
|
}
|
|
|
|
ThemeManager::Mode ThemeManager::modeFromString(const QString &value, bool *ok)
|
|
{
|
|
const QString normalized = value.trimmed().toLower();
|
|
if (normalized == QLatin1String("light")) { if (ok) *ok = true; return Mode::Light; }
|
|
if (normalized == QLatin1String("dark")) { if (ok) *ok = true; return Mode::Dark; }
|
|
if (normalized == QLatin1String("schedule")) { if (ok) *ok = true; return Mode::Schedule; }
|
|
if (ok) *ok = (normalized == QLatin1String("system") || normalized.isEmpty());
|
|
return Mode::System;
|
|
}
|
|
|
|
QString ThemeManager::modeLabel(Mode mode)
|
|
{
|
|
switch (mode) {
|
|
case Mode::Light: return QObject::tr("浅色");
|
|
case Mode::Dark: return QObject::tr("深色");
|
|
case Mode::Schedule: return QObject::tr("按时段自动");
|
|
case Mode::System: break;
|
|
}
|
|
return QObject::tr("跟随系统");
|
|
}
|
|
|
|
ThemeManager::Scheme ThemeManager::detectSystemScheme() const
|
|
{
|
|
// 1) 显式覆盖:打包、测试或用户强制指定时使用。
|
|
const QString override = qEnvironmentVariable("VOILETMARK_THEME").trimmed().toLower();
|
|
if (override == QLatin1String("dark"))
|
|
return Scheme::Dark;
|
|
if (override == QLatin1String("light"))
|
|
return Scheme::Light;
|
|
|
|
#if defined(Q_OS_WIN)
|
|
QSettings registry(
|
|
QStringLiteral("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"),
|
|
QSettings::NativeFormat);
|
|
const QVariant appsUseLight = registry.value(QStringLiteral("AppsUseLightTheme"));
|
|
if (appsUseLight.isValid())
|
|
return appsUseLight.toInt() == 0 ? Scheme::Dark : Scheme::Light;
|
|
return m_paletteGuess;
|
|
#elif defined(Q_OS_MACOS)
|
|
const QString style = runCommand(QStringLiteral("defaults"),
|
|
{QStringLiteral("read"), QStringLiteral("-g"),
|
|
QStringLiteral("AppleInterfaceStyle")},
|
|
500);
|
|
return style.compare(QLatin1String("Dark"), Qt::CaseInsensitive) == 0 ? Scheme::Dark : Scheme::Light;
|
|
#else
|
|
// 2) gsettings 需要启动子进程,做 3 秒缓存;找不到命令就整段跳过。
|
|
static const QString gsettings = QStandardPaths::findExecutable(QStringLiteral("gsettings"));
|
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
|
if (!gsettings.isEmpty()
|
|
&& (now - m_lastGsettingsCheck > 3000 || m_cachedColorScheme.isEmpty())) {
|
|
m_cachedColorScheme = runCommand(gsettings,
|
|
{QStringLiteral("get"),
|
|
QStringLiteral("org.gnome.desktop.interface"),
|
|
QStringLiteral("color-scheme")},
|
|
500);
|
|
m_cachedGtkTheme = runCommand(gsettings,
|
|
{QStringLiteral("get"),
|
|
QStringLiteral("org.gnome.desktop.interface"),
|
|
QStringLiteral("gtk-theme")},
|
|
500);
|
|
m_lastGsettingsCheck = now;
|
|
}
|
|
|
|
const QString colorScheme = m_cachedColorScheme.toLower();
|
|
if (colorScheme.contains(QLatin1String("prefer-dark")))
|
|
return Scheme::Dark;
|
|
if (colorScheme.contains(QLatin1String("prefer-light")))
|
|
return Scheme::Light;
|
|
|
|
// 3) KDE 全局配色。
|
|
QSettings kdeGlobals(QDir::homePath() + QStringLiteral("/.config/kdeglobals"),
|
|
QSettings::IniFormat);
|
|
const QString kdeScheme = kdeGlobals.value(QStringLiteral("General/ColorScheme")).toString();
|
|
if (!kdeScheme.isEmpty())
|
|
return looksDark(kdeScheme) ? Scheme::Dark : Scheme::Light;
|
|
|
|
// 4) GTK 主题名。
|
|
if (!m_cachedGtkTheme.isEmpty())
|
|
return looksDark(m_cachedGtkTheme) ? Scheme::Dark : Scheme::Light;
|
|
|
|
// 5) XFCE 主题名。
|
|
const QString xfceName = xfceThemeName();
|
|
if (!xfceName.isEmpty())
|
|
return looksDark(xfceName) ? Scheme::Dark : Scheme::Light;
|
|
|
|
// 6) 兜底。
|
|
return m_paletteGuess;
|
|
#endif
|
|
}
|