Compare commits

..

No commits in common. "d1be0567de5baa01484dc9fbe5c524d4481d36cc" and "824e8bdf62d991584e020ae3831d5ebf67ecf775" have entirely different histories.

16 changed files with 220 additions and 458 deletions

View File

@ -11,7 +11,6 @@
#include <QPixmap>
#include <QPointF>
#include <QRectF>
#include <QSize>
#include <QString>
#include <QSvgRenderer>
@ -99,7 +98,7 @@ QString svgPathFor(Glyph t)
} // namespace
QIcon makeGlyph(Glyph type, const QColor& color, int px, int padRight)
QIcon makeGlyph(Glyph type, const QColor& color, int px)
{
const QString svg =
QStringLiteral("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' "
@ -111,13 +110,12 @@ QIcon makeGlyph(Glyph type, const QColor& color, int px, int padRight)
// 以 3x 超采样渲染再设 devicePixelRatio保证在任意缩放/DPI 下都清晰。
constexpr qreal kSuper = 3.0;
const int dim = qRound(px * kSuper); // 图标本体边长(方形)
const int dimW = qRound((px + padRight) * kSuper); // 含右透明内边距的画布宽
QImage img(dimW, dim, QImage::Format_ARGB32_Premultiplied);
const int dim = qRound(px * kSuper);
QImage img(dim, dim, QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::transparent);
QPainter p(&img);
p.setRenderHint(QPainter::Antialiasing, true);
renderer.render(&p, QRectF(0, 0, dim, dim)); // 图标居左方形渲染,右侧 padRight 留透明
renderer.render(&p, QRectF(0, 0, dim, dim));
p.end();
QPixmap pm = QPixmap::fromImage(img);
@ -216,13 +214,10 @@ void setThemedGlyph(QLabel* label, Glyph type, int px)
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, label, [apply]() { apply(); });
}
void setThemedGlyph(QAbstractButton* button, Glyph type, int px, int padRight)
void setThemedGlyph(QAbstractButton* button, Glyph type, int px)
{
if (!button) return;
auto apply = [button, type, px, padRight]() {
button->setIcon(makeGlyph(type, themedIconColor(), px, padRight));
if (padRight > 0) button->setIconSize(QSize(px + padRight, px)); // 含右内边距,文字被右推
};
auto apply = [button, type, px]() { button->setIcon(makeGlyph(type, themedIconColor(), px)); };
apply();
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, button, [apply]() { apply(); });
}

View File

@ -34,19 +34,13 @@ enum class Glyph {
Gear, // 设置(齿轮)
};
// 「图标+文字」按钮的图标→文字间距补丁Fusion 内置约 4px本值补到规范 §6.7 的 6px。
// 做法把图标渲染进“px 宽图标 + padRight 透明右边距”的画布,文字被这段透明区右推。
inline constexpr int kGlyphTextGapPad = 2;
// 生成指定颜色、像素尺寸的图标(默认 16px内部按 2x 绘制保证清晰)。
QIcon makeGlyph(Glyph type, const QColor& color, int px = 16);
// 生成指定颜色、像素尺寸的图标(默认 16px内部按 3x 绘制保证清晰)。
// padRight>0 时图标画布右侧留透明内边距(用于「图标+文字」按钮统一间距),图标本体仍为 px×px 居左。
QIcon makeGlyph(Glyph type, const QColor& color, int px = 16, int padRight = 0);
// 随主题明暗自动着色的 glyph取主题文本色暗色用浅色、亮色用深色主题切换时自动重绘。
// 随 ElaTheme 明暗自动着色的 glyph取主题文本色暗色用浅色、亮色用深色主题切换时自动重绘。
// 用于面板表头/页签等 chrome 图标,避免固定色在暗色下看不清。
// 按钮版的 padRight见 kGlyphTextGapPad>0 时本函数同时把 iconSize 设为 (px+padRight)×px。
void setThemedGlyph(QLabel* label, Glyph type, int px);
void setThemedGlyph(QAbstractButton* button, Glyph type, int px, int padRight = 0);
void setThemedGlyph(QAbstractButton* button, Glyph type, int px);
// 生成树展开/折叠箭头 PNG 到临时目录,返回文件路径(供树的 QSS `image: url(...)` 引用)。
// 配合 QTreeView::branch 背景使用,可让选中高亮不覆盖左侧缩进/箭头列且不丢箭头。

View File

