266 lines
12 KiB
C++
266 lines
12 KiB
C++
#include "panels/columns/CategorySection.hpp"
|
||
|
||
#include <QComboBox>
|
||
#include <QDate>
|
||
#include <map>
|
||
#include <set>
|
||
#include "panels/columns/DateRangeEdit.hpp"
|
||
#include <QHBoxLayout>
|
||
#include <QLabel>
|
||
#include <QMenu>
|
||
#include <QPoint>
|
||
#include <QPushButton>
|
||
#include <QSet>
|
||
#include <QSignalBlocker>
|
||
#include <QToolButton>
|
||
#include <QTreeWidget>
|
||
#include <QTreeWidgetItemIterator>
|
||
#include <QVBoxLayout>
|
||
|
||
#include "Theme.hpp"
|
||
#include "panels/DatasetListPanel.hpp"
|
||
#include "repo/DatasetFieldDictionary.hpp"
|
||
|
||
namespace geopro::app {
|
||
|
||
using geopro::data::DsRow;
|
||
using geopro::data::DsTypeFields;
|
||
|
||
CategorySection::CategorySection(const CategorySpec& spec, geopro::data::DatasetFieldDictionary* dict,
|
||
QWidget* parent)
|
||
: QWidget(parent), spec_(spec), dict_(dict) {
|
||
auto* root = new QVBoxLayout(this);
|
||
root->setContentsMargins(0, 0, 0, 0);
|
||
root->setSpacing(0);
|
||
|
||
// 数据类型标题行(spec §7):折叠箭头+标题(左) | 「+新增三维体」(右,仅反演类,在标题行而非筛选行)。
|
||
auto* headerRow = new QWidget(this);
|
||
auto* hl = new QHBoxLayout(headerRow);
|
||
hl->setContentsMargins(space::kSm, 0, space::kSm, 0);
|
||
hl->setSpacing(space::kSm);
|
||
header_ = new QToolButton(headerRow);
|
||
header_->setText(QString::fromStdString(spec_.title));
|
||
header_->setCheckable(true);
|
||
header_->setChecked(true);
|
||
header_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||
header_->setArrowType(Qt::DownArrow);
|
||
header_->setAutoRaise(true);
|
||
hl->addWidget(header_);
|
||
hl->addStretch(1);
|
||
if (spec_.canGenerateVolume) {
|
||
auto* gen = new QToolButton(headerRow);
|
||
gen->setText(QStringLiteral("+ 新增三维体"));
|
||
gen->setAutoRaise(true);
|
||
connect(gen, &QToolButton::clicked, this, [this] {
|
||
emit generateVolumeRequested(QString::fromStdString(spec_.dsTypeCode), checkedDsIds());
|
||
});
|
||
hl->addWidget(gen);
|
||
}
|
||
root->addWidget(headerRow);
|
||
|
||
body_ = new QWidget(this);
|
||
auto* body = new QVBoxLayout(body_);
|
||
body->setContentsMargins(space::kMd, space::kSm, space::kMd, space::kMd);
|
||
body->setSpacing(space::kSm);
|
||
|
||
// 筛选行:采集时间范围(在前)+ 装置类型(在后,仅 ERT 类)。
|
||
auto* filterRow = new QHBoxLayout();
|
||
dateRange_ = new DateRangeEdit(body_);
|
||
connect(dateRange_, &DateRangeEdit::rangeChanged, this, [this] { rebuildList(); });
|
||
filterRow->addWidget(dateRange_, 1);
|
||
if (spec_.hasArrayTypeFilter) {
|
||
arrayCombo_ = new QComboBox(body_);
|
||
arrayCombo_->addItem(QStringLiteral("全部装置"), QString());
|
||
connect(arrayCombo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||
[this](int) { rebuildList(); });
|
||
filterRow->addWidget(arrayCombo_);
|
||
}
|
||
body->addLayout(filterRow);
|
||
|
||
// 段体:可勾选数据树(勾选=渲染)。复用数据列表卡片委托与 populateDatasetList。
|
||
list_ = new QTreeWidget(body_);
|
||
list_->setHeaderHidden(true);
|
||
list_->setRootIsDecorated(true);
|
||
list_->setIndentation(14); // 紧凑父子缩进(默认 20 太宽)
|
||
applyDatasetCardDelegate(list_);
|
||
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) { emitChecked(); });
|
||
connect(list_, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* it, int) {
|
||
const QString id = it->data(0, kDsIdRole).toString();
|
||
if (id.isEmpty()) return;
|
||
emit detailRequested(id, it->data(0, kDsDdCodeRole).toString(), it->data(0, kDsNameRole).toString());
|
||
});
|
||
if (spec_.id == "voxel") { // 仅三维体段提供右键操作菜单(体/切片/异常)
|
||
list_->setContextMenuPolicy(Qt::CustomContextMenu);
|
||
connect(list_, &QTreeWidget::customContextMenuRequested, this, &CategorySection::showContextMenu);
|
||
}
|
||
body->addWidget(list_, 1);
|
||
|
||
root->addWidget(body_, 1);
|
||
|
||
connect(header_, &QToolButton::toggled, this, [this](bool on) {
|
||
body_->setVisible(on);
|
||
header_->setArrowType(on ? Qt::DownArrow : Qt::RightArrow);
|
||
});
|
||
}
|
||
|
||
void CategorySection::setStructure(const std::vector<geopro::data::StructNode>& nodes) {
|
||
structure_ = nodes; // 容器分层(项目根/GS/TM→ds)在 Task 12 接入真实结构后据此构建。
|
||
}
|
||
|
||
void CategorySection::setDatasets(const std::vector<DsRow>& rows) {
|
||
rows_ = rows;
|
||
refreshArrayCombo();
|
||
rebuildList();
|
||
}
|
||
|
||
void CategorySection::refreshArrayCombo() {
|
||
if (!spec_.hasArrayTypeFilter || !arrayCombo_) return;
|
||
const QString prev = arrayCombo_->currentData().toString();
|
||
const QSignalBlocker block(arrayCombo_);
|
||
arrayCombo_->clear();
|
||
arrayCombo_->addItem(QStringLiteral("全部装置"), QString());
|
||
if (dict_) {
|
||
const auto& en = dict_->arrayTypeEnum(); // 全局装置枚举 itemValue→中文
|
||
QSet<QString> seen;
|
||
// 列出当前数据里出现过、且命中枚举的装置值 → 中文名。
|
||
for (const auto& r : rows_) {
|
||
for (const auto& kv : r.properties) {
|
||
const auto it = en.find(kv.value);
|
||
if (it == en.end()) continue; // 非装置类型值
|
||
const QString val = QString::fromStdString(kv.value);
|
||
if (seen.contains(val)) continue;
|
||
seen.insert(val);
|
||
arrayCombo_->addItem(QString::fromStdString(it->second), val);
|
||
}
|
||
}
|
||
}
|
||
const int idx = arrayCombo_->findData(prev); // 尽量保留上次选择
|
||
arrayCombo_->setCurrentIndex(idx >= 0 ? idx : 0);
|
||
}
|
||
|
||
bool CategorySection::passesFilters(const DsRow& row) const {
|
||
// 装置类型筛选(全局枚举;"全部"=空不筛):ds.properties 任一 value 命中选中 itemValue。
|
||
if (spec_.hasArrayTypeFilter && arrayCombo_) {
|
||
const QString sel = arrayCombo_->currentData().toString();
|
||
if (!sel.isEmpty()) {
|
||
bool hit = false;
|
||
for (const auto& kv : row.properties)
|
||
if (QString::fromStdString(kv.value) == sel) { hit = true; break; }
|
||
if (!hit) return false;
|
||
}
|
||
}
|
||
// 采集时间范围(collectTime 经 dict 取;缺则回退 createTime;空范围不约束)。
|
||
const QDate from = dateRange_ ? dateRange_->from() : QDate();
|
||
const QDate to = dateRange_ ? dateRange_->to() : QDate();
|
||
if (from.isValid() || to.isValid()) {
|
||
const DsTypeFields* f = dict_ ? dict_->fields(spec_.dsTypeCode) : nullptr;
|
||
std::string ts = f ? collectTimeOf(row, *f) : std::string();
|
||
if (ts.empty()) ts = row.createTime;
|
||
const QDate d = QDate::fromString(QString::fromStdString(ts).left(10), QStringLiteral("yyyy-MM-dd"));
|
||
if (d.isValid()) {
|
||
if (from.isValid() && d < from) return false;
|
||
if (to.isValid() && d > to) return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void CategorySection::rebuildList() {
|
||
std::vector<DsRow> filtered;
|
||
filtered.reserve(rows_.size());
|
||
for (const auto& r : rows_)
|
||
if (passesFilters(r)) filtered.push_back(r);
|
||
|
||
// 从项目根的层级树:容器节点(结构,剪枝仅留有 ds 的路径)+ ds(挂 parentId 或 structParentId)。
|
||
std::vector<DsRow> display;
|
||
if (!structure_.empty()) {
|
||
std::map<std::string, const geopro::data::StructNode*> byId;
|
||
for (const auto& n : structure_) byId[n.id] = &n;
|
||
std::set<std::string> keep; // 收集每个 ds 的结构归属向上的祖先链
|
||
for (const auto& d : filtered) {
|
||
std::string p = d.structParentId;
|
||
while (!p.empty() && byId.count(p) && !keep.count(p)) {
|
||
keep.insert(p);
|
||
p = byId[p]->parentId;
|
||
}
|
||
}
|
||
for (const auto& n : structure_)
|
||
if (keep.count(n.id)) {
|
||
DsRow c;
|
||
c.id = n.id;
|
||
c.dsName = n.name;
|
||
c.ddCode = "container";
|
||
c.parentId = n.parentId;
|
||
display.push_back(std::move(c));
|
||
}
|
||
}
|
||
for (const auto& d : filtered) {
|
||
DsRow x = d;
|
||
if (x.parentId.empty()) x.parentId = x.structParentId; // 源 ds / 体 → 挂结构容器
|
||
display.push_back(std::move(x));
|
||
}
|
||
|
||
{
|
||
const QSignalBlocker block(list_);
|
||
populateDatasetList(list_, display, /*append=*/false);
|
||
for (QTreeWidgetItemIterator it(list_); *it; ++it) {
|
||
// 容器节点(项目根/GS/TM)只作层级骨架——明确去掉复选框、不可勾选。
|
||
if ((*it)->data(0, kDsDdCodeRole).toString() == QStringLiteral("container")) {
|
||
(*it)->setFlags((*it)->flags() & ~Qt::ItemIsUserCheckable);
|
||
continue;
|
||
}
|
||
(*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable);
|
||
(*it)->setCheckState(0, Qt::Unchecked);
|
||
}
|
||
}
|
||
list_->expandAll(); // 展开容器层级(项目根/GS/TM),让体/切片/异常可见
|
||
emitChecked(); // 重建后必为空选,清掉上次渲染勾选
|
||
}
|
||
|
||
QStringList CategorySection::checkedDsIds() const {
|
||
QStringList ids;
|
||
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
||
if ((*it)->checkState(0) == Qt::Checked) ids << (*it)->data(0, kDsIdRole).toString();
|
||
return ids;
|
||
}
|
||
|
||
void CategorySection::emitChecked() { emit checkedDatasetsChanged(checkedDsIds()); }
|
||
|
||
void CategorySection::showContextMenu(const QPoint& pos) {
|
||
using geopro::render::interact::SliceAxis;
|
||
QTreeWidgetItem* it = list_->itemAt(pos);
|
||
if (!it) return;
|
||
const QString id = it->data(0, kDsIdRole).toString();
|
||
const QString ddCode = it->data(0, kDsDdCodeRole).toString();
|
||
if (id.isEmpty() || ddCode == QStringLiteral("container")) return; // 容器节点无操作菜单
|
||
const QString name = it->data(0, kDsNameRole).toString();
|
||
QMenu menu(this);
|
||
menu.addAction(QStringLiteral("详情"), this,
|
||
[this, id, ddCode, name] { emit detailRequested(id, ddCode, name); });
|
||
if (ddCode == QStringLiteral("dd_voxel")) { // 三维体
|
||
QMenu* sl = menu.addMenu(QStringLiteral("生成切片"));
|
||
sl->addAction(QStringLiteral("上下"), this, [this] { emit sliceRequested(SliceAxis::UpDown); });
|
||
sl->addAction(QStringLiteral("前后"), this, [this] { emit sliceRequested(SliceAxis::FrontBack); });
|
||
sl->addAction(QStringLiteral("左右"), this, [this] { emit sliceRequested(SliceAxis::LeftRight); });
|
||
sl->addAction(QStringLiteral("任意"), this, [this] { emit sliceRequested(SliceAxis::Oblique); });
|
||
menu.addAction(QStringLiteral("色阶…"), this, [this, id] { emit colorScaleRequested(id); });
|
||
} else if (ddCode == QStringLiteral("dd_slice")) { // 切片
|
||
menu.addAction(QStringLiteral("保存位姿"), this, [this, id] { emit sliceSaveRequested(id); });
|
||
menu.addAction(QStringLiteral("另存为…"), this, [this, id] { emit sliceSaveAsRequested(id); });
|
||
QMenu* ex = menu.addMenu(QStringLiteral("导出"));
|
||
ex->addAction(QStringLiteral("图片"), this, [this, id] { emit sliceExportImageRequested(id); });
|
||
ex->addAction(QStringLiteral("dat"), this, [this, id] { emit sliceExportDatRequested(id); });
|
||
menu.addAction(QStringLiteral("色阶…"), this, [this, id] { emit colorScaleRequested(id); });
|
||
menu.addSeparator();
|
||
menu.addAction(QStringLiteral("删除"), this, [this, id, ddCode] { emit deleteDatasetRequested(id, ddCode); });
|
||
} else if (ddCode == QStringLiteral("dd_anomaly")) { // 异常
|
||
menu.addAction(QStringLiteral("显示"), this, [this, id] { emit anomalyVisibilityChanged(id, true); });
|
||
menu.addAction(QStringLiteral("隐藏"), this, [this, id] { emit anomalyVisibilityChanged(id, false); });
|
||
menu.addSeparator();
|
||
menu.addAction(QStringLiteral("删除"), this, [this, id, ddCode] { emit deleteDatasetRequested(id, ddCode); });
|
||
}
|
||
menu.exec(list_->viewport()->mapToGlobal(pos));
|
||
}
|
||
|
||
} // namespace geopro::app
|