geopro/src/app/panels/columns/CategorySection.cpp

266 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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