804 lines
26 KiB
C++
804 lines
26 KiB
C++
#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——一旦改写 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
|