@ -85,7 +85,7 @@ QWidget* buildPanelHeader(Glyph icon, const QString& title, const QVector<Header
auto* lay = new QHBoxLayout(header);
lay->setContentsMargins(12, 0, 8, 0);
lay->setSpacing(geopro::app::space::kSm);
lay->setSpacing(8);
auto* iconLbl = new QLabel(header);
setThemedGlyph(iconLbl, icon, kTitleIcon); // 随主题着色(暗色下也清晰)
@ -131,7 +131,8 @@ TabbedPanel buildTabbedPanel(const QVector<PanelTab>& tabs, const QVector<Header
auto* btn = new QToolButton(header); // 页签与工具条统一: QToolButton + 强调色下划线 QSS
btn->setObjectName(QStringLiteral("tabBtn"));
btn->setText(t.title);
setThemedGlyph(btn, t.icon, kTabIcon, kGlyphTextGapPad); // 随主题着色 + 图标→文字6px(§6.7)
setThemedGlyph(btn, t.icon, kTabIcon); // 随主题着色
btn->setIconSize(QSize(kTabIcon, kTabIcon));
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
btn->setCheckable(true);
btn->setCursor(Qt::PointingHandCursor);

View File

@ -28,11 +28,11 @@ QString statusText(int s) {
}
// 状态语义色(寻路):未开始=弱化中性、进行中=信息蓝(活动中);未知状态用中性灰。
QColor statusColor(int s) {
const char* statusColorHex(int s) {
switch (s) {
case 1: return tokenColor("text/tertiary"); // 未开始:弱化
case 2: return tokenColor("accent/primary"); // 进行中:活动中
default: return tokenColor("text/secondary"); // 未知:中性
case 1: return "#8A93A3"; // 未开始:弱化
case 2: return semantic::kInfo; // 进行中:活动中
default: return "#5A6B85"; // 未知:中性
}
}
} // namespace
@ -155,12 +155,12 @@ void ProjectListDialog::query() {
set(0, QString::number((pageNo_ - 1) * pageSize_ + i + 1));
auto* nameItem = new QTableWidgetItem(QString::fromStdString(p.name));
nameItem->setData(Qt::UserRole, QString::fromStdString(p.id));
nameItem->setForeground(tokenColor("accent/primary"));
nameItem->setForeground(QColor("#2D6CB5"));
table_->setItem(i, 1, nameItem);
set(2, QString::fromStdString(p.code));
// 状态列语义着色:颜色承载“未开始/进行中”分类,进行中加粗强调(不只靠颜色)。
auto* statusItem = new QTableWidgetItem(statusText(p.status));
statusItem->setForeground(statusColor(p.status));
statusItem->setForeground(QColor(statusColorHex(p.status)));
if (p.status == 2) {
QFont f = statusItem->font();
f.setBold(true);

View File

@ -74,8 +74,8 @@ QWidget* buildAppearancePage() {
rlay->setContentsMargins(96 + 12, 0, 0, 0); // 与控件列对齐
rlay->setSpacing(10);
auto* hint = new QLabel(QStringLiteral("界面字号将在重启后生效"), restartRow);
geopro::app::applyTokenizedStyleSheet(
hint, QStringLiteral("color:{{text/secondary}}; font-size:%1px;")
geopro::app::applyThemedStyleSheet(
hint, QStringLiteral("color:#5A6B85; font-size:%1px;")
.arg(geopro::app::scaledPx(geopro::app::type::kCaption)));
auto* restartBtn = new QPushButton(QStringLiteral("立即重启"), restartRow);
rlay->addWidget(hint);

View File

@ -1,7 +1,5 @@
#include "Theme.hpp"
#include "Glyphs.hpp"
#include <QApplication>
#include <QColor>
#include <QFont>
@ -157,11 +155,6 @@ 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 */
@ -456,34 +449,64 @@ ads--CDockWidgetTab[activeTab="true"] QLabel {
}
)QSS";
// 全局复选指示器:用 writeCheckboxIcon 生成清晰复选框 PNG未选=明显边框空心框,
// 选中=强调色填充+白勾),统一作用于 QCheckBox 与 树/列表的勾选指示器。规避 Fusion
// 原生复选框在浅底下边框过淡看不清的问题——全 UI 一套,避免逐控件打补丁。
QString indicatorQss(bool dark)
// ── 主题桥配色:工作台标准控件的 QSS/调色板颜色直接取自 ElaTheme
// 保证里外ElaWindow 外壳 ↔ 内部工作台)在明/暗两模下完全一致——这是关键,
// 否则外壳一种灰、工作台另一种灰,明暗都显得割裂。
// 做法:把浅色设计稿里每个色令牌按语义角色替换为 ElaTheme 当前模式的真实颜色。
// 浅→暗 颜色映射(全 UI 唯一颜色来源)。明色 = 令牌本身QSS 直接写的就是明色);
// 暗色 = 此表对应值。覆盖全部 QSS 用色,缺一即在暗色下露浅色。改色只改这一处。
struct DarkPair {
const char* light;
const char* dark;
};
const DarkPair kDarkMap[] = {
// 背景(外壳→面板→抬升)
{"#F4F6FA", "#1E1F22"}, {"#FFFFFF", "#2B2D30"}, {"#EDF1F7", "#34373C"},
{"#F0F2F6", "#2B2D30"}, {"#EAEEF4", "#3A3D42"}, {"#EAEEF5", "#34373C"},
{"#EEF2FB", "#34373C"}, {"#E6EBF3", "#34373C"}, {"#EEF3FB", "#34373C"},
{"#EAF1FB", "#2F4257"}, {"#DCE6F4", "#2A3A4F"}, {"#DCE9F8", "#33527A"},
{"#FBEAD2", "#46371F"},
// 文字
{"#1F2A3D", "#E6E8EB"}, {"#5A6B85", "#A0A8B4"}, {"#3A475C", "#C4CCD8"},
{"#8A93A3", "#828B98"}, {"#9AA6B6", "#6E7681"}, {"#1B3D67", "#E8F1FB"},
// 边框
{"#D5DBE5", "#3A3D42"}, {"#C2CCDA", "#484C52"}, {"#C7D2E0", "#484C52"},
{"#E1E6EE", "#34373C"}, {"#E6EAF1", "#34373C"}, {"#EEF1F5", "#34373C"},
{"#DCE0E7", "#3A3D42"}, {"#A7B4C7", "#4A4E54"},
// 强调(品牌蓝)
{"#2D6CB5", "#5E9BD6"}, {"#2862A6", "#6FA8DD"}, {"#234F87", "#4E89C4"},
// 语义
{"#C0392B", "#E06A5E"}, {"#B45309", "#E0964A"}, {"#15803D", "#5BBF7A"},
};
QString darkOf(const QString& lightHex)
{
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);
for (const auto& p : kDarkMap)
if (lightHex.compare(QLatin1String(p.light), Qt::CaseInsensitive) == 0)
return QString::fromLatin1(p.dark);
return lightHex;
}
// 设计令牌(浅色 hex) → 当前明暗的真实色。
QColor roleColor(bool dark, const char* lightHex)
{
return dark ? QColor(darkOf(QString::fromLatin1(lightHex))) : QColor(QLatin1String(lightHex));
}
// 把一段浅色设计稿 QSS 按当前明暗着色:明色原样;暗色把每个浅色令牌替换为暗色。
QString themedQss(const QString& designQss, bool dark)
{
if (!dark) return designQss;
QString s = designQss;
for (const auto& p : kDarkMap)
s.replace(QString::fromLatin1(p.light), QString::fromLatin1(p.dark), Qt::CaseInsensitive);
return s;
}
// 当前模式的全局 QSS。
QString styleSheetForMode(bool /*dark*/)
{
const bool dark = isDarkTheme();
return fillTokens(QString::fromUtf8(kStyleSheet)) + indicatorQss(dark);
return fillTokens(QString::fromUtf8(kStyleSheet));
}
// 调色板同样取自 ElaTheme让无 QSS 覆盖处的标准控件也与外壳一致。
@ -639,6 +662,11 @@ bool isDarkTheme()
return ThemeManager::instance().isDark();
}
QString themed(const QString& designQss)
{
return themedQss(designQss, isDarkTheme());
}
void vtkBackground(double& r, double& g, double& b)
{
// 规范 §0.5/§11数据画布永远深色不随明暗切换。取 canvas/bg。
@ -648,6 +676,14 @@ void vtkBackground(double& r, double& g, double& b)
b = c.blueF();
}
void applyThemedStyleSheet(QWidget* w, const QString& designQss)
{
if (!w) return;
w->setStyleSheet(themedQss(designQss, isDarkTheme()));
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, w,
[w, designQss]() { w->setStyleSheet(themedQss(designQss, isDarkTheme())); });
}
QString token(const char* name) { return tokenHex(name, isDarkTheme()); }
QColor tokenColor(const char* name) { return QColor(token(name)); }

View File

@ -19,7 +19,7 @@ class QWidget;
namespace geopro::app {
// 主题管理器(纯 Qt替代 ElaTheme持有当前明暗 + 是否跟随系统;切换发 changed() 信号,
// 全 UI全局 QSS 由 main 重应用、内联 chrome 由 applyTokenizedStyleSheet据此热切重着色。
// 全 UI全局 QSS 由 main 重应用、内联 chrome 由 applyThemedStyleSheet据此热切重着色。
class ThemeManager : public QObject {
Q_OBJECT
public:
@ -103,7 +103,7 @@ inline constexpr const char* kWarningFill = "#FBEAD2"; // 警告底纹(配 kW
} // namespace semantic
// 应用专业主题Fusion + 调色板 + 全局样式表。dark=true 走暗色P2 主题桥用)。
// 暗色复用同一 QSS 结构,颜色全由 kTokens 双值fillTokens/tokenHex驱动;幂等,可随主题切换重复调用。
// 暗色复用同一 QSS 结构,仅按 kDarkMap 换色;幂等,可随主题切换重复调用。
void applyThemeMode(QApplication& app, bool dark);
// 浅色主题快捷入口(= applyThemeMode(app,false))。经典壳启动调用一次。
@ -128,6 +128,15 @@ bool isDarkTheme();
// VTK 渲染器背景色(随当前主题,取 ElaTheme 窗口底色)。写入 r/g/b01
void vtkBackground(double& r, double& g, double& b);
// 把一段「浅色设计稿 QSS」按当前 ElaTheme 配色着色应用到 widget并随明/暗切换自动重着色。
// 用于 TopBar/PanelHeader/浮层 等带内联 setStyleSheet 的自定义 chrome——让它们也跟随主题
// (设计稿里用浅色令牌 #1F2A3D/#FFFFFF/#2D6CB5… 书写即可,与全局 QSS 同一套角色映射)。
void applyThemedStyleSheet(QWidget* w, const QString& designQss);
// 把一段「浅色设计稿 QSS」按当前 ElaTheme 配色着色后返回(供需要拼接/手动 setStyleSheet 的场景,
// 如 ADS dockManager 在其自带样式后追加规则)。不自动随主题切换,调用方需自行在切换时重取。
QString themed(const QString& designQss);
// ── 语义令牌(单一事实来源,取值见 Theme.cpp kTokens规范 §1.5 + 附录 A + §1.3)──
// 组件只引语义 token禁止散落硬编码 hex。token 名形如 "bg/panel"、"accent/primary"。
QString token(const char* name); // 当前明暗下的 hex未知名返回品红 "#FF00FF" 以便一眼发现漏配)

View File

@ -3,20 +3,15 @@
#include <QAbstractButton>
#include <QActionGroup>
#include <QColor>
#include <QEvent>
#include <QFont>
#include <QFrame>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QMenu>
#include <QMenuBar>
#include <QPainter>
#include <QPixmap>
#include <QSize>
#include <QVBoxLayout>
#include <QStringList>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWidget>
#include "Glyphs.hpp"
@ -55,29 +50,6 @@ QWidget* makeIconButton(QWidget* parent, Glyph icon, const QString& tip)
return btn;
}
// 圆形头像图标:强调色填充 + 白色缩写。2x 绘制保证高 DPI 清晰。
QPixmap renderAvatar(const QString& initials, int px, const QColor& bg, const QColor& fg)
{
constexpr int kScale = 2;
const int s = px * kScale;
QPixmap pm(s, s);
pm.fill(Qt::transparent);
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing, true);
p.setPen(Qt::NoPen);
p.setBrush(bg);
p.drawEllipse(0, 0, s, s);
QFont f = p.font();
f.setPixelSize(static_cast<int>(s * 0.4));
f.setBold(true);
p.setFont(f);
p.setPen(fg);
p.drawText(QRect(0, 0, s, s), Qt::AlignCenter, initials);
p.end();
pm.setDevicePixelRatio(kScale);
return pm;
}
// ── 四个菜单(结构对齐需求;叶子项当前为静态占位,后续接真实页面)──
QMenu* buildViewMenu(QWidget* p)
{
@ -160,24 +132,16 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
// 角色名=caption(12)。原 11px 角色名上调到 12去掉只差 1px 的糊层级。
// 切换器(ElaToolButton)/图标(ElaIconButton) 自绘 Fluent不再写它们的 QSS。
// 仅保留:工具条底/分隔线、头像(圆形自定义)、用户名/角色。头像白字用 {{text/on-primary}} 令牌。
// 切换器下拉箭头:用生成的高清 chevron PNG 作 menu-indicator替代旧的粗糙文字箭头中性灰双主题可读。
const QString chevron = geopro::app::writeChevronIcon(true, QColor("#7C8493"));
geopro::app::applyTokenizedStyleSheet(
this, QStringLiteral(
"#appToolBar { background:{{bg/header}}; border-bottom:1px solid {{divider}}; }"
"#topDivider { color:{{divider}}; }"
"QToolButton::menu-indicator { image:none; }"
"#wsSwitcher { color:{{text/primary}}; border:none; border-radius:8px; padding:8px 26px 8px 12px;"
"#wsSwitcher { color:{{text/primary}}; border:none; border-radius:8px; padding:8px 12px;"
" font-size:%6px; font-weight:%4; }"
"#wsSwitcher:hover { background:{{bg/hover}}; }"
"#wsSwitcher::menu-indicator { image:url(%7); width:13px; height:13px;"
" subcontrol-position: right center; subcontrol-origin: padding; right:8px; }"
"QToolButton#iconBtn { border:none; border-radius:8px; padding:8px; }"
"QToolButton#iconBtn:hover { background:{{bg/hover}}; }"
"#userBtn { border:none; border-radius:8px; padding:4px 10px 4px 6px;"
" color:{{text/primary}}; font-size:%3px; }"
"#userBtn:hover { background:{{bg/hover}}; }"
"#userBtn::menu-indicator { image:none; }"
"#avatar { background:{{accent/primary}}; color:{{text/on-primary}}; border-radius:17px; font-weight:%2;"
" font-size:%1px; }"
"#userName { color:{{text/primary}}; font-size:%3px; font-weight:%4; }"
@ -187,17 +151,17 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
.arg(scaledPx(type::kLabel))
.arg(type::kWeightSemibold)
.arg(scaledPx(type::kCaption))
.arg(scaledPx(type::kTitle))
.arg(chevron));
.arg(scaledPx(type::kTitle)));
auto* lay = new QHBoxLayout(this);
lay->setContentsMargins(14, 0, 14, 0);
lay->setSpacing(0);
// 工作空间切换器QToolButton + 主题化 QSS下拉箭头用高清 chevron menu-indicator;数据驱动)。
// 工作空间切换器QToolButton + 主题化 QSS下拉箭头用文字"▾"保证清晰;数据驱动)。
wsBtn_ = new QToolButton(this);
wsBtn_->setObjectName(QStringLiteral("wsSwitcher"));
setThemedGlyph(wsBtn_, Glyph::Workspace, kWorkspaceIcon, kGlyphTextGapPad); // 图标→文字6px(§6.7)
setThemedGlyph(wsBtn_, Glyph::Workspace, kWorkspaceIcon); // 中性主题色(蓝只留给选中/激活)
wsBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon));
wsBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
wsBtn_->setPopupMode(QToolButton::InstantPopup);
wsBtn_->setCursor(Qt::PointingHandCursor);
@ -212,7 +176,8 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
// 项目切换器QToolButton + 主题化 QSS数据驱动
projBtn_ = new QToolButton(this);
projBtn_->setObjectName(QStringLiteral("wsSwitcher"));
setThemedGlyph(projBtn_, Glyph::Folder, kWorkspaceIcon, kGlyphTextGapPad); // 中性主题色 + 图标→文字6px
setThemedGlyph(projBtn_, Glyph::Folder, kWorkspaceIcon); // 中性主题色
projBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon));
projBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
projBtn_->setPopupMode(QToolButton::InstantPopup);
projBtn_->setCursor(Qt::PointingHandCursor);
@ -232,65 +197,31 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
lay->addWidget(makeDivider(this));
lay->addSpacing(12);
// 用户区:头像(圆形,竖直居中) + 右侧 姓名(上)/职务(下) 左对齐 + 下拉箭头;整块可点 → 菜单。
// 用普通 QWidget + eventFilterQWidget 按子布局正确撑开QPushButton 装布局会按空文字算尺寸挤成一团)。
userRow_ = new QWidget(this);
userRow_->setObjectName(QStringLiteral("userBtn"));
userRow_->setAttribute(Qt::WA_StyledBackground, true); // 令 QSS 背景(hover)在 QWidget 上生效
userRow_->setCursor(Qt::PointingHandCursor);
userRow_->installEventFilter(this);
auto* uLay = new QHBoxLayout(userRow_);
uLay->setContentsMargins(8, 3, 8, 3);
uLay->setSpacing(10);
auto* avatar = new QLabel(userRow_);
avatar->setPixmap(
renderAvatar(QStringLiteral("ZL"), 34, geopro::app::tokenColor("accent/primary"), Qt::white));
// 用户区:头像可点击 → 弹出菜单(退出登录)。
auto* avatar = new QToolButton(this);
avatar->setObjectName(QStringLiteral("avatar"));
avatar->setText(QStringLiteral("ZL"));
avatar->setFixedSize(34, 34);
avatar->setAttribute(Qt::WA_TransparentForMouseEvents);
uLay->addWidget(avatar, 0, Qt::AlignVCenter);
auto* nameBox = new QWidget(userRow_);
nameBox->setAttribute(Qt::WA_TransparentForMouseEvents);
auto* nameLay = new QVBoxLayout(nameBox);
nameLay->setContentsMargins(0, 0, 0, 0);
nameLay->setSpacing(0);
auto* userName = new QLabel(QStringLiteral("张磊"), nameBox);
userName->setObjectName(QStringLiteral("userName"));
auto* userRole = new QLabel(QStringLiteral("高级工程师"), nameBox);
userRole->setObjectName(QStringLiteral("userRole"));
nameLay->addWidget(userName);
nameLay->addWidget(userRole);
uLay->addWidget(nameBox, 0, Qt::AlignVCenter);
auto* chevronLbl = new QLabel(userRow_);
chevronLbl->setPixmap(QPixmap(geopro::app::writeChevronIcon(true, QColor("#7C8493")))
.scaled(12, 12, Qt::KeepAspectRatio, Qt::SmoothTransformation));
chevronLbl->setAttribute(Qt::WA_TransparentForMouseEvents);
uLay->addWidget(chevronLbl, 0, Qt::AlignVCenter);
// 下拉菜单(加宽):账户 / 个人资料 / 偏好设置 / API 密钥 / 退出登录。
userMenu_ = new QMenu(this);
userMenu_->setMinimumWidth(200);
userMenu_->addAction(QStringLiteral("账户"));
userMenu_->addAction(QStringLiteral("个人资料"));
QObject::connect(userMenu_->addAction(QStringLiteral("偏好设置")), &QAction::triggered, this,
[this] { emit settingsRequested(); });
userMenu_->addAction(QStringLiteral("API 密钥"));
userMenu_->addSeparator();
QObject::connect(userMenu_->addAction(QStringLiteral("退出登录")), &QAction::triggered, this,
avatar->setCursor(Qt::PointingHandCursor);
avatar->setPopupMode(QToolButton::InstantPopup);
auto* userMenu = new QMenu(avatar);
QObject::connect(userMenu->addAction(QStringLiteral("退出登录")), &QAction::triggered, this,
[this] { emit logoutRequested(); });
avatar->setMenu(userMenu);
lay->addWidget(avatar);
lay->addSpacing(8);
lay->addWidget(userRow_);
}
bool TopBar::eventFilter(QObject* obj, QEvent* event) {
if (obj == userRow_ && event->type() == QEvent::MouseButtonRelease) {
if (userMenu_)
userMenu_->exec(userRow_->mapToGlobal(QPoint(0, userRow_->height() + 2)));
return true;
}
return QWidget::eventFilter(obj, event);
auto* userBox = new QWidget(this);
auto* userLay = new QVBoxLayout(userBox);
userLay->setContentsMargins(0, 0, 0, 0);
userLay->setSpacing(0);
auto* userName = new QLabel(QStringLiteral("张磊"), userBox);
userName->setObjectName(QStringLiteral("userName"));
auto* userRole = new QLabel(QStringLiteral("高级工程师"), userBox);
userRole->setObjectName(QStringLiteral("userRole"));
userLay->addWidget(userName);
userLay->addWidget(userRole);
lay->addWidget(userBox);
}
void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QString& currentId) {
@ -310,7 +241,7 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QStri
group->addAction(a);
if (id == currentId) currentName = name;
QObject::connect(a, &QAction::triggered, this, [this, id, name]() {
wsBtn_->setText(name); // 立即反馈
wsBtn_->setText(name + QStringLiteral("")); // 立即反馈
emit workspaceSwitchRequested(id);
});
}
@ -319,7 +250,8 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QStri
none->setEnabled(false);
}
wsBtn_->setMenu(menu);
wsBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择空间") : currentName);
wsBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择空间") : currentName) +
QStringLiteral(""));
}
void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QString& currentId,
@ -340,7 +272,7 @@ void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QS
group->addAction(a);
if (id == currentId) currentName = name;
QObject::connect(a, &QAction::triggered, this, [this, id, name]() {
projBtn_->setText(name);
projBtn_->setText(name + QStringLiteral(""));
emit projectSwitchRequested(id);
});
}
@ -354,11 +286,12 @@ void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QS
QObject::connect(all, &QAction::triggered, this, [this]() { emit allProjectsRequested(); });
}
projBtn_->setMenu(menu);
projBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择项目") : currentName);
projBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择项目") : currentName) +
QStringLiteral(""));
}
void TopBar::setProjectButtonText(const QString& name) {
projBtn_->setText(name);
projBtn_->setText(name + QStringLiteral(""));
}
} // namespace geopro::app

