geopro/src/app/panels/ObjectTreePanel.cpp

304 lines
14 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/ObjectTreePanel.hpp"
#include <QEvent>
#include <QLabel>
#include <QMenu>
#include <QModelIndex>
#include <QMouseEvent>
#include <QPoint>
#include <QSignalBlocker>
#include <QStringList>
#include <QStyle>
#include <QStyleOptionViewItem>
#include <QTimer>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <functional>
#include "Glyphs.hpp"
#include "Theme.hpp"
#include "dto/NavDto.hpp"
namespace geopro::app {
namespace {
constexpr int kRoleObjId = Qt::UserRole + 2; // 节点对象 idGS/TM 都存)
constexpr int kRoleConfType = Qt::UserRole + 3; // 1=GS 2=TM
constexpr int kRoleTypeId = Qt::UserRole + 4; // 类型 id编辑调 getDynamicForm 用)
constexpr int kRoleIsRoot = Qt::UserRole + 5; // 项目根标记(按 GS 处理,但仅 新建GS/TM/属性)
constexpr int kConfTypeGs = 1; // GS工区
constexpr int kConfTypeTm = 2; // TM 叶子
// topLevel=true 仅用于项目根:按 GS 处理xlsx 第32行 + 真实数据 TM 挂根),
// 携带其 id/typeId可右键 新建GS/TM/属性;勾选随 2D/3D 批次暂不开放。
void addNodes(QTreeWidgetItem* parent, const std::vector<data::dto::StructTreeNode>& nodes,
bool topLevel) {
for (const auto& n : nodes) {
auto* item = new QTreeWidgetItem(parent);
item->setText(0, QString::fromStdString(n.node.name));
item->setData(0, kRoleObjId, QString::fromStdString(n.node.id));
item->setData(0, kRoleTypeId, QString::fromStdString(n.node.typeId));
if (topLevel) {
// 项目根:作为 GS 承载id 携带),不可勾选;菜单仅 新建GS/TM/属性。
item->setData(0, kRoleConfType, kConfTypeGs);
item->setData(0, kRoleIsRoot, true);
} else if (n.isTm) {
item->setData(0, kRoleConfType, kConfTypeTm);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(0, Qt::Unchecked);
} else {
item->setData(0, kRoleConfType, kConfTypeGs); // GS
item->setFlags(item->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate);
item->setCheckState(0, Qt::Unchecked);
}
addNodes(item, n.children, false); // 子层永远非顶层
}
}
} // namespace
ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
auto* lay = new QVBoxLayout(this);
lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(0);
// Qt 原生标准树:复选框/展开箭头由 Fusion 绘制(明暗都清晰);配色取自全局主题 QSS。
tree_ = new QTreeWidget(this);
tree_->setHeaderHidden(true);
tree_->setIndentation(14); // 收紧缩进
lay->addWidget(tree_, 1);
hint_ = new QLabel(QStringLiteral("正在加载对象…"), this);
hint_->setAlignment(Qt::AlignCenter);
geopro::app::applyTokenizedStyleSheet(hint_, QStringLiteral("color:{{text/disabled}}; padding:16px;"));
hint_->setVisible(false);
lay->addWidget(hint_);
// viewport 事件过滤:记录鼠标按下是否落在复选框区,用于区分「选中」与「勾选」手势。
tree_->viewport()->installEventFilter(this);
QObject::connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int) {
if (pressOnCheckbox_) { // 点的是复选框:只切换勾选态,不当作「选中」(不重载数据集列表)
pressOnCheckbox_ = false;
return;
}
const bool isRoot = item->data(0, kRoleIsRoot).toBool();
const QString id = item->data(0, kRoleObjId).toString();
const int confType = item->data(0, kRoleConfType).toInt();
if (id.isEmpty() || confType == 0) return;
const QString typeId = item->data(0, kRoleTypeId).toString();
// 对象属性面板:项目根也发(携 isRoot=true面板据此显只读占位
emit objectSelectedForEdit(id, confType, typeId, item->text(0), isRoot);
if (isRoot) return; // 项目根:不联动数据列表/异常(仅右键操作 + 属性占位)
emit objectClicked(id, confType);
});
// 勾选变化GS 级联会触发多次 itemChanged用 0ms 单发合并成一次「收集勾选叶子并发射」。
QObject::connect(tree_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
if (checkPending_) return;
checkPending_ = true;
QTimer::singleShot(0, this, [this]() {
checkPending_ = false;
QStringList tmIds;
std::function<void(QTreeWidgetItem*)> walk = [&](QTreeWidgetItem* node) {
for (int i = 0; i < node->childCount(); ++i) {
QTreeWidgetItem* c = node->child(i);
if (c->data(0, kRoleConfType).toInt() == 2 && c->checkState(0) == Qt::Checked)
tmIds << c->data(0, kRoleObjId).toString();
walk(c);
}
};
walk(tree_->invisibleRootItem());
emit checkedTmsChanged(tmIds);
});
});
// 右键菜单(对齐菜单文档:显示/隐藏、定位、属性、异常详情、编辑、新建GS/TM、导入DS、删除
// 项目根=新建GS/TM/属性GS=全项(+新建GS/TM)TM=全项(+导入DS无新建GS)。
tree_->setContextMenuPolicy(Qt::CustomContextMenu);
QObject::connect(tree_, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
QTreeWidgetItem* item = tree_->itemAt(pos);
if (!item) return;
const QString id = item->data(0, kRoleObjId).toString();
const int confType = item->data(0, kRoleConfType).toInt();
if (id.isEmpty() || confType == 0) return;
const QString typeId = item->data(0, kRoleTypeId).toString();
const QString name = item->text(0);
const bool isRoot = item->data(0, kRoleIsRoot).toBool();
const bool isGs = (confType == kConfTypeGs);
const bool isTm = (confType == kConfTypeTm);
QMenu menu(this);
auto add = [&](const QString& text, const QString& action) {
menu.addAction(text, this,
[this, action, id, confType, typeId, name]() {
emit contextActionRequested(action, id, confType, typeId, name);
});
};
if (isRoot) {
// 项目根(按 GS仅 新建检测对象(GS) / 新建方法对象(TM) / 属性。
add(QStringLiteral("新建检测对象"), QStringLiteral("newGs"));
add(QStringLiteral("新建方法对象"), QStringLiteral("newTm"));
menu.addSeparator();
add(QStringLiteral("属性"), QStringLiteral("properties"));
menu.exec(tree_->viewport()->mapToGlobal(pos));
return;
}
add(QStringLiteral("显示 / 隐藏"), QStringLiteral("showHide"));
add(QStringLiteral("定位"), QStringLiteral("locate"));
menu.addSeparator();
add(QStringLiteral("属性"), QStringLiteral("properties"));
add(QStringLiteral("异常详情"), QStringLiteral("exceptionDetail"));
menu.addSeparator();
add(QStringLiteral("编辑"), QStringLiteral("edit"));
if (isGs) {
// GS 节点:新建检测对象 / 新建方法对象。TM 节点上不显示「新建检测对象」——xlsxtm 上新建GS 无效。)
add(QStringLiteral("新建检测对象"), QStringLiteral("newGs"));
add(QStringLiteral("新建方法对象"), QStringLiteral("newTm"));
}
if (isTm) {
// TM 节点:仅「新建方法对象」(同级,父=该 TM 的父 GS/根)+ 导入 DS。
// xlsxtm 上新建GS 无效,故不显示「新建检测对象」。)
add(QStringLiteral("新建方法对象"), QStringLiteral("newTm"));
add(QStringLiteral("导入数据集…"), QStringLiteral("importDs"));
}
menu.addSeparator();
add(QStringLiteral("删除"), QStringLiteral("delete"));
menu.exec(tree_->viewport()->mapToGlobal(pos));
});
}
bool ObjectTreePanel::eventFilter(QObject* watched, QEvent* event) {
if (tree_ && watched == tree_->viewport() && event->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(event);
const QPoint pos = me->position().toPoint();
pressOnCheckbox_ = false;
const QModelIndex idx = tree_->indexAt(pos);
if (idx.isValid() && (idx.flags() & Qt::ItemIsUserCheckable)) {
// 用样式计算该项复选框指示区的精确矩形(含缩进偏移由 visualRect 给出)。
QStyleOptionViewItem opt;
opt.initFrom(tree_);
opt.rect = tree_->visualRect(idx);
opt.features |= QStyleOptionViewItem::HasCheckIndicator;
const QRect cb =
tree_->style()->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &opt, tree_);
if (cb.contains(pos)) pressOnCheckbox_ = true;
}
}
return QWidget::eventFilter(watched, event);
}
// ── 快速筛选:遍历所有 TM 叶子,对其 setCheckState。批量改用 SignalBlocker 屏蔽逐项 itemChanged
// 末尾手动触发一次 itemChanged 让既有 0ms 合并逻辑收集并发射 checkedTmsChanged。──
void ObjectTreePanel::setAllTmsChecked(bool checked) {
if (!tree_) return;
const Qt::CheckState st = checked ? Qt::Checked : Qt::Unchecked;
std::function<void(QTreeWidgetItem*)> walk = [&](QTreeWidgetItem* node) {
for (int i = 0; i < node->childCount(); ++i) {
QTreeWidgetItem* c = node->child(i);
if (c->data(0, kRoleConfType).toInt() == kConfTypeTm) c->setCheckState(0, st);
walk(c);
}
};
{
const QSignalBlocker block(tree_);
walk(tree_->invisibleRootItem());
}
emit tree_->itemChanged(nullptr, 0); // 触发既有合并发射
}
void ObjectTreePanel::invertTmChecks() {
if (!tree_) return;
std::function<void(QTreeWidgetItem*)> walk = [&](QTreeWidgetItem* node) {
for (int i = 0; i < node->childCount(); ++i) {
QTreeWidgetItem* c = node->child(i);
if (c->data(0, kRoleConfType).toInt() == kConfTypeTm)
c->setCheckState(0, c->checkState(0) == Qt::Checked ? Qt::Unchecked : Qt::Checked);
walk(c);
}
};
{
const QSignalBlocker block(tree_);
walk(tree_->invisibleRootItem());
}
emit tree_->itemChanged(nullptr, 0);
}
QString ObjectTreePanel::currentParentForNew() const {
if (!tree_) return {};
// 新建对象的父必须是 GS/项目根GS 可挂 GS/根TM 父恒为 GS/根)。
// 选中 TM 时不能以 TM 作父,上溯到其最近的 GS/根 祖先。
if (QTreeWidgetItem* cur = tree_->currentItem()) {
if (cur->data(0, kRoleConfType).toInt() == kConfTypeTm) {
for (QTreeWidgetItem* p = cur->parent(); p; p = p->parent()) {
const QString pid = p->data(0, kRoleObjId).toString();
if (!pid.isEmpty() && p->data(0, kRoleConfType).toInt() == kConfTypeGs) return pid;
}
// TM 无 GS/根祖先(异常)→ 回落项目根
} else {
const QString id = cur->data(0, kRoleObjId).toString();
if (!id.isEmpty()) return id; // GS 或 项目根
}
}
// 未选中或上溯失败回落到项目根节点顶层首个setStructure 保证结构含项目根)。
if (tree_->topLevelItemCount() > 0)
return tree_->topLevelItem(0)->data(0, kRoleObjId).toString();
return {};
}
int ObjectTreePanel::currentSelectedConfType() const {
if (!tree_) return 0;
QTreeWidgetItem* cur = tree_->currentItem();
if (!cur) return 0;
// 项目根 confType 也存为 GS(1),故 root/GS 同归为 1TM=2。
return cur->data(0, kRoleConfType).toInt();
}
QString ObjectTreePanel::parentObjectId(const QString& objectId) const {
if (!tree_ || objectId.isEmpty()) return {};
// 按 id 定位树项。
QTreeWidgetItem* found = nullptr;
std::function<void(QTreeWidgetItem*)> find = [&](QTreeWidgetItem* node) {
for (int i = 0; i < node->childCount() && !found; ++i) {
QTreeWidgetItem* c = node->child(i);
if (c->data(0, kRoleObjId).toString() == objectId) {
found = c;
return;
}
find(c);
}
};
find(tree_->invisibleRootItem());
if (!found) return {};
// 上溯到最近的 GS/根祖先(口径同 currentParentForNew 的 TM 上溯)。
for (QTreeWidgetItem* p = found->parent(); p; p = p->parent()) {
const QString pid = p->data(0, kRoleObjId).toString();
if (!pid.isEmpty() && p->data(0, kRoleConfType).toInt() == kConfTypeGs) return pid;
}
return {};
}
void ObjectTreePanel::setStructure(const QString& projectName,
const std::vector<data::StructNode>& nodes) {
const QSignalBlocker block(tree_); // 重建触发 itemChanged先屏蔽
tree_->clear();
const auto roots = data::dto::buildStructTree(nodes);
if (roots.empty()) {
showMessage(projectName.isEmpty() ? QStringLiteral("(暂无项目)")
: QStringLiteral("(该项目暂无结构)"));
return;
}
hint_->setVisible(false);
tree_->setVisible(true);
addNodes(tree_->invisibleRootItem(), roots, true); // 结构已含项目根节点,根为非交互容器
tree_->expandAll();
}
void ObjectTreePanel::showMessage(const QString& message) {
tree_->clear();
tree_->setVisible(false);
hint_->setText(message);
hint_->setVisible(true);
}
} // namespace geopro::app