geopro/src/app/Theme.cpp

804 lines
26 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "Theme.hpp"
#include "Glyphs.hpp"
#include <QApplication>
#include <QColor>
#include <QFont>
#include <QObject>
#include <QPalette>
#include <QProxyStyle>
#include <QSettings>
#include <QStyleFactory>
#include <QStyleHints>
#include <QWidget>
namespace geopro::app {
namespace {
// 应用样式:在 Fusion 基础上把下拉框弹窗改为「列表紧贴文本框下方」(而非 Fusion 默认的
// 菜单式弹窗覆盖当前项——那会导致弹窗位置怪、容器+列表两层、选中不清)。
class AppProxyStyle : public QProxyStyle {
public:
AppProxyStyle() : QProxyStyle(QStyleFactory::create(QStringLiteral("Fusion"))) {}
int styleHint(StyleHint hint, const QStyleOption* opt, const QWidget* w,
QStyleHintReturn* ret) const override
{
if (hint == QStyle::SH_ComboBox_Popup) return 0;
return QProxyStyle::styleHint(hint, opt, w, ret);
}
};
// ── 语义令牌表(全 UI 唯一颜色来源)。改色只改这一处。 ──────────────────
// 取值来源:规范 §1.5 语义映射 + 附录 A 速查 + §1.3 画布专用色。
// 画布(canvas/*)与 bg/canvas 两模式同值——规范 §0.5「视图区永远深色」。
struct Token { const char* name; const char* light; const char* dark; };
const Token kTokens[] = {
// 背景
{"bg/app", "#F7F8FA", "#0E1116"},
{"bg/panel", "#FFFFFF", "#161A20"},
{"bg/panel-subtle", "#FCFCFD", "#161B22"},
{"bg/header", "#FFFFFF", "#12161C"},
{"bg/hover", "#EFF1F4", "#1B2129"},
{"bg/selected", "#EFF5FF", "#16243F"},
{"bg/canvas", "#0B1320", "#0B1320"},
// 边框
{"border/default", "#E3E6EB", "#262C35"},
{"border/strong", "#CDD2DA", "#333B45"},
{"border/focus", "#3B73EC", "#5E8DF5"},
// 文字
{"text/primary", "#272C35", "#E6E9EF"},
{"text/secondary", "#5A626F", "#A4ADBB"},
{"text/tertiary", "#7C8493", "#7A8494"},
{"text/disabled", "#A8AFBC", "#5A626F"},
{"text/link", "#3B73EC", "#5E8DF5"},
{"text/on-primary", "#FFFFFF", "#FFFFFF"},
// 强调
{"accent/primary", "#3B73EC", "#5E8DF5"},
{"accent/primary-hover", "#2B5FD9", "#93B4FA"},
{"accent/primary-pressed","#2450B8", "#3B73EC"},
// 其他
{"divider", "#E3E6EB", "#22272F"},
{"scrollbar/thumb", "#CDD2DA", "#3A424D"},
{"scrollbar/thumb-hover", "#A8AFBC", "#4A535F"},
// 状态色(主色 + 浅底)规范 §1.4
{"status/danger", "#E5484D", "#FF6166"},
{"status/danger-bg", "#FDECEC", "#3A1D1F"},
{"status/warning", "#E08A1E", "#F5A623"},
{"status/warning-bg", "#FBF0DD", "#3A2C12"},
{"status/success", "#2E9E5B", "#46C07A"},
{"status/success-bg", "#E7F6ED", "#16301F"},
{"status/info", "#3B73EC", "#5E8DF5"},
{"status/info-bg", "#EFF5FF", "#16243F"},
{"status/neutral", "#7C8493", "#8A93A3"},
// 画布专用(两模式同值)规范 §1.3
{"canvas/bg", "#0B1320", "#0B1320"},
{"canvas/bg-soft", "#111B2D", "#111B2D"},
{"canvas/grid", "#1E2A3D", "#1E2A3D"},
{"canvas/text", "#E6ECF5", "#E6ECF5"},
{"canvas/text-dim", "#8A97AC", "#8A97AC"},
};
QString tokenHex(const char* name, bool dark)
{
for (const auto& t : kTokens)
if (qstrcmp(t.name, name) == 0) return QString::fromLatin1(dark ? t.dark : t.light);
return QStringLiteral("#FF00FF"); // 漏配的令牌显眼品红,便于一眼发现
}
// 全局样式表(浅色专业)。只描述外观,不含任何交互逻辑。
// 注意:刻意不重写 QCheckBox::indicator —— Fusion 一旦检测到 indicator 子控件被改写,
// 就需要自带勾选 image否则勾选态会变成空白方块。这里交给 Fusion 原生绘制,
// 它会自动采用调色板的 Highlight(accent/primary) 作勾选色,省去打包图片资源。
const char* kStyleSheet = R"QSS(
/* ── 基础 ───────────────────────────────────────────────── */
QWidget {
color: {{text/primary}};
}
QMainWindow, QDialog {
background: {{bg/app}};
}
/* QToolTip 不写 QSS用系统原生工具提示自定义 QSS 会让弹窗圆角露直角、且不像原生)。 */
/* ── 视图内工具条2D/3D、数据详情白底分段控件柔和不刺眼 ── */
QToolBar {
background: {{bg/panel}};
border: none;
border-bottom: 1px solid {{divider}};
padding: 6px 8px;
spacing: 4px;
}
QToolBar QToolButton {
background: transparent;
color: {{text/secondary}};
border: none;
border-radius: 8px;
padding: 6px 14px;
font-weight: 500;
}
QToolBar QToolButton:hover {
background: {{bg/hover}};
color: {{text/primary}};
}
QToolBar QToolButton:pressed {
background: {{bg/selected}};
}
QToolBar QToolButton:checked {
background: {{bg/hover}};
color: {{accent/primary}};
font-weight: 600;
}
QToolBar QToolButton:checked:hover {
background: {{bg/selected}};
}
QToolBar::separator {
background: {{divider}};
width: 1px;
margin: 6px 8px;
}
/* ── 树 / 列表:无边框(靠面板与留白分隔,去掉线框感)+ 充足行距 ── */
QTreeWidget, QListWidget, QTreeView, QListView {
background: {{bg/panel}};
border: none;
padding: 6px;
outline: none;
}
QTreeWidget::item, QListWidget::item, QTreeView::item, QListView::item {
padding: 4px 8px;
}
QTreeWidget::item:hover, QListWidget::item:hover,
QTreeView::item:hover, QListView::item:hover {
background: {{bg/hover}};
}
QTreeWidget::item:selected, QListWidget::item:selected,
QTreeView::item:selected, QListView::item:selected {
background: {{bg/selected}};
color: {{text/primary}};
}
QTreeWidget::item:selected:!active, QListWidget::item:selected:!active,
QTreeView::item:selected:!active, QListView::item:selected:!active {
background: {{bg/selected}};
color: {{text/primary}};
}
/* 注意:不要给 QTreeView::branch 设 background——一旦改写 branchQt 会停止绘制
默认的展开/折叠箭头(与 indicator 同类陷阱),父节点折叠图标会消失。 */
/* 表头(对象显示栏) */
QHeaderView::section {
background: {{bg/hover}};
color: {{text/secondary}};
border: none;
border-bottom: 1px solid {{border/default}};
padding: 6px 8px;
font-weight: 600;
}
/* ── 标签页(数据 / 文件):现代下划线 tab无边框盒子 ──────── */
QTabWidget::pane {
border: none;
border-top: 1px solid {{divider}};
top: 0;
background: {{bg/panel}};
}
QTabBar {
background: transparent;
}
QTabBar::tab {
background: transparent;
color: {{text/secondary}};
border: none;
border-bottom: 2px solid transparent;
padding: 8px 16px;
margin-right: 4px;
}
QTabBar::tab:selected {
color: {{accent/primary}};
border-bottom: 2px solid {{accent/primary}};
font-weight: 600;
}
QTabBar::tab:hover:!selected {
color: {{text/primary}};
}
/* ── 复选框(仅调间距/字色indicator 交给 Fusion 原生)──── */
QCheckBox {
spacing: 7px;
color: {{text/primary}};
}
QCheckBox:disabled {
color: {{text/disabled}};
}
/* ── 通用按钮 / 输入(登录窗内部各自再覆盖)────────────────── */
QPushButton {
background: {{bg/panel}};
color: {{text/primary}};
border: 1px solid {{border/strong}};
border-radius: 4px; /* radius/sm */
padding: 6px 14px;
}
QPushButton:hover {
background: {{bg/hover}};
border-color: {{accent/primary}};
}
QPushButton:pressed {
background: {{bg/selected}};
}
QPushButton:default {
background: {{accent/primary}};
color: {{text/on-primary}};
border-color: {{accent/primary}};
}
QPushButton:default:hover {
background: {{accent/primary-hover}};
}
QPushButton:disabled {
background: {{bg/app}};
color: {{text/disabled}};
border-color: {{border/default}};
}
/* 输入框(规范 §7.1):默认 border/default、hover border/strong、focus border/focus。
高 ~28px = 字号 13px + 上下 padding 6px + 边框 1px×2 ≈ min-height 16px。
注意Qt QSS 不支持 box-shadow规范的 focus「外发光」无法实现仅用 border/focus 近似。 */
QLineEdit {
background: {{bg/panel}};
color: {{text/primary}};
border: 1px solid {{border/default}};
border-radius: 4px; /* radius/sm */
padding: 6px 8px;
min-height: 16px;
selection-background-color: {{accent/primary}};
selection-color: {{text/on-primary}};
}
QLineEdit:hover {
border-color: {{border/strong}};
}
QLineEdit:focus {
border: 1px solid {{border/focus}};
}
QLineEdit:disabled {
background: {{bg/app}};
color: {{text/disabled}};
}
/* 多行文本(备注/描述):与输入框同款 box边框/底色/圆角/内距),仅不设 min-height
(高度由控件自定)。避免多行框沿用 Fusion 默认边框、与单行输入观感不一。 */
QPlainTextEdit, QTextEdit {
background: {{bg/panel}};
color: {{text/primary}};
border: 1px solid {{border/default}};
border-radius: 4px; /* radius/sm */
padding: 4px 8px;
selection-background-color: {{accent/primary}};
selection-color: {{text/on-primary}};
}
QPlainTextEdit:hover, QTextEdit:hover {
border-color: {{border/strong}};
}
QPlainTextEdit:focus, QTextEdit:focus {
border: 1px solid {{border/focus}};
}
QPlainTextEdit:disabled, QTextEdit:disabled {
background: {{bg/app}};
color: {{text/disabled}};
}
/* ── 滚动条:纤细现代(无需图片资源)───────────────────────── */
QScrollBar:vertical {
background: transparent;
width: 12px;
margin: 2px;
}
QScrollBar::handle:vertical {
background: {{scrollbar/thumb}};
border-radius: 6px;
min-height: 28px;
}
QScrollBar::handle:vertical:hover {
background: {{scrollbar/thumb-hover}};
}
QScrollBar:horizontal {
background: transparent;
height: 12px;
margin: 2px;
}
QScrollBar::handle:horizontal {
background: {{scrollbar/thumb}};
border-radius: 6px;
min-width: 28px;
}
QScrollBar::handle:horizontal:hover {
background: {{scrollbar/thumb-hover}};
}
QScrollBar::add-line, QScrollBar::sub-line {
width: 0;
height: 0;
}
QScrollBar::add-page, QScrollBar::sub-page {
background: transparent;
}
/* ── 分隔条:默认近乎隐形,悬停时才显淡色(去掉灰硬条)──────── */
QSplitter::handle {
background: {{divider}};
}
QSplitter::handle:hover {
background: {{accent/primary}};
}
ads--CDockSplitter::handle {
background: {{divider}};
}
ads--CDockSplitter::handle:hover {
background: {{accent/primary}};
}
/* ── 状态栏:底部信息条(坐标系 / 状态指示,常驻可见)──────── */
QStatusBar {
background: {{bg/panel}};
color: {{text/secondary}};
border-top: 1px solid {{divider}};
}
QStatusBar::item {
border: none;
}
QStatusBar QLabel {
color: {{text/secondary}};
padding: 0 4px;
}
/* ── 菜单栏 / 菜单(标准 QMenuBar/QMenu刻意不设 border-radius——弹窗圆角靠系统(Win11
原生圆角)QSS 设圆角会露出后面的直角。仅设底/字/选中,干净不刺眼。 */
QMenuBar {
background: {{bg/panel}};
color: {{text/primary}};
border-bottom: 1px solid {{divider}};
padding: 2px 6px;
}
QMenuBar::item {
background: transparent;
padding: 6px 12px;
border-radius: 4px; /* radius/sm */
}
QMenuBar::item:selected {
background: {{bg/hover}};
color: {{accent/primary}};
}
QMenuBar::item:pressed {
background: {{bg/selected}};
}
QMenu {
background: {{bg/panel}};
color: {{text/primary}};
border: 1px solid {{border/default}};
border-radius: 6px; /* radius/md浮层容器 */
padding: 4px;
}
QMenu::item {
padding: 6px 24px 6px 14px;
border-radius: 4px; /* radius/sm */
}
QMenu::item:selected {
background: {{bg/hover}};
color: {{accent/primary}};
}
QMenu::separator {
height: 1px;
background: {{divider}};
margin: 4px 8px;
}
/* ── 下拉框(按需出现时也与主题一致)──────────────────────── */
/* 下拉框(规范 §7.2):外观对齐输入框 —— 默认 border/default、hover border/strong、
focus border/focus、radius/sm。弹窗用 radius/md浮层项 hover bg/hover。 */
QComboBox {
background: {{bg/panel}};
color: {{text/primary}};
border: 1px solid {{border/default}};
border-radius: 4px; /* radius/sm */
padding: 6px 8px; /* 与 QLineEdit 完全一致 */
min-height: 16px; /* 与 QLineEdit 完全一致 → 同高(可编辑/不可编辑均如此)*/
}
QComboBox:hover {
border-color: {{border/strong}};
}
QComboBox:focus {
border-color: {{border/focus}};
}
QComboBox:disabled {
background: {{bg/app}};
color: {{text/disabled}};
}
/* 数字框/日期/时间编辑器:与输入框/下拉框同款外观。QSpinBox/QDoubleSpinBox/QDateEdit/
QTimeEdit/QDateTimeEdit 均派生自 QAbstractSpinBox一处统一其 box高度/边框/圆角/内距),
避免数字框比下拉框/输入框矮(历史坑:只给了 QLineEdit/QComboBox/Date 没给 SpinBox。 */
QAbstractSpinBox {
background: {{bg/panel}};
color: {{text/primary}};
border: 1px solid {{border/default}};
border-radius: 4px; /* radius/sm */
padding: 6px 8px;
min-height: 16px; /* 与 QLineEdit/QComboBox 完全一致 → 同高 */
}
QAbstractSpinBox:hover {
border-color: {{border/strong}};
}
QAbstractSpinBox:focus {
border-color: {{border/focus}};
}
QAbstractSpinBox:disabled {
background: {{bg/app}};
color: {{text/disabled}};
}
/* 下拉按钮平面化(去 Fusion 原生斜角/分隔)+ 统一的扁平 chevron 箭头qrc 内嵌 SVG
覆写 ::drop-down 必须同时提供 ::down-arrow 图,否则箭头消失(历史坑)。 */
QComboBox::drop-down, QDateEdit::drop-down, QTimeEdit::drop-down, QDateTimeEdit::drop-down {
subcontrol-origin: padding;
subcontrol-position: center right;
width: 20px;
border: none;
background: transparent;
}
QComboBox::down-arrow, QDateEdit::down-arrow, QTimeEdit::down-arrow, QDateTimeEdit::down-arrow {
image: url(:/icons/chevron-down.svg);
width: 12px;
height: 12px;
}
/* 数字框上下按钮:平面化 + 扁平 chevron与下拉箭头同族。覆写 up/down-button 须同时给
up/down-arrow 图,否则 Fusion 原生箭头消失。日期/时间用日历下拉(::drop-down不在此列。 */
QSpinBox::up-button, QDoubleSpinBox::up-button {
subcontrol-origin: border;
subcontrol-position: top right;
width: 18px;
border: none;
border-left: 1px solid {{border/default}};
background: transparent;
}
QSpinBox::down-button, QDoubleSpinBox::down-button {
subcontrol-origin: border;
subcontrol-position: bottom right;
width: 18px;
border: none;
border-left: 1px solid {{border/default}};
background: transparent;
}
QSpinBox::up-button:hover, QDoubleSpinBox::up-button:hover,
QSpinBox::down-button:hover, QDoubleSpinBox::down-button:hover {
background: {{bg/hover}};
}
QSpinBox::up-arrow, QDoubleSpinBox::up-arrow {
image: url(:/icons/chevron-up.svg);
width: 10px;
height: 10px;
}
QSpinBox::down-arrow, QDoubleSpinBox::down-arrow {
image: url(:/icons/chevron-down.svg);
width: 10px;
height: 10px;
}
QComboBox QAbstractItemView {
background: {{bg/panel}};
border: 1px solid {{border/default}};
border-radius: 6px; /* radius/md浮层容器 */
outline: none;
padding: 2px;
}
QComboBox QAbstractItemView::item {
border: none;
padding: 6px 10px;
min-height: 20px;
color: {{text/primary}};
}
QComboBox QAbstractItemView::item:selected {
background: {{bg/hover}};
color: {{accent/primary}};
}
/* ── 分组框(按需出现时也与主题一致)──────────────────────── */
QGroupBox {
border: 1px solid {{border/default}};
border-radius: 8px;
margin-top: 10px;
padding-top: 6px;
font-weight: 600;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px;
padding: 0 4px;
color: {{text/secondary}};
}
/* ── 进度条(长任务反馈,遵循 Doherty 阈值)──────────────────── */
QProgressBar {
background: {{divider}};
border: none;
border-radius: 6px;
height: 8px;
text-align: center;
color: {{text/secondary}};
}
QProgressBar::chunk {
background: {{accent/primary}};
border-radius: 6px;
}
/* ── 滑块(规范 §6.13如简化容差4px 轨道 + 已填充段强调色 + 14px 白圆手柄 ──
handle margin -5px 使 14px 手柄在 4px 轨道上垂直居中。仅横向app 用横向滑块)。 */
QSlider::groove:horizontal {
height: 4px;
background: {{border/default}};
border-radius: 2px;
}
QSlider::sub-page:horizontal {
background: {{accent/primary}};
border-radius: 2px;
}
QSlider::add-page:horizontal {
background: {{border/default}};
border-radius: 2px;
}
QSlider::handle:horizontal {
width: 14px;
height: 14px;
margin: -5px 0;
border-radius: 7px;
background: {{bg/panel}};
border: 1px solid {{border/strong}};
}
QSlider::handle:horizontal:hover {
border-color: {{accent/primary}};
}
/* 对话框容器圆角/投影(规范 §7.5 radius/lg + shadow/dialogQt 顶层原生窗口忽略
QSS 的 border-radius且 QSS 无法绘制窗口外阴影——此处刻意不做,留待原生/QGraphicsEffect。 */
/* ── ADS 停靠:标题栏做成「固定面板表头」(对齐原型)──────────────
面板已锁定(无关闭/浮动/拖动),每个区只一个标签即表头:统一浅色头 +
蓝色下划线强调 + 深色加粗标题。各区一致,读起来像固定子窗口表头。 */
ads--CDockAreaWidget {
background: {{bg/app}};
}
ads--CDockAreaTitleBar {
background: {{bg/hover}};
border-bottom: 1px solid {{border/default}};
padding: 0;
}
ads--CDockWidgetTab {
background: {{bg/hover}};
border: none;
border-bottom: 2px solid transparent;
padding: 7px 12px;
min-height: 22px;
}
ads--CDockWidgetTab[activeTab="true"] {
background: {{bg/hover}};
border-bottom: 2px solid {{accent/primary}};
}
ads--CDockWidgetTab QLabel {
color: {{text/secondary}};
font-weight: 600;
}
ads--CDockWidgetTab[activeTab="true"] QLabel {
color: {{text/primary}};
font-weight: 600;
}
)QSS";
// 全局复选指示器:用 writeCheckboxIcon 生成清晰复选框 PNG未选=明显边框空心框,
// 选中=强调色填充+白勾),统一作用于 QCheckBox 与 树/列表的勾选指示器。规避 Fusion
// 原生复选框在浅底下边框过淡看不清的问题——全 UI 一套,避免逐控件打补丁。
QString indicatorQss(bool dark)
{
const QColor border = QColor(tokenHex("border/strong", dark));
const QColor boxBg = QColor(tokenHex("bg/panel", dark));
const QColor accent = QColor(tokenHex("accent/primary", dark));
const QString tag = dark ? QStringLiteral("gd") : QStringLiteral("gl"); // 全局缓存标签
const QString off = geopro::app::writeCheckboxIcon(false, border, boxBg, Qt::white, tag);
const QString on = geopro::app::writeCheckboxIcon(true, accent, accent, Qt::white, tag);
return QStringLiteral(
"QCheckBox::indicator, QTreeView::indicator, QListView::indicator,"
"QTreeWidget::indicator, QListWidget::indicator { width:16px; height:16px; }"
"QCheckBox::indicator:unchecked, QTreeView::indicator:unchecked,"
"QListView::indicator:unchecked, QTreeWidget::indicator:unchecked,"
"QListWidget::indicator:unchecked { image:url(%1); }"
"QCheckBox::indicator:checked, QTreeView::indicator:checked,"
"QListView::indicator:checked, QTreeWidget::indicator:checked,"
"QListWidget::indicator:checked { image:url(%2); }")
.arg(off, on);
}
// 当前模式的全局 QSS。
QString styleSheetForMode(bool /*dark*/)
{
const bool dark = isDarkTheme();
return fillTokens(QString::fromUtf8(kStyleSheet)) + indicatorQss(dark);
}
// 调色板同样取自 ElaTheme让无 QSS 覆盖处的标准控件也与外壳一致。
QPalette buildPalette(bool dark)
{
QPalette p;
const QColor shell = QColor(tokenHex("bg/app", dark));
const QColor panel = QColor(tokenHex("bg/panel", dark));
const QColor text = QColor(tokenHex("text/primary", dark));
const QColor muted = QColor(tokenHex("text/secondary", dark));
const QColor accent = QColor(tokenHex("accent/primary", dark));
const QColor border = QColor(tokenHex("border/default", dark));
const QColor disabled = QColor(tokenHex("text/disabled", dark));
const QColor hoverBg = QColor(tokenHex("bg/hover", dark));
p.setColor(QPalette::Window, shell);
p.setColor(QPalette::WindowText, text);
p.setColor(QPalette::Base, panel);
p.setColor(QPalette::AlternateBase, QColor(tokenHex("bg/panel-subtle", dark)));
p.setColor(QPalette::Text, text);
p.setColor(QPalette::Button, hoverBg);
p.setColor(QPalette::ButtonText, text);
p.setColor(QPalette::ToolTipBase, text);
p.setColor(QPalette::ToolTipText, panel);
p.setColor(QPalette::Highlight, accent);
p.setColor(QPalette::HighlightedText, QColor(tokenHex("text/on-primary", dark)));
p.setColor(QPalette::PlaceholderText, muted);
p.setColor(QPalette::Link, accent);
// Fusion 的明暗 3D 角色统一压成边框色,立体效果塌成平面(去斜角/凹槽)。
p.setColor(QPalette::Light, panel);
p.setColor(QPalette::Midlight, border);
p.setColor(QPalette::Mid, border);
p.setColor(QPalette::Dark, border);
p.setColor(QPalette::Shadow, border);
p.setColor(QPalette::Disabled, QPalette::Text, disabled);
p.setColor(QPalette::Disabled, QPalette::WindowText, disabled);
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled);
return p;
}
} // namespace
void applyThemeMode(QApplication& app, bool dark)
{
// Fusion + 下拉框弹窗修正AppProxyStyle跨平台一致、对 QSS 友好。
app.setStyle(new AppProxyStyle());
// 基础字体:微软雅黑 UI基准字号取令牌 type::kBody(13px),与 QSS 同单位。
QFont base(QStringLiteral("Microsoft YaHei UI"));
base.setPixelSize(scaledPx(type::kBody)); // 随界面字号缩放
base.setStyleStrategy(QFont::PreferAntialias);
app.setFont(base);
app.setPalette(buildPalette(dark));
app.setStyleSheet(styleSheetForMode(dark));
}
void applyTheme(QApplication& app)
{
applyThemeMode(app, false);
}
// ── 主题管理器(替代 ElaTheme+ 设置:主题 / 字号 偏好 ──────────────────
namespace {
constexpr int kBaseFontPx = 13; // 基准字号
int g_fontScale = 100; // 当前字号缩放百分比
} // namespace
ThemeManager& ThemeManager::instance()
{
static ThemeManager inst;
return inst;
}
ThemeManager::ThemeManager(QObject* parent) : QObject(parent)
{
applyPersisted();
// 跟随系统时,系统明暗变化即同步并发 changed。
QObject::connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this,
[this](Qt::ColorScheme) {
if (!follow_) return;
const bool d = qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark;
if (d != dark_) {
dark_ = d;
emit changed();
}
});
}
void ThemeManager::applyPersisted()
{
const QString m =
QSettings().value(QStringLiteral("ui/themeMode"), QStringLiteral("system")).toString();
if (m == QStringLiteral("light")) {
follow_ = false;
dark_ = false;
} else if (m == QStringLiteral("dark")) {
follow_ = false;
dark_ = true;
} else { // system
follow_ = true;
dark_ = qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark;
}
}
void ThemeManager::setMode(const QString& mode)
{
QSettings().setValue(QStringLiteral("ui/themeMode"), mode);
applyPersisted();
emit changed(); // 热切:全 UI 重着色
}
QString themeModePreference()
{
return QSettings().value(QStringLiteral("ui/themeMode"), QStringLiteral("system")).toString();
}
void applyPersistedThemeMode()
{
ThemeManager::instance().applyPersisted();
}
void setThemeModePreference(const QString& mode)
{
ThemeManager::instance().setMode(mode); // 持久化 + 热切
}
int fontScalePreference()
{
return QSettings().value(QStringLiteral("ui/fontScale"), 100).toInt();
}
void setFontScalePreference(int percent)
{
QSettings().setValue(QStringLiteral("ui/fontScale"), percent); // 重启后生效
}
void applyPersistedFontScale()
{
g_fontScale = fontScalePreference();
QFont f = qApp->font();
f.setPixelSize(kBaseFontPx * g_fontScale / 100);
qApp->setFont(f);
}
int scaledPx(int basePx)
{
return basePx * g_fontScale / 100;
}
bool isDarkTheme()
{
return ThemeManager::instance().isDark();
}
void vtkBackground(double& r, double& g, double& b)
{
// 规范 §0.5/§11数据画布永远深色不随明暗切换。取 canvas/bg。
const QColor c = tokenColor("canvas/bg"); // #0B1320
r = c.redF();
g = c.greenF();
b = c.blueF();
}
QString token(const char* name) { return tokenHex(name, isDarkTheme()); }
QColor tokenColor(const char* name) { return QColor(token(name)); }
QString fillTokens(const QString& tmpl)
{
const bool dark = isDarkTheme();
QString s = tmpl;
for (const auto& t : kTokens)
s.replace(QStringLiteral("{{%1}}").arg(QLatin1String(t.name)),
QString::fromLatin1(dark ? t.dark : t.light));
return s;
}
void applyTokenizedStyleSheet(QWidget* w, const QString& tmpl)
{
if (!w) return;
w->setStyleSheet(fillTokens(tmpl));
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, w,
[w, tmpl]() { w->setStyleSheet(fillTokens(tmpl)); });
}
} // namespace geopro::app