View File

@ -4,8 +4,6 @@
#include "repo/RepoTypes.hpp"
class QToolButton;
class QEvent;
class QMenu;
namespace geopro::app {
@ -23,9 +21,6 @@ public:
bool hasMore);
void setProjectButtonText(const QString& name); // 弹窗切换项目后更新按钮文字
protected:
bool eventFilter(QObject* obj, QEvent* event) override; // 用户区整块可点 → 弹菜单
signals:
void workspaceSwitchRequested(const QString& tenantId);
void projectSwitchRequested(const QString& projectId);
@ -36,8 +31,6 @@ signals:
private:
QToolButton* wsBtn_ = nullptr;
QToolButton* projBtn_ = nullptr;
QWidget* userRow_ = nullptr; // 用户区整块(头像+姓名/职务+箭头)
QMenu* userMenu_ = nullptr; // 用户下拉菜单
};
} // namespace geopro::app

View File

@ -89,17 +89,17 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
// 字号引用 Theme 排版令牌:品牌名=display(24)、副标题/字段标签=caption(12)。
// 登录窗整体随 ElaTheme 着色(与 Ela 化的输入/按钮一致,避免暗系统下浅窗+暗控件割裂)。
// 品牌带文字用 white 关键字(不入角色映射→恒为白),保证落在蓝色横幅上始终可读。
geopro::app::applyTokenizedStyleSheet(
geopro::app::applyThemedStyleSheet(
this, QStringLiteral(
"QDialog { background: {{bg/app}}; }"
"QDialog { background: #F4F6FA; }"
"#headerBand {"
" background: qlineargradient(x1:0, y1:0, x2:1, y2:1,"
" stop:0 {{accent/primary}}, stop:1 {{accent/primary-pressed}}); }"
"#brandTitle { color: {{text/on-primary}}; font-size: %1px; font-weight: %2; }"
" stop:0 #2D6CB5, stop:1 #234F87); }"
"#brandTitle { color: white; font-size: %1px; font-weight: %2; }"
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }"
"#fieldLabel { color: {{text/secondary}}; font-size: %4px; font-weight: %5; }"
"#fieldLabel { color: #5A6B85; font-size: %4px; font-weight: %5; }"
// 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。
"#captchaImg { border: 1px solid {{border/strong}}; border-radius: 8px; background: {{bg/hover}}; }")
"#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }")
.arg(scaledPx(type::kDisplay))
.arg(type::kWeightBold)
.arg(scaledPx(type::kCaption))
@ -178,11 +178,11 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
refreshBtn_ = new QPushButton(QStringLiteral("看不清?换一张"), body);
refreshBtn_->setFlat(true);
refreshBtn_->setCursor(Qt::PointingHandCursor);
geopro::app::applyTokenizedStyleSheet(
geopro::app::applyThemedStyleSheet(
refreshBtn_,
QStringLiteral(
"QPushButton { color: {{accent/primary}}; border: none; background: transparent; padding: 2px 0; }"
"QPushButton:hover { color: {{accent/primary-pressed}}; text-decoration: underline; }"));
"QPushButton { color: #2D6CB5; border: none; background: transparent; padding: 2px 0; }"
"QPushButton:hover { color: #234F87; text-decoration: underline; }"));
refreshRow->addWidget(refreshBtn_);
form->addLayout(refreshRow);
@ -193,8 +193,8 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
// 错误提示:固定占位高度,避免出现时整体布局跳动。
errorLabel_ = new QLabel(body);
geopro::app::applyTokenizedStyleSheet(
errorLabel_, QStringLiteral("color: {{status/danger}}; font-size: %1px;").arg(scaledPx(type::kCaption)));
geopro::app::applyThemedStyleSheet(
errorLabel_, QStringLiteral("color: #C0392B; font-size: %1px;").arg(scaledPx(type::kCaption)));
errorLabel_->setWordWrap(true);
errorLabel_->setMinimumHeight(18);
form->addWidget(errorLabel_);

View File

@ -330,11 +330,6 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
" font-size:%1px; }"
"QToolButton:hover{ background:{{bg/hover}}; }"
"QToolButton:checked{ color:{{accent/primary}}; font-weight:%2;"
" border-bottom:2px solid {{accent/primary}}; }"
"QToolButton#dataTab{ border:none; border-radius:0; background:transparent;"
" border-bottom:2px solid transparent; color:{{text/secondary}}; padding:8px 8px; }"
"QToolButton#dataTab:hover{ color:{{text/primary}}; background:transparent; }"
"QToolButton#dataTab:checked{ color:{{accent/primary}}; font-weight:%2;"
" border-bottom:2px solid {{accent/primary}}; }")
.arg(geopro::app::scaledPx(geopro::app::type::kBody))
.arg(geopro::app::type::kWeightSemibold);
@ -487,8 +482,6 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
detailGroup->setExclusive(true);
auto* actScatter = makeBarBtn(QStringLiteral("原数据"), true);
auto* actSection = makeBarBtn(QStringLiteral("网格数据"), true);
actScatter->setObjectName(QStringLiteral("dataTab"));
actSection->setObjectName(QStringLiteral("dataTab"));
detailGroup->addButton(actScatter);
detailGroup->addButton(actSection);
detailBarLay->addWidget(actScatter);
@ -524,23 +517,40 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 左上 dock对象树真实结构项目根 → GS → TM。被动视图数据由控制器推送。
auto* objectTree = new geopro::app::ObjectTreePanel();
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象"));
leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象"),
auto* leftDock = new ads::CDockWidget(QStringLiteral("对象显示栏"));
leftDock->setWidget(wrapWithHeader(geopro::app::Glyph::Tree, QStringLiteral("对象显示栏"),
objectTree,
{{geopro::app::Glyph::Plus, QStringLiteral("新建对象")}}));
auto* leftArea = dockManager->addDockWidget(ads::LeftDockWidgetArea, leftDock);
// 列表选中色:写死的强调蓝(明 #C2D9F2 / 暗 #33527A+ 适配文字,:!active 防失焦变淡;
// 与对象树选中色一致。本地 QSS 覆盖全局弱选中色,随主题重设。
auto applyListSelection = [](QListWidget* lw) {
auto styleIt = [lw]() {
const bool dark = geopro::app::isDarkTheme();
const QString selBg = dark ? QStringLiteral("#33527A") : QStringLiteral("#C2D9F2");
const QString selFg = dark ? QStringLiteral("#E8F1FB") : QStringLiteral("#14385F");
lw->setStyleSheet(QStringLiteral("QListWidget::item:selected{ background:%1; color:%2; }"
"QListWidget::item:selected:!active{ background:%1;"
" color:%2; }")
.arg(selBg, selFg));
};
styleIt();
QObject::connect(&geopro::app::ThemeManager::instance(), &geopro::app::ThemeManager::changed,
lw, [styleIt]() { styleIt(); });
};
// 左下 dock数据真实显示栏(选中测线后列其采集批次=数据集;tab 数据/文件)。
auto* datasetTabs = new QTabWidget();
auto* datasetList = new QListWidget();
geopro::app::applyDatasetCardDelegate(datasetList);
applyListSelection(datasetList);
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
auto* fileList = new QListWidget();
geopro::app::applyDatasetCardDelegate(fileList);
applyListSelection(fileList);
datasetTabs->addTab(fileList, QStringLiteral("文件"));
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据集"));
auto* datasetDock = new ads::CDockWidget(QStringLiteral("数据真实显示栏"));
auto* datasetBox = wrapWithHeader(
geopro::app::Glyph::Dataset, QStringLiteral("数据集"), datasetTabs,
geopro::app::Glyph::Dataset, QStringLiteral("数据真实显示栏"), datasetTabs,
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
{geopro::app::Glyph::Upload, QStringLiteral("上传")}});
datasetDock->setWidget(datasetBox);
@ -550,14 +560,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 右上 dock异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。
auto* anomalyList = new QListWidget();
geopro::app::applyAnomalyCardDelegate(anomalyList);
applyListSelection(anomalyList);
auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)"));
objAttrLabel->setWordWrap(true);
objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
objAttrLabel->setMargin(8);
auto anomalyPanel = geopro::app::buildTabbedPanel(
{{geopro::app::Glyph::Anomaly, QStringLiteral("异常"), anomalyList, true},
{{geopro::app::Glyph::Anomaly, QStringLiteral("异常列表"), anomalyList, true},
{geopro::app::Glyph::Property, QStringLiteral("对象属性"), objAttrLabel, false}},
{{geopro::app::Glyph::Filter, QStringLiteral("筛选")},
{geopro::app::Glyph::Plus, QStringLiteral("添加异常")}});
@ -572,7 +582,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
anomalyBadge->style()->polish(anomalyBadge);
}
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常/对象属性"));
auto* rightDock = new ads::CDockWidget(QStringLiteral("异常列表/对象属性"));
rightDock->setWidget(anomalyPanel.container);
auto* rightArea = dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock);
@ -581,9 +591,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
propLabel->setWordWrap(true);
propLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
propLabel->setMargin(8);
auto* propDock = new ads::CDockWidget(QStringLiteral("数据集属性"));
auto* propDock = new ads::CDockWidget(QStringLiteral("属性"));
propDock->setWidget(
wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("数据集属性"), propLabel));
wrapWithHeader(geopro::app::Glyph::Property, QStringLiteral("属性"), propLabel));
dockManager->addDockWidget(ads::BottomDockWidgetArea, propDock, rightArea);
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
@ -886,6 +896,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
auto* m = new QListWidgetItem(QStringLiteral("加载更多(%1/%2").arg(loaded).arg(total), lw);
m->setData(geopro::app::kDsLoadMoreRole, true);
m->setTextAlignment(Qt::AlignCenter);
m->setForeground(QColor("#2D6CB5"));
}
return loaded;
};
@ -935,7 +946,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
objectTree->setStructure(projectName, nodes);
datasetList->clear();
fileList->clear();
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集"));
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
datasetTabs->setTabText(0, QStringLiteral("数据"));
datasetTabs->setTabText(1, QStringLiteral("文件"));
});
@ -946,7 +957,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
removeLoadMore(datasetList);
geopro::app::populateDatasetList(datasetList, rows, append);
const int loaded = addLoadMore(datasetList, total);
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集"));
if (datasetTitle) datasetTitle->setText(QStringLiteral("数据集显示栏"));
datasetTabs->setTabText(
0, total > 0 ? QStringLiteral("数据 (%1/%2)").arg(loaded).arg(total)
: QStringLiteral("数据"));
@ -999,9 +1010,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
const QSettings settings;
const QByteArray geo = settings.value(QStringLiteral("ui/geometry")).toByteArray();
if (!geo.isEmpty()) window.restoreGeometry(geo);
// 注意ADS 按 dock 唯一名作键。改过 dock 名后旧布局会失配 → bump 此键丢弃旧布局,
// 回落到下方 addDockWidget 的默认排布(再改 dock 名时同样要 bump 版本号)。
const QByteArray dockState = settings.value(QStringLiteral("ui/dockState_v2")).toByteArray();
const QByteArray dockState = settings.value(QStringLiteral("ui/dockState")).toByteArray();
if (!dockState.isEmpty()) {
dockManager->restoreState(dockState);
// restoreState 重建停靠区会重新显示 ADS 标题栏,再隐藏一次保持“无双标题”。
@ -1012,7 +1021,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
QObject::connect(qApp, &QCoreApplication::aboutToQuit, dockManager, [dockManager, &window]() {
QSettings settings;
settings.setValue(QStringLiteral("ui/geometry"), window.saveGeometry());
settings.setValue(QStringLiteral("ui/dockState_v2"), dockManager->saveState());
settings.setValue(QStringLiteral("ui/dockState"), dockManager->saveState());
});
}

