#include "Theme.hpp" #include "Glyphs.hpp" #include #include #include #include #include #include #include #include #include #include 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——一旦改写 branch,Qt 会停止绘制 默认的展开/折叠箭头(与 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/dialog):Qt 顶层原生窗口忽略 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