266 lines
11 KiB
C++
266 lines
11 KiB
C++
#include "panels/AnomalyListPanel.hpp"
|
||
|
||
#include <cmath>
|
||
#include <cstddef>
|
||
|
||
#include <QAbstractItemModel>
|
||
#include <QColor>
|
||
#include <QEvent>
|
||
#include <QListWidget>
|
||
#include <QListWidgetItem>
|
||
#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 {
|
||
|
||
// 由 localPts 算「位置(质心x)·深(质心y)·尺寸(包络对角)」摘要文本。
|
||
// 异常坐标在剖面距离/深度空间(x=距离米, y=深度米)。
|
||
QString summarize(const geopro::core::Anomaly& a)
|
||
{
|
||
if (a.localPts.empty()) return QStringLiteral("(无几何)");
|
||
double cx = 0.0, cy = 0.0;
|
||
double minx = a.localPts[0].x, maxx = a.localPts[0].x;
|
||
double miny = a.localPts[0].y, maxy = a.localPts[0].y;
|
||
for (const auto& p : a.localPts) {
|
||
cx += p.x; cy += p.y;
|
||
if (p.x < minx) minx = p.x;
|
||
if (p.x > maxx) maxx = p.x;
|
||
if (p.y < miny) miny = p.y;
|
||
if (p.y > maxy) maxy = p.y;
|
||
}
|
||
const auto n = static_cast<double>(a.localPts.size());
|
||
cx /= n; cy /= n;
|
||
const double span = std::hypot(maxx - minx, maxy - miny);
|
||
return QStringLiteral("位置 %1m · 深 %2m · 尺寸 %3m")
|
||
.arg(cx, 0, 'f', 0)
|
||
.arg(cy, 0, 'f', 0)
|
||
.arg(span, 0, 'f', 0);
|
||
}
|
||
|
||
// lineColor 字符串 → QColor(兼容 "#RRGGBB" 与 "rgba(...)")。
|
||
QColor barColor(const QString& s)
|
||
{
|
||
const auto c = geopro::core::parseColor(s.toStdString(), geopro::core::AlphaScale::Bit255);
|
||
return QColor(c.r, c.g, c.b);
|
||
}
|
||
|
||
// 异常分级 → 状态语义键(规范 §1.4/§6.3/§8.3:高=Danger 中=Warning 低=Info 未知=Neutral)。
|
||
// Anomaly 数据模型不带显式 level 字段,故按可得信号稳健推断:
|
||
// 1) typeName/name 含「高/中/低」字样 → 直接定级;
|
||
// 2) 否则按 lineColor 色相归类(红→高 橙/黄→中 蓝→低,与右栏列表/三维标注牌同色);
|
||
// 3) 仍无法判定 → Neutral(停用/未知),避免乱给状态色。
|
||
// 返回值为状态 token 前缀("danger"/"warning"/"info"/"neutral"),调用方据此拼 token 名。
|
||
QString anomalyStatus(const geopro::core::Anomaly& a)
|
||
{
|
||
const QString tag = QString::fromStdString(a.typeName + a.name);
|
||
if (tag.contains(QStringLiteral("高"))) return QStringLiteral("danger");
|
||
if (tag.contains(QStringLiteral("中"))) return QStringLiteral("warning");
|
||
if (tag.contains(QStringLiteral("低"))) return QStringLiteral("info");
|
||
|
||
// 按 lineColor 色相归类(HSV 色相环:红≈0/360 橙黄≈20–70 蓝≈190–260)。
|
||
const QColor c = barColor(QString::fromStdString(a.lineColor));
|
||
if (c.isValid() && c.saturationF() > 0.25) {
|
||
const int h = c.hue(); // -1=无色相(灰)
|
||
if (h >= 0) {
|
||
if (h < 20 || h >= 330) return QStringLiteral("danger");
|
||
if (h < 75) return QStringLiteral("warning");
|
||
if (h >= 185 && h < 265) return QStringLiteral("info");
|
||
}
|
||
}
|
||
return QStringLiteral("neutral");
|
||
}
|
||
|
||
// 状态键 + 后缀 → 主题 token 名(如 status + "danger" + "-bg")。neutral 无 -bg,回落中性面。
|
||
QColor statusColor(const QString& status, bool bg)
|
||
{
|
||
if (status == QStringLiteral("neutral"))
|
||
return geopro::app::tokenColor(bg ? "bg/panel-subtle" : "status/neutral");
|
||
const QString name = QStringLiteral("status/%1%2").arg(status, bg ? QStringLiteral("-bg") : QString());
|
||
return geopro::app::tokenColor(name.toUtf8().constData());
|
||
}
|
||
|
||
// 右侧眼睛命中区(卡片右端,竖直居中)。
|
||
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;
|
||
|
||
// 分级状态键(§6.3):高=Danger 中=Warning 低=Info 未知=Neutral。
|
||
const QString status = idx.data(kAnomalyStatusRole).toString();
|
||
|
||
// 卡底(§6.3 规范§3.2 radius/md=6):静止态 = 该分级状态浅底;选中/hover 叠一档
|
||
// 交互态(选中=bg/selected 强调底,hover=bg/hover),让交互可辨又不丢分级语义。
|
||
QPainterPath path; path.addRoundedRect(r, geopro::app::radius::kMd, geopro::app::radius::kMd);
|
||
const QColor cardBg = selected ? geopro::app::tokenColor("bg/selected")
|
||
: hover ? geopro::app::tokenColor("bg/hover")
|
||
: statusColor(status, /*bg=*/true);
|
||
p->fillPath(path, cardBg);
|
||
|
||
// 左 3px 状态色竖条(取分级状态主色,与卡底/标签同源;§6.3)
|
||
p->fillRect(QRect(r.left(), r.top() + 4, 3, r.height() - 8),
|
||
statusColor(status, /*bg=*/false));
|
||
|
||
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;
|
||
|
||
// 第一行:状态圆点 + 名称(text/body-strong 600)。圆点 8px,色 = 分级状态主色(§6.3)。
|
||
const int dot = 8;
|
||
const QRect nameR(left + dot + 6, r.top() + 8, rowW - dot - 6, 20);
|
||
p->setBrush(statusColor(status, /*bg=*/false));
|
||
p->setPen(Qt::NoPen);
|
||
p->drawEllipse(QPointF(left + dot / 2.0, nameR.center().y()), dot / 2.0, dot / 2.0);
|
||
QFont nf = opt.font; nf.setPixelSize(geopro::app::scaledPx(13)); nf.setWeight(QFont::DemiBold);
|
||
p->setFont(nf);
|
||
p->setPen(geopro::app::tokenColor("text/primary"));
|
||
p->setBrush(Qt::NoBrush);
|
||
p->drawText(nameR, Qt::AlignLeft | Qt::AlignVCenter,
|
||
p->fontMetrics().elidedText(name, Qt::ElideRight, nameR.width()));
|
||
|
||
// 第二行:分级胶囊标签 + 摘要
|
||
int x = left;
|
||
const int cy = r.top() + 38;
|
||
if (!type.isEmpty()) {
|
||
// 等级标签(§6.8):胶囊 radius/pill(按高度算半径),底 = 状态浅底,文字 = 状态主色。
|
||
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, statusColor(status, /*bg=*/true));
|
||
p->setPen(statusColor(status, /*bg=*/false));
|
||
p->drawText(pill, Qt::AlignCenter, type);
|
||
x = pill.right() + 8;
|
||
}
|
||
if (!summary.isEmpty()) {
|
||
// 属性行数值(§2.1/§6.3):用等宽字族,保证「140m · 18m / 32 Ω·m」逐列对齐。
|
||
QFont sf = opt.font;
|
||
sf.setFamilies(QString::fromLatin1(geopro::app::type::kMonoFamily).split(QStringLiteral(", ")));
|
||
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)
|
||
{
|
||
if (!list) return;
|
||
list->clear();
|
||
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);
|
||
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->setData(kAnomalyStatusRole, anomalyStatus(a)); // 分级状态键(驱动卡底/标签/竖条同色)
|
||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||
item->setCheckState(Qt::Checked); // 默认显示
|
||
}
|
||
}
|
||
|
||
QString anomalyVisibleCountText(QListWidget* list)
|
||
{
|
||
if (!list) return QStringLiteral("0/0");
|
||
int total = 0, visible = 0;
|
||
for (int i = 0; i < list->count(); ++i) {
|
||
const QListWidgetItem* it = list->item(i);
|
||
if (!it) continue;
|
||
++total;
|
||
if (it->checkState() == Qt::Checked) ++visible;
|
||
}
|
||
return QStringLiteral("%1/%2").arg(visible).arg(total);
|
||
}
|
||
|
||
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
|