View File

@ -3,26 +3,22 @@
#include <cmath>
#include <cstddef>
#include <QAbstractItemModel>
#include <QColor>
#include <QEvent>
#include <QIcon>
#include <QListWidget>
#include <QListWidgetItem>
#include <QMouseEvent>
#include <QObject>
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QPixmap>
#include <QString>
#include <QStyledItemDelegate>
#include "Theme.hpp"
#include "model/ColorScale.hpp"
namespace geopro::app {
namespace {
// 颜色块图标边长(像素)。
constexpr int kSwatch = 12;
// 由 localPts 算「位置(质心x)·深(质心y)·尺寸(包络对角)」摘要文本。
// 异常坐标在剖面距离/深度空间(x=距离米, y=深度米)。
QString summarize(const geopro::core::Anomaly& a)
@ -47,128 +43,15 @@ QString summarize(const geopro::core::Anomaly& a)
.arg(span, 0, 'f', 0);
}
// lineColor 字符串 → QColor兼容 "#RRGGBB" 与 "rgba(...)"
QColor barColor(const QString& s)
// lineColor 字符串("#RRGGBB"/"rgba(...)") → 颜色块 QPixmap
QPixmap swatch(const std::string& colorStr)
{
const auto c = geopro::core::parseColor(s.toStdString(), geopro::core::AlphaScale::Bit255);
return QColor(c.r, c.g, c.b);
const auto c = geopro::core::parseColor(colorStr, geopro::core::AlphaScale::Bit255);
QPixmap pm(kSwatch, kSwatch);
pm.fill(QColor(c.r, c.g, c.b));
return pm;
}
// 右侧眼睛命中区(卡片右端,竖直居中)。
QRect anomalyEyeRect(const QRect& itemRect)
{
const QRect r = itemRect.adjusted(4, 2, -4, -2);
const int sz = 22;
return QRect(r.right() - sz - 8, r.center().y() - sz / 2, sz, sz);
}
class AnomalyCardDelegate : public QStyledItemDelegate {
public:
using QStyledItemDelegate::QStyledItemDelegate;
QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const override
{
return QSize(0, 58);
}
bool editorEvent(QEvent* e, QAbstractItemModel* model, const QStyleOptionViewItem& opt,
const QModelIndex& idx) override
{
if (e->type() == QEvent::MouseButtonRelease) {
auto* me = static_cast<QMouseEvent*>(e);
if (anomalyEyeRect(opt.rect).contains(me->position().toPoint())) {
const auto cur = static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt());
model->setData(idx, cur == Qt::Checked ? Qt::Unchecked : Qt::Checked,
Qt::CheckStateRole);
return true; // 吃掉点击:只切显隐,不改选中
}
}
return QStyledItemDelegate::editorEvent(e, model, opt, idx);
}
void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override
{
p->save();
p->setRenderHint(QPainter::Antialiasing, true);
const QRect r = opt.rect.adjusted(4, 3, -4, -3);
const bool selected = opt.state & QStyle::State_Selected;
const bool hover = opt.state & QStyle::State_MouseOver;
// 卡底hover/选中高亮)
if (selected || hover) {
QPainterPath path; path.addRoundedRect(r, 6, 6);
p->fillPath(path, geopro::app::tokenColor(selected ? "bg/selected" : "bg/hover"));
}
// 左 3px 状态色竖条(取异常自身 lineColor
p->fillRect(QRect(r.left(), r.top() + 4, 3, r.height() - 8),
barColor(idx.data(kAnomalyColorRole).toString()));
const QString name = idx.data(Qt::DisplayRole).toString();
const QString type = idx.data(kAnomalyTypeRole).toString();
const QString summary = idx.data(kAnomalySummaryRole).toString();
const int left = r.left() + 14;
const int right = anomalyEyeRect(opt.rect).left() - 8; // 给眼睛留位
const int rowW = right - left;
// 第一行:名称(加粗)
QFont nf = opt.font; nf.setPixelSize(geopro::app::scaledPx(13)); nf.setWeight(QFont::DemiBold);
p->setFont(nf);
p->setPen(geopro::app::tokenColor("text/primary"));
const QRect nameR(left, r.top() + 8, rowW, 20);
p->drawText(nameR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(name, Qt::ElideRight, rowW));
// 第二行:类型胶囊 + 摘要
int x = left;
const int cy = r.top() + 38;
if (!type.isEmpty()) {
QFont pf = opt.font; pf.setPixelSize(geopro::app::scaledPx(11));
p->setFont(pf);
const QFontMetrics fm(pf);
const int tw = fm.horizontalAdvance(type);
const int ph = fm.height() + 2;
const QRect pill(x, cy - ph / 2, tw + 12, ph);
QPainterPath pp; pp.addRoundedRect(pill, ph / 2.0, ph / 2.0);
p->fillPath(pp, geopro::app::tokenColor("bg/hover"));
p->setPen(geopro::app::tokenColor("text/secondary"));
p->drawText(pill, Qt::AlignCenter, type);
x = pill.right() + 8;
}
if (!summary.isEmpty()) {
QFont sf = opt.font; sf.setPixelSize(geopro::app::scaledPx(11));
p->setFont(sf);
p->setPen(geopro::app::tokenColor("text/secondary"));
const QRect sumR(x, cy - 10, right - x, 20);
p->drawText(sumR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(summary, Qt::ElideRight, sumR.width()));
}
// 右侧眼睛(显隐):可见=次要色睁眼;隐藏=禁用色 + 斜杠
const bool visible =
static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt()) == Qt::Checked;
const QColor eyeCol = geopro::app::tokenColor(visible ? "text/secondary" : "text/disabled");
const QRectF eb = anomalyEyeRect(opt.rect);
const QPointF c = eb.center();
const double w = eb.width() * 0.42, h = eb.height() * 0.24;
p->setPen(QPen(eyeCol, 1.4));
p->setBrush(Qt::NoBrush);
QPainterPath eye;
eye.moveTo(c.x() - w, c.y());
eye.quadTo(c.x(), c.y() - h * 2.0, c.x() + w, c.y());
eye.quadTo(c.x(), c.y() + h * 2.0, c.x() - w, c.y());
p->drawPath(eye);
p->setBrush(eyeCol);
p->drawEllipse(c, h * 0.95, h * 0.95);
p->setBrush(Qt::NoBrush);
if (!visible)
p->drawLine(QPointF(c.x() - w, c.y() + h * 1.6), QPointF(c.x() + w, c.y() - h * 1.6));
p->restore();
}
};
} // namespace
void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anomaly>& anomalies)
@ -178,24 +61,16 @@ void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anom
for (std::size_t i = 0; i < anomalies.size(); ++i) {
const auto& a = anomalies[i];
const QString name = QString::fromStdString(a.name.empty() ? "异常" : a.name);
auto* item = new QListWidgetItem(name, list);
const QString type = QString::fromStdString(a.typeName);
QString text = name;
if (!type.isEmpty()) text += QStringLiteral("%1").arg(type);
text += QStringLiteral("\n%1").arg(summarize(a));
auto* item = new QListWidgetItem(QIcon(swatch(a.lineColor)), text, list);
item->setData(kAnomalyIndexRole, static_cast<int>(i));
item->setData(kAnomalyColorRole, QString::fromStdString(a.lineColor));
item->setData(kAnomalyTypeRole, QString::fromStdString(a.typeName));
item->setData(kAnomalySummaryRole, summarize(a));
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(Qt::Checked); // 默认显示
}
}
void applyAnomalyCardDelegate(QListWidget* list)
{
if (!list) return;
list->setItemDelegate(new AnomalyCardDelegate(list));
list->setMouseTracking(true);
list->setSpacing(0);
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, list,
[list]() { list->viewport()->update(); });
}
} // namespace geopro::app

View File

@ -10,14 +10,6 @@ namespace geopro::app {
// 异常索引存于条目的 Qt::UserRole= 在原异常 vector 中的下标,用于显隐映射)。
constexpr int kAnomalyIndexRole = 0x0100; // Qt::UserRole
// 卡片委托读取的结构化角色(避免把数据塞进显示文本)。
constexpr int kAnomalyColorRole = 0x0101; // lineColor 字符串
constexpr int kAnomalyTypeRole = 0x0102; // typeName
constexpr int kAnomalySummaryRole = 0x0103; // 位置·深·尺寸 摘要
// 给异常列表套用卡片委托(左色条+名称+类型标签+摘要+右侧显隐眼睛规范§6.3)。
void applyAnomalyCardDelegate(QListWidget* list);
// 用异常填充 QListWidget对齐原型右上「异常列表」每条目 = 颜色块图标 + 名称 +
// 派生「位置 Xm · 深 Ym · 尺寸 Zm」(由 location.coordinate 质心/包络算)。
// 条目可勾选:勾=显示(默认全勾);勾选状态变化由调用方连接驱动该异常 actor 显隐。

View File

@ -3,13 +3,7 @@
#include <QColor>
#include <QListWidget>
#include <QListWidgetItem>
#include <QObject>
#include <QPainter>
#include <QPainterPath>
#include <QString>
#include <QStyledItemDelegate>
#include "Theme.hpp"
namespace geopro::app {
@ -20,88 +14,6 @@ QString humanSize(long long b) {
if (kb < 1024.0) return QStringLiteral("%1 KB").arg(kb, 0, 'f', 1);
return QStringLiteral("%1 MB").arg(kb / 1024.0, 0, 'f', 1);
}
// 数据/文件列表卡片委托:标题+元信息双行、悬停/选中圆角高亮 + 选中左 2px 强调竖条规范§6.2)。
// 特殊行(加载更多 / 占位提示)退回为居中纯文本,不画卡片。
class DatasetCardDelegate : public QStyledItemDelegate {
public:
using QStyledItemDelegate::QStyledItemDelegate;
QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex& idx) const override {
const bool special =
idx.data(kDsLoadMoreRole).toBool() || !(idx.flags() & Qt::ItemIsSelectable);
return QSize(0, special ? 34 : 52);
}
void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override {
p->save();
p->setRenderHint(QPainter::Antialiasing, true);
const QString disp = idx.data(Qt::DisplayRole).toString();
// 「加载更多」居中强调色文本hover 时加底)。
if (idx.data(kDsLoadMoreRole).toBool()) {
if (opt.state & QStyle::State_MouseOver) {
QPainterPath bgp;
bgp.addRoundedRect(opt.rect.adjusted(4, 2, -4, -2), 6, 6);
p->fillPath(bgp, geopro::app::tokenColor("bg/hover"));
}
p->setPen(geopro::app::tokenColor("accent/primary"));
p->drawText(opt.rect, Qt::AlignCenter, disp);
p->restore();
return;
}
// 占位提示行(不可选):居中淡色文本。
if (!(idx.flags() & Qt::ItemIsSelectable)) {
p->setPen(geopro::app::tokenColor("text/disabled"));
p->drawText(opt.rect, Qt::AlignCenter, disp);
p->restore();
return;
}
// 卡片
const QRect r = opt.rect.adjusted(4, 2, -4, -2);
const bool selected = opt.state & QStyle::State_Selected;
const bool hover = opt.state & QStyle::State_MouseOver;
if (selected || hover) {
QPainterPath path;
path.addRoundedRect(r, 6, 6);
p->fillPath(path, geopro::app::tokenColor(selected ? "bg/selected" : "bg/hover"));
}
if (selected) { // 左 2px 强调竖条规范§6.2
p->fillRect(QRect(r.left(), r.top() + 4, 2, r.height() - 8),
geopro::app::tokenColor("accent/primary"));
}
QString title = disp, meta;
const int nl = disp.indexOf(QLatin1Char('\n'));
if (nl >= 0) {
title = disp.left(nl);
meta = disp.mid(nl + 1);
}
const QRect textR = r.adjusted(14, 6, -12, -6);
// 标题
QFont tf = opt.font;
tf.setPixelSize(geopro::app::scaledPx(13));
p->setFont(tf);
p->setPen(geopro::app::tokenColor("text/primary"));
const QRect titleR(textR.left(), textR.top(), textR.width(), textR.height() / 2);
p->drawText(titleR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(title, Qt::ElideRight, titleR.width()));
// 元信息
if (!meta.isEmpty()) {
QFont mf = opt.font;
mf.setPixelSize(geopro::app::scaledPx(11));
p->setFont(mf);
p->setPen(geopro::app::tokenColor("text/tertiary"));
const QRect metaR(textR.left(), textR.center().y() + 1, textR.width(),
textR.height() / 2);
p->drawText(metaR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(meta, Qt::ElideRight, metaR.width()));
}
p->restore();
}
};
} // namespace
void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append) {
@ -125,6 +37,7 @@ void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>&
if (!append && rows.empty()) {
auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list);
hint->setFlags(Qt::NoItemFlags);
hint->setForeground(QColor("#9AA6B6"));
hint->setTextAlignment(Qt::AlignCenter);
return;
}
@ -140,13 +53,4 @@ void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>&
}
}
void applyDatasetCardDelegate(QListWidget* list) {
if (!list) return;
list->setItemDelegate(new DatasetCardDelegate(list));
list->setMouseTracking(true); // 让委托收到 hover 状态
list->setSpacing(0); // 卡间距由委托内边距控制
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, list,
[list]() { list->viewport()->update(); });
}
} // namespace geopro::app

View File

@ -18,7 +18,4 @@ void populateDatasetList(QListWidget* list, const std::vector<geopro::data::DsRo
// 文件页签:每条 = 文件名 +可读大小UserRole 存 dsId、+2 存文件 url。空时显示占位。
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append);
// 给数据/文件列表套用卡片委托(标题+元信息双行、悬停/选中圆角高亮+左强调竖条规范§6.2)。
void applyDatasetCardDelegate(QListWidget* list);
} // namespace geopro::app

View File

@ -40,6 +40,30 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
tree_->setHeaderHidden(true);
tree_->setIndentation(14); // 收紧缩进
// 清晰复选框:自绘 PNG未选=明显边框空心框,选中=强调色底+白勾),明暗各一套、切换重绘。
// 规避 Fusion 原生复选框在浅底下边框过淡、看不清的问题。
auto applyCheckboxStyle = [this]() {
const bool dark = geopro::app::isDarkTheme();
const QColor border = geopro::app::tokenColor("border/strong"); // 未选复选框描边规范§6.12
const QColor boxBg = geopro::app::tokenColor("bg/panel");
const QColor accent = geopro::app::tokenColor("accent/primary"); // 选中填充
const QString tag = dark ? QStringLiteral("d") : QStringLiteral("l");
const QString off = geopro::app::writeCheckboxIcon(false, border, boxBg, Qt::white, tag);
const QString on = geopro::app::writeCheckboxIcon(true, accent, accent, Qt::white, tag);
// 选中底/字取语义令牌,与全局树/列表选中一致规范§6.1/§10
const QString selBg = geopro::app::token("bg/selected");
const QString selFg = geopro::app::token("text/primary");
tree_->setStyleSheet(QStringLiteral("QTreeView::indicator{ width:16px; height:16px; }"
"QTreeView::indicator:unchecked{ image:url(%1); }"
"QTreeView::indicator:checked{ image:url(%2); }"
"QTreeView::item:selected{ background:%3; color:%4; }"
"QTreeView::item:selected:!active{ background:%3; color:%4; }")
.arg(off, on, selBg, selFg));
};
applyCheckboxStyle();
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, tree_,
[applyCheckboxStyle]() { applyCheckboxStyle(); });
lay->addWidget(tree_, 1);
hint_ = new QLabel(QStringLiteral("正在加载对象…"), this);