Compare commits
10 Commits
824e8bdf62
...
d1be0567de
| Author | SHA1 | Date |
|---|---|---|
|
|
d1be0567de | |
|
|
9680fefbe3 | |
|
|
3ccb8df4ed | |
|
|
c953b35334 | |
|
|
9e80b2fea1 | |
|
|
5f02d494dc | |
|
|
2a666663e7 | |
|
|
b78969471e | |
|
|
8f31f043df | |
|
|
b26dcc1ca7 |
|
|
@ -11,6 +11,7 @@
|
|||
#include <QPixmap>
|
||||
#include <QPointF>
|
||||
#include <QRectF>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QSvgRenderer>
|
||||
|
||||
|
|
@ -98,7 +99,7 @@ QString svgPathFor(Glyph t)
|
|||
|
||||
} // namespace
|
||||
|
||||
QIcon makeGlyph(Glyph type, const QColor& color, int px)
|
||||
QIcon makeGlyph(Glyph type, const QColor& color, int px, int padRight)
|
||||
{
|
||||
const QString svg =
|
||||
QStringLiteral("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' "
|
||||
|
|
@ -110,12 +111,13 @@ QIcon makeGlyph(Glyph type, const QColor& color, int px)
|
|||
|
||||
// 以 3x 超采样渲染再设 devicePixelRatio,保证在任意缩放/DPI 下都清晰。
|
||||
constexpr qreal kSuper = 3.0;
|
||||
const int dim = qRound(px * kSuper);
|
||||
QImage img(dim, dim, QImage::Format_ARGB32_Premultiplied);
|
||||
const int dim = qRound(px * kSuper); // 图标本体边长(方形)
|
||||
const int dimW = qRound((px + padRight) * kSuper); // 含右透明内边距的画布宽
|
||||
QImage img(dimW, 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));
|
||||
renderer.render(&p, QRectF(0, 0, dim, dim)); // 图标居左方形渲染,右侧 padRight 留透明
|
||||
p.end();
|
||||
|
||||
QPixmap pm = QPixmap::fromImage(img);
|
||||
|
|
@ -214,10 +216,13 @@ 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)
|
||||
void setThemedGlyph(QAbstractButton* button, Glyph type, int px, int padRight)
|
||||
{
|
||||
if (!button) return;
|
||||
auto apply = [button, type, px]() { button->setIcon(makeGlyph(type, themedIconColor(), px)); };
|
||||
auto apply = [button, type, px, padRight]() {
|
||||
button->setIcon(makeGlyph(type, themedIconColor(), px, padRight));
|
||||
if (padRight > 0) button->setIconSize(QSize(px + padRight, px)); // 含右内边距,文字被右推
|
||||
};
|
||||
apply();
|
||||
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, button, [apply]() { apply(); });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,13 +34,19 @@ enum class Glyph {
|
|||
Gear, // 设置(齿轮)
|
||||
};
|
||||
|
||||
// 生成指定颜色、像素尺寸的图标(默认 16px,内部按 2x 绘制保证清晰)。
|
||||
QIcon makeGlyph(Glyph type, const QColor& color, int px = 16);
|
||||
// 「图标+文字」按钮的图标→文字间距补丁:Fusion 内置约 4px,本值补到规范 §6.7 的 6px。
|
||||
// 做法:把图标渲染进“px 宽图标 + padRight 透明右边距”的画布,文字被这段透明区右推。
|
||||
inline constexpr int kGlyphTextGapPad = 2;
|
||||
|
||||
// 随 ElaTheme 明暗自动着色的 glyph(取主题文本色:暗色用浅色、亮色用深色),主题切换时自动重绘。
|
||||
// 生成指定颜色、像素尺寸的图标(默认 16px,内部按 3x 绘制保证清晰)。
|
||||
// padRight>0 时图标画布右侧留透明内边距(用于「图标+文字」按钮统一间距),图标本体仍为 px×px 居左。
|
||||
QIcon makeGlyph(Glyph type, const QColor& color, int px = 16, int padRight = 0);
|
||||
|
||||
// 随主题明暗自动着色的 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);
|
||||
void setThemedGlyph(QAbstractButton* button, Glyph type, int px, int padRight = 0);
|
||||
|
||||
// 生成树展开/折叠箭头 PNG 到临时目录,返回文件路径(供树的 QSS `image: url(...)` 引用)。
|
||||
// 配合 QTreeView::branch 背景使用,可让选中高亮不覆盖左侧缩进/箭头列且不丢箭头。
|
||||
|
|
|
|||
|
|
@ -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(8);
|
||||
lay->setSpacing(geopro::app::space::kSm);
|
||||
|
||||
auto* iconLbl = new QLabel(header);
|
||||
setThemedGlyph(iconLbl, icon, kTitleIcon); // 随主题着色(暗色下也清晰)
|
||||
|
|
@ -131,8 +131,7 @@ 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); // 随主题着色
|
||||
btn->setIconSize(QSize(kTabIcon, kTabIcon));
|
||||
setThemedGlyph(btn, t.icon, kTabIcon, kGlyphTextGapPad); // 随主题着色 + 图标→文字6px(§6.7)
|
||||
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
btn->setCheckable(true);
|
||||
btn->setCursor(Qt::PointingHandCursor);
|
||||
|
|
|
|||
|
|
@ -28,11 +28,11 @@ QString statusText(int s) {
|
|||
}
|
||||
|
||||
// 状态语义色(寻路):未开始=弱化中性、进行中=信息蓝(活动中);未知状态用中性灰。
|
||||
const char* statusColorHex(int s) {
|
||||
QColor statusColor(int s) {
|
||||
switch (s) {
|
||||
case 1: return "#8A93A3"; // 未开始:弱化
|
||||
case 2: return semantic::kInfo; // 进行中:活动中
|
||||
default: return "#5A6B85"; // 未知:中性
|
||||
case 1: return tokenColor("text/tertiary"); // 未开始:弱化
|
||||
case 2: return tokenColor("accent/primary"); // 进行中:活动中
|
||||
default: return tokenColor("text/secondary"); // 未知:中性
|
||||
}
|
||||
}
|
||||
} // 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(QColor("#2D6CB5"));
|
||||
nameItem->setForeground(tokenColor("accent/primary"));
|
||||
table_->setItem(i, 1, nameItem);
|
||||
set(2, QString::fromStdString(p.code));
|
||||
// 状态列语义着色:颜色承载“未开始/进行中”分类,进行中加粗强调(不只靠颜色)。
|
||||
auto* statusItem = new QTableWidgetItem(statusText(p.status));
|
||||
statusItem->setForeground(QColor(statusColorHex(p.status)));
|
||||
statusItem->setForeground(statusColor(p.status));
|
||||
if (p.status == 2) {
|
||||
QFont f = statusItem->font();
|
||||
f.setBold(true);
|
||||
|
|
|
|||
|
|
@ -74,8 +74,8 @@ QWidget* buildAppearancePage() {
|
|||
rlay->setContentsMargins(96 + 12, 0, 0, 0); // 与控件列对齐
|
||||
rlay->setSpacing(10);
|
||||
auto* hint = new QLabel(QStringLiteral("界面字号将在重启后生效"), restartRow);
|
||||
geopro::app::applyThemedStyleSheet(
|
||||
hint, QStringLiteral("color:#5A6B85; font-size:%1px;")
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
hint, QStringLiteral("color:{{text/secondary}}; font-size:%1px;")
|
||||
.arg(geopro::app::scaledPx(geopro::app::type::kCaption)));
|
||||
auto* restartBtn = new QPushButton(QStringLiteral("立即重启"), restartRow);
|
||||
rlay->addWidget(hint);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#include "Theme.hpp"
|
||||
|
||||
#include "Glyphs.hpp"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
|
|
@ -155,6 +157,11 @@ 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 同类陷阱),父节点折叠图标会消失。 */
|
||||
|
||||
|
|
@ -449,64 +456,34 @@ ads--CDockWidgetTab[activeTab="true"] QLabel {
|
|||
}
|
||||
)QSS";
|
||||
|
||||
// ── 主题桥配色:工作台标准控件的 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)
|
||||
// 全局复选指示器:用 writeCheckboxIcon 生成清晰复选框 PNG(未选=明显边框空心框,
|
||||
// 选中=强调色填充+白勾),统一作用于 QCheckBox 与 树/列表的勾选指示器。规避 Fusion
|
||||
// 原生复选框在浅底下边框过淡看不清的问题——全 UI 一套,避免逐控件打补丁。
|
||||
QString indicatorQss(bool dark)
|
||||
{
|
||||
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;
|
||||
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*/)
|
||||
{
|
||||
return fillTokens(QString::fromUtf8(kStyleSheet));
|
||||
const bool dark = isDarkTheme();
|
||||
return fillTokens(QString::fromUtf8(kStyleSheet)) + indicatorQss(dark);
|
||||
}
|
||||
|
||||
// 调色板同样取自 ElaTheme,让无 QSS 覆盖处的标准控件也与外壳一致。
|
||||
|
|
@ -662,11 +639,6 @@ 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。
|
||||
|
|
@ -676,14 +648,6 @@ 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)); }
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class QWidget;
|
|||
namespace geopro::app {
|
||||
|
||||
// 主题管理器(纯 Qt,替代 ElaTheme):持有当前明暗 + 是否跟随系统;切换发 changed() 信号,
|
||||
// 全 UI(全局 QSS 由 main 重应用、内联 chrome 由 applyThemedStyleSheet)据此热切重着色。
|
||||
// 全 UI(全局 QSS 由 main 重应用、内联 chrome 由 applyTokenizedStyleSheet)据此热切重着色。
|
||||
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 结构,仅按 kDarkMap 换色;幂等,可随主题切换重复调用。
|
||||
// 暗色复用同一 QSS 结构,颜色全由 kTokens 双值(fillTokens/tokenHex)驱动;幂等,可随主题切换重复调用。
|
||||
void applyThemeMode(QApplication& app, bool dark);
|
||||
|
||||
// 浅色主题快捷入口(= applyThemeMode(app,false))。经典壳启动调用一次。
|
||||
|
|
@ -128,15 +128,6 @@ bool isDarkTheme();
|
|||
// VTK 渲染器背景色(随当前主题,取 ElaTheme 窗口底色)。写入 r/g/b(0–1)。
|
||||
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" 以便一眼发现漏配)
|
||||
|
|
|
|||
|
|
@ -3,15 +3,20 @@
|
|||
#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"
|
||||
|
|
@ -50,6 +55,29 @@ 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)
|
||||
{
|
||||
|
|
@ -132,16 +160,24 @@ 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 12px;"
|
||||
"#wsSwitcher { color:{{text/primary}}; border:none; border-radius:8px; padding:8px 26px 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; }"
|
||||
|
|
@ -151,17 +187,17 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
|||
.arg(scaledPx(type::kLabel))
|
||||
.arg(type::kWeightSemibold)
|
||||
.arg(scaledPx(type::kCaption))
|
||||
.arg(scaledPx(type::kTitle)));
|
||||
.arg(scaledPx(type::kTitle))
|
||||
.arg(chevron));
|
||||
|
||||
auto* lay = new QHBoxLayout(this);
|
||||
lay->setContentsMargins(14, 0, 14, 0);
|
||||
lay->setSpacing(0);
|
||||
|
||||
// 工作空间切换器(QToolButton + 主题化 QSS;下拉箭头用文字"▾"保证清晰;数据驱动)。
|
||||
// 工作空间切换器(QToolButton + 主题化 QSS;下拉箭头用高清 chevron menu-indicator;数据驱动)。
|
||||
wsBtn_ = new QToolButton(this);
|
||||
wsBtn_->setObjectName(QStringLiteral("wsSwitcher"));
|
||||
setThemedGlyph(wsBtn_, Glyph::Workspace, kWorkspaceIcon); // 中性主题色(蓝只留给选中/激活)
|
||||
wsBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon));
|
||||
setThemedGlyph(wsBtn_, Glyph::Workspace, kWorkspaceIcon, kGlyphTextGapPad); // 图标→文字6px(§6.7)
|
||||
wsBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
wsBtn_->setPopupMode(QToolButton::InstantPopup);
|
||||
wsBtn_->setCursor(Qt::PointingHandCursor);
|
||||
|
|
@ -176,8 +212,7 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
|||
// 项目切换器(QToolButton + 主题化 QSS;数据驱动)。
|
||||
projBtn_ = new QToolButton(this);
|
||||
projBtn_->setObjectName(QStringLiteral("wsSwitcher"));
|
||||
setThemedGlyph(projBtn_, Glyph::Folder, kWorkspaceIcon); // 中性主题色
|
||||
projBtn_->setIconSize(QSize(kWorkspaceIcon, kWorkspaceIcon));
|
||||
setThemedGlyph(projBtn_, Glyph::Folder, kWorkspaceIcon, kGlyphTextGapPad); // 中性主题色 + 图标→文字6px
|
||||
projBtn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
projBtn_->setPopupMode(QToolButton::InstantPopup);
|
||||
projBtn_->setCursor(Qt::PointingHandCursor);
|
||||
|
|
@ -197,31 +232,65 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) {
|
|||
lay->addWidget(makeDivider(this));
|
||||
lay->addSpacing(12);
|
||||
|
||||
// 用户区:头像可点击 → 弹出菜单(退出登录)。
|
||||
auto* avatar = new QToolButton(this);
|
||||
avatar->setObjectName(QStringLiteral("avatar"));
|
||||
avatar->setText(QStringLiteral("ZL"));
|
||||
avatar->setFixedSize(34, 34);
|
||||
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);
|
||||
// 用户区:头像(圆形,竖直居中) + 右侧 姓名(上)/职务(下) 左对齐 + 下拉箭头;整块可点 → 菜单。
|
||||
// 用普通 QWidget + eventFilter:QWidget 按子布局正确撑开(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* userBox = new QWidget(this);
|
||||
auto* userLay = new QVBoxLayout(userBox);
|
||||
userLay->setContentsMargins(0, 0, 0, 0);
|
||||
userLay->setSpacing(0);
|
||||
auto* userName = new QLabel(QStringLiteral("张磊"), userBox);
|
||||
auto* avatar = new QLabel(userRow_);
|
||||
avatar->setPixmap(
|
||||
renderAvatar(QStringLiteral("ZL"), 34, geopro::app::tokenColor("accent/primary"), Qt::white));
|
||||
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("高级工程师"), userBox);
|
||||
auto* userRole = new QLabel(QStringLiteral("高级工程师"), nameBox);
|
||||
userRole->setObjectName(QStringLiteral("userRole"));
|
||||
userLay->addWidget(userName);
|
||||
userLay->addWidget(userRole);
|
||||
lay->addWidget(userBox);
|
||||
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,
|
||||
[this] { emit logoutRequested(); });
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QString& currentId) {
|
||||
|
|
@ -241,7 +310,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 + QStringLiteral(" ▾")); // 立即反馈
|
||||
wsBtn_->setText(name); // 立即反馈
|
||||
emit workspaceSwitchRequested(id);
|
||||
});
|
||||
}
|
||||
|
|
@ -250,8 +319,7 @@ void TopBar::setWorkspaces(const std::vector<data::Workspace>& list, const QStri
|
|||
none->setEnabled(false);
|
||||
}
|
||||
wsBtn_->setMenu(menu);
|
||||
wsBtn_->setText((currentName.isEmpty() ? QStringLiteral("选择空间") : currentName) +
|
||||
QStringLiteral(" ▾"));
|
||||
wsBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择空间") : currentName);
|
||||
}
|
||||
|
||||
void TopBar::setProjects(const std::vector<data::ProjectSummary>& list, const QString& currentId,
|
||||
|
|
@ -272,7 +340,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 + QStringLiteral(" ▾"));
|
||||
projBtn_->setText(name);
|
||||
emit projectSwitchRequested(id);
|
||||
});
|
||||
}
|
||||
|
|
@ -286,12 +354,11 @@ 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) +
|
||||
QStringLiteral(" ▾"));
|
||||
projBtn_->setText(currentName.isEmpty() ? QStringLiteral("选择项目") : currentName);
|
||||
}
|
||||
|
||||
void TopBar::setProjectButtonText(const QString& name) {
|
||||
projBtn_->setText(name + QStringLiteral(" ▾"));
|
||||
projBtn_->setText(name);
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
#include "repo/RepoTypes.hpp"
|
||||
|
||||
class QToolButton;
|
||||
class QEvent;
|
||||
class QMenu;
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
|
|
@ -21,6 +23,9 @@ 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);
|
||||
|
|
@ -31,6 +36,8 @@ signals:
|
|||
private:
|
||||
QToolButton* wsBtn_ = nullptr;
|
||||
QToolButton* projBtn_ = nullptr;
|
||||
QWidget* userRow_ = nullptr; // 用户区整块(头像+姓名/职务+箭头)
|
||||
QMenu* userMenu_ = nullptr; // 用户下拉菜单
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -89,17 +89,17 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
|
|||
// 字号引用 Theme 排版令牌:品牌名=display(24)、副标题/字段标签=caption(12)。
|
||||
// 登录窗整体随 ElaTheme 着色(与 Ela 化的输入/按钮一致,避免暗系统下浅窗+暗控件割裂)。
|
||||
// 品牌带文字用 white 关键字(不入角色映射→恒为白),保证落在蓝色横幅上始终可读。
|
||||
geopro::app::applyThemedStyleSheet(
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
this, QStringLiteral(
|
||||
"QDialog { background: #F4F6FA; }"
|
||||
"QDialog { background: {{bg/app}}; }"
|
||||
"#headerBand {"
|
||||
" background: qlineargradient(x1:0, y1:0, x2:1, y2:1,"
|
||||
" stop:0 #2D6CB5, stop:1 #234F87); }"
|
||||
"#brandTitle { color: white; font-size: %1px; font-weight: %2; }"
|
||||
" stop:0 {{accent/primary}}, stop:1 {{accent/primary-pressed}}); }"
|
||||
"#brandTitle { color: {{text/on-primary}}; font-size: %1px; font-weight: %2; }"
|
||||
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: %3px; }"
|
||||
"#fieldLabel { color: #5A6B85; font-size: %4px; font-weight: %5; }"
|
||||
"#fieldLabel { color: {{text/secondary}}; font-size: %4px; font-weight: %5; }"
|
||||
// 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。
|
||||
"#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }")
|
||||
"#captchaImg { border: 1px solid {{border/strong}}; border-radius: 8px; background: {{bg/hover}}; }")
|
||||
.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::applyThemedStyleSheet(
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
refreshBtn_,
|
||||
QStringLiteral(
|
||||
"QPushButton { color: #2D6CB5; border: none; background: transparent; padding: 2px 0; }"
|
||||
"QPushButton:hover { color: #234F87; text-decoration: underline; }"));
|
||||
"QPushButton { color: {{accent/primary}}; border: none; background: transparent; padding: 2px 0; }"
|
||||
"QPushButton:hover { color: {{accent/primary-pressed}}; 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::applyThemedStyleSheet(
|
||||
errorLabel_, QStringLiteral("color: #C0392B; font-size: %1px;").arg(scaledPx(type::kCaption)));
|
||||
geopro::app::applyTokenizedStyleSheet(
|
||||
errorLabel_, QStringLiteral("color: {{status/danger}}; font-size: %1px;").arg(scaledPx(type::kCaption)));
|
||||
errorLabel_->setWordWrap(true);
|
||||
errorLabel_->setMinimumHeight(18);
|
||||
form->addWidget(errorLabel_);
|
||||
|
|
|
|||
|
|
@ -330,6 +330,11 @@ 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);
|
||||
|
|
@ -482,6 +487,8 @@ 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);
|
||||
|
|
@ -517,40 +524,23 @@ 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();
|
||||
applyListSelection(datasetList);
|
||||
geopro::app::applyDatasetCardDelegate(datasetList);
|
||||
datasetTabs->addTab(datasetList, QStringLiteral("数据"));
|
||||
auto* fileList = new QListWidget();
|
||||
applyListSelection(fileList);
|
||||
geopro::app::applyDatasetCardDelegate(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);
|
||||
|
|
@ -560,14 +550,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
|
||||
// 右上 dock:异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。
|
||||
auto* anomalyList = new QListWidget();
|
||||
applyListSelection(anomalyList);
|
||||
geopro::app::applyAnomalyCardDelegate(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("添加异常")}});
|
||||
|
|
@ -582,7 +572,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);
|
||||
|
||||
|
|
@ -591,9 +581,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);
|
||||
|
||||
// 固定全部面板(对齐原型):移除 关闭/浮动/拖动/钉住 等子窗口操作,仅保留分隔条调整边界。
|
||||
|
|
@ -896,7 +886,6 @@ 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;
|
||||
};
|
||||
|
|
@ -946,7 +935,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("文件"));
|
||||
});
|
||||
|
|
@ -957,7 +946,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("数据"));
|
||||
|
|
@ -1010,7 +999,9 @@ 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);
|
||||
const QByteArray dockState = settings.value(QStringLiteral("ui/dockState")).toByteArray();
|
||||
// 注意:ADS 按 dock 唯一名作键。改过 dock 名后旧布局会失配 → bump 此键丢弃旧布局,
|
||||
// 回落到下方 addDockWidget 的默认排布(再改 dock 名时同样要 bump 版本号)。
|
||||
const QByteArray dockState = settings.value(QStringLiteral("ui/dockState_v2")).toByteArray();
|
||||
if (!dockState.isEmpty()) {
|
||||
dockManager->restoreState(dockState);
|
||||
// restoreState 重建停靠区会重新显示 ADS 标题栏,再隐藏一次保持“无双标题”。
|
||||
|
|
@ -1021,7 +1012,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"), dockManager->saveState());
|
||||
settings.setValue(QStringLiteral("ui/dockState_v2"), dockManager->saveState());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,22 +3,26 @@
|
|||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QColor>
|
||||
#include <QIcon>
|
||||
#include <QEvent>
|
||||
#include <QListWidget>
|
||||
#include <QListWidgetItem>
|
||||
#include <QPixmap>
|
||||
#include <QMouseEvent>
|
||||
#include <QObject>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPen>
|
||||
#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)
|
||||
|
|
@ -43,15 +47,128 @@ QString summarize(const geopro::core::Anomaly& a)
|
|||
.arg(span, 0, 'f', 0);
|
||||
}
|
||||
|
||||
// lineColor 字符串("#RRGGBB"/"rgba(...)") → 颜色块 QPixmap。
|
||||
QPixmap swatch(const std::string& colorStr)
|
||||
// lineColor 字符串 → QColor(兼容 "#RRGGBB" 与 "rgba(...)")。
|
||||
QColor barColor(const QString& s)
|
||||
{
|
||||
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;
|
||||
const auto c = geopro::core::parseColor(s.toStdString(), geopro::core::AlphaScale::Bit255);
|
||||
return QColor(c.r, c.g, c.b);
|
||||
}
|
||||
|
||||
// 右侧眼睛命中区(卡片右端,竖直居中)。
|
||||
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)
|
||||
|
|
@ -61,16 +178,24 @@ 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);
|
||||
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);
|
||||
auto* item = new QListWidgetItem(name, 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
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ 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 显隐。
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@
|
|||
#include <QColor>
|
||||
#include <QListWidget>
|
||||
#include <QListWidgetItem>
|
||||
#include <QObject>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QString>
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
|
|
@ -14,6 +20,88 @@ 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) {
|
||||
|
|
@ -37,7 +125,6 @@ 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;
|
||||
}
|
||||
|
|
@ -53,4 +140,13 @@ 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
|
||||
|
|
|
|||
|
|
@ -18,4 +18,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -40,30 +40,6 @@ 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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue