#include "panels/ObjectTreePanel.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Glyphs.hpp" #include "Theme.hpp" #include "dto/NavDto.hpp" namespace geopro::app { namespace { constexpr int kRoleObjId = Qt::UserRole + 2; // 节点对象 id(GS/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& 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 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 节点上不显示「新建检测对象」——xlsx:tm 上新建GS 无效。) add(QStringLiteral("新建检测对象"), QStringLiteral("newGs")); add(QStringLiteral("新建方法对象"), QStringLiteral("newTm")); } if (isTm) { // TM 节点:仅「新建方法对象」(同级,父=该 TM 的父 GS/根)+ 导入 DS。 // (xlsx:tm 上新建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(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 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 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 同归为 1;TM=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 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& 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