feat/vtk-3d-view #7
|
|
@ -24,6 +24,7 @@
|
|||
#include "Glyphs.hpp"
|
||||
#include "Theme.hpp"
|
||||
#include "dto/NavDto.hpp"
|
||||
#include "panels/ObjectTreeSelection.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ 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 kRoleChildCount = Qt::UserRole + 6; // §6.1 计数徽标:GS 直接子节点数(>0 才显示)
|
||||
constexpr int kRoleGsDsOn = Qt::UserRole + 7; // GS 自身直挂 ds 开关(bool;三态聚合用,spec §6)
|
||||
constexpr int kConfTypeGs = 1; // GS(工区)
|
||||
constexpr int kConfTypeTm = 2; // TM 叶子
|
||||
|
||||
|
|
@ -120,7 +122,9 @@ void addNodes(QTreeWidgetItem* parent, const std::vector<data::dto::StructTreeNo
|
|||
item->setIcon(0, makeGlyph(Glyph::SurveyLine, iconColor, iconPx));
|
||||
} else {
|
||||
item->setData(0, kRoleConfType, kConfTypeGs); // GS
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate);
|
||||
// 停用 Qt::ItemIsAutoTristate:GS 三态由 recomputeGsState 手动聚合(自身 ds 开关 + 子 TM)。
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setData(0, kRoleGsDsOn, false); // GS 自身直挂 ds 开关初始关
|
||||
item->setCheckState(0, Qt::Unchecked);
|
||||
item->setIcon(0, makeGlyph(Glyph::WorkArea, iconColor, iconPx));
|
||||
}
|
||||
|
|
@ -158,6 +162,23 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
|
|||
QObject::connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int) {
|
||||
if (pressOnCheckbox_) { // 点的是复选框:只切换勾选态,不当作「选中」(不重载数据集列表)
|
||||
pressOnCheckbox_ = false;
|
||||
// GS 复选框点击:基于点击前聚合(dsOn + 子 TM 均未被本次点击改写)翻转全开/全关(spec §6)。
|
||||
// Qt 因 ItemIsUserCheckable 已临时 toggle 了 GS 自身 checkState,但随后 recomputeGsState 会覆盖回正确三态。
|
||||
if (item->data(0, kRoleConfType).toInt() == kConfTypeGs && !item->data(0, kRoleIsRoot).toBool()) {
|
||||
const bool dsOn = item->data(0, kRoleGsDsOn).toBool();
|
||||
int total = 0, checked = 0;
|
||||
for (int i = 0; i < item->childCount(); ++i) {
|
||||
QTreeWidgetItem* c = item->child(i);
|
||||
if (c->data(0, kRoleConfType).toInt() != kConfTypeTm) continue;
|
||||
++total;
|
||||
if (c->checkState(0) == Qt::Checked) ++checked;
|
||||
}
|
||||
const bool turnOn = (aggregateGsState(dsOn, checked, total) == GsCheck::Unchecked);
|
||||
item->setData(0, kRoleGsDsOn, turnOn);
|
||||
setAllChildTmChecked(item, turnOn);
|
||||
recomputeGsState(item);
|
||||
emitCheckedSources();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const bool isRoot = item->data(0, kRoleIsRoot).toBool();
|
||||
|
|
@ -170,23 +191,24 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
|
|||
if (isRoot) return; // 项目根:不联动数据列表/异常(仅右键操作 + 属性占位)
|
||||
emit objectClicked(id, confType);
|
||||
});
|
||||
// 勾选变化:GS 级联会触发多次 itemChanged,用 0ms 单发合并成一次「收集勾选叶子并发射」。
|
||||
// 勾选变化:TM 勾选/GS 级联触发多次 itemChanged,用 0ms 单发合并;
|
||||
// 合并里先按子 TM 重算每个 GS 三态(手动聚合),再收集勾选源并集发射。
|
||||
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) {
|
||||
std::function<void(QTreeWidgetItem*)> recompAll = [&](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);
|
||||
if (c->data(0, kRoleConfType).toInt() == kConfTypeGs &&
|
||||
!c->data(0, kRoleIsRoot).toBool())
|
||||
recomputeGsState(c); // 内部 SignalBlocker,不会再触发 itemChanged
|
||||
recompAll(c);
|
||||
}
|
||||
};
|
||||
walk(tree_->invisibleRootItem());
|
||||
emit checkedTmsChanged(tmIds);
|
||||
recompAll(tree_->invisibleRootItem());
|
||||
emitCheckedSources();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -228,6 +250,29 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
|
|||
menu.addSeparator();
|
||||
// 「编辑」弹窗通道已移除:GS/TM 统一走「属性」面板(右上对象属性)就地编辑,避免双编辑入口。
|
||||
if (isGs) {
|
||||
// GS「选择」子菜单:分别开关「自身直挂 ds」与「全部子 TM」(spec §6 三态手动维护)。
|
||||
QMenu* sel = menu.addMenu(QStringLiteral("选择"));
|
||||
int tmCount = 0;
|
||||
for (int i = 0; i < item->childCount(); ++i)
|
||||
if (item->child(i)->data(0, kRoleConfType).toInt() == kConfTypeTm) ++tmCount;
|
||||
QAction* dsAct = sel->addAction(QStringLiteral("ds"));
|
||||
dsAct->setCheckable(true);
|
||||
dsAct->setChecked(item->data(0, kRoleGsDsOn).toBool());
|
||||
QObject::connect(dsAct, &QAction::triggered, this, [this, item](bool on) {
|
||||
item->setData(0, kRoleGsDsOn, on);
|
||||
recomputeGsState(item);
|
||||
emitCheckedSources();
|
||||
});
|
||||
QAction* tmAct = sel->addAction(QStringLiteral("tm"));
|
||||
tmAct->setCheckable(true);
|
||||
tmAct->setChecked(allTmChecked(item));
|
||||
tmAct->setEnabled(tmCount > 0);
|
||||
QObject::connect(tmAct, &QAction::triggered, this, [this, item](bool on) {
|
||||
setAllChildTmChecked(item, on);
|
||||
recomputeGsState(item);
|
||||
emitCheckedSources();
|
||||
});
|
||||
menu.addSeparator();
|
||||
// GS 节点:新建检测对象 / 新建方法对象。(TM 节点上不显示「新建检测对象」——xlsx:tm 上新建GS 无效。)
|
||||
add(QStringLiteral("新建检测对象"), QStringLiteral("newGs"));
|
||||
add(QStringLiteral("新建方法对象"), QStringLiteral("newTm"));
|
||||
|
|
@ -262,6 +307,81 @@ bool ObjectTreePanel::eventFilter(QObject* watched, QEvent* event) {
|
|||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
// ── GS 三态手动维护(spec §6):停用 AutoTristate 后,GS 复选框状态完全由此派生。──
|
||||
void ObjectTreePanel::recomputeGsState(QTreeWidgetItem* gs) {
|
||||
if (!gs || gs->data(0, kRoleConfType).toInt() != kConfTypeGs || gs->data(0, kRoleIsRoot).toBool())
|
||||
return;
|
||||
const bool dsOn = gs->data(0, kRoleGsDsOn).toBool();
|
||||
int total = 0, checked = 0;
|
||||
for (int i = 0; i < gs->childCount(); ++i) {
|
||||
QTreeWidgetItem* c = gs->child(i);
|
||||
if (c->data(0, kRoleConfType).toInt() != kConfTypeTm) continue;
|
||||
++total;
|
||||
if (c->checkState(0) == Qt::Checked) ++checked;
|
||||
}
|
||||
const GsCheck s = aggregateGsState(dsOn, checked, total);
|
||||
const QSignalBlocker block(tree_); // setCheckState 不再触发 itemChanged 递归
|
||||
gs->setCheckState(0, s == GsCheck::Checked ? Qt::Checked
|
||||
: s == GsCheck::Partial ? Qt::PartiallyChecked
|
||||
: Qt::Unchecked);
|
||||
}
|
||||
|
||||
void ObjectTreePanel::setAllChildTmChecked(QTreeWidgetItem* gs, bool checked) {
|
||||
if (!gs) return;
|
||||
const Qt::CheckState st = checked ? Qt::Checked : Qt::Unchecked;
|
||||
const QSignalBlocker block(tree_);
|
||||
for (int i = 0; i < gs->childCount(); ++i) {
|
||||
QTreeWidgetItem* c = gs->child(i);
|
||||
if (c->data(0, kRoleConfType).toInt() == kConfTypeTm) c->setCheckState(0, st);
|
||||
}
|
||||
}
|
||||
|
||||
bool ObjectTreePanel::allTmChecked(QTreeWidgetItem* gs) const {
|
||||
if (!gs) return false;
|
||||
int total = 0, checked = 0;
|
||||
for (int i = 0; i < gs->childCount(); ++i) {
|
||||
QTreeWidgetItem* c = gs->child(i);
|
||||
if (c->data(0, kRoleConfType).toInt() != kConfTypeTm) continue;
|
||||
++total;
|
||||
if (c->checkState(0) == Qt::Checked) ++checked;
|
||||
}
|
||||
return total > 0 && checked == total;
|
||||
}
|
||||
|
||||
// 收集勾选源并集:勾选的 TM(confType=2) + ds 开的 GS(confType=1) + 项目根直挂 ds(固定纳入),
|
||||
// 按 {id,confType} 去重,发 checkedSourcesChanged;兼发旧 checkedTmsChanged(Task 12 删)。
|
||||
void ObjectTreePanel::emitCheckedSources() {
|
||||
if (!tree_) return;
|
||||
std::vector<geopro::data::DataSource> src;
|
||||
QStringList tmIds;
|
||||
std::function<void(QTreeWidgetItem*)> walk = [&](QTreeWidgetItem* node) {
|
||||
for (int i = 0; i < node->childCount(); ++i) {
|
||||
QTreeWidgetItem* c = node->child(i);
|
||||
const int ct = c->data(0, kRoleConfType).toInt();
|
||||
if (ct == kConfTypeTm && c->checkState(0) == Qt::Checked) {
|
||||
const QString id = c->data(0, kRoleObjId).toString();
|
||||
src.push_back({id.toStdString(), kConfTypeTm});
|
||||
tmIds << id;
|
||||
}
|
||||
if (ct == kConfTypeGs && !c->data(0, kRoleIsRoot).toBool() && c->data(0, kRoleGsDsOn).toBool())
|
||||
src.push_back({c->data(0, kRoleObjId).toString().toStdString(), kConfTypeGs});
|
||||
walk(c);
|
||||
}
|
||||
};
|
||||
walk(tree_->invisibleRootItem());
|
||||
// 项目根直挂 ds 固定纳入(spec §6:项目根无复选框、直挂 ds 固定显示)。
|
||||
if (tree_->topLevelItemCount() > 0) {
|
||||
QTreeWidgetItem* root = tree_->topLevelItem(0);
|
||||
if (root->data(0, kRoleIsRoot).toBool())
|
||||
src.push_back({root->data(0, kRoleObjId).toString().toStdString(), kConfTypeGs});
|
||||
}
|
||||
const auto deduped = dedupeSources(std::move(src));
|
||||
QList<geopro::data::DataSource> list;
|
||||
for (const auto& s : deduped) list.push_back(s);
|
||||
emit checkedSourcesChanged(list);
|
||||
emit checkedTmsChanged(tmIds);
|
||||
}
|
||||
|
||||
// ── 快速筛选:遍历所有 TM 叶子,对其 setCheckState。批量改用 SignalBlocker 屏蔽逐项 itemChanged,
|
||||
// 末尾手动触发一次 itemChanged 让既有 0ms 合并逻辑收集并发射 checkedTmsChanged。──
|
||||
void ObjectTreePanel::setAllTmsChecked(bool checked) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
#pragma once
|
||||
#include <QList>
|
||||
#include <QStringList>
|
||||
#include <QWidget>
|
||||
#include <vector>
|
||||
#include "repo/RepoTypes.hpp"
|
||||
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class QLabel;
|
||||
|
||||
namespace geopro::app {
|
||||
|
|
@ -46,13 +48,22 @@ signals:
|
|||
// 单击行(含项目根,带 typeId/name/isRoot):驱动对象属性面板的可编辑表单。
|
||||
void objectSelectedForEdit(const QString& objectId, int confType, const QString& typeId,
|
||||
const QString& name, bool isRoot);
|
||||
// 当前全部被勾选的 TM 叶子 id(已合并发射)。
|
||||
// 当前全部被勾选的 TM 叶子 id(已合并发射)。【旧信号,Task 12 接线切换后删除】
|
||||
void checkedTmsChanged(const QStringList& tmObjectIds);
|
||||
// 当前全部被勾选的数据源并集(TM + GS 自身 ds 开 + 项目根直挂 ds),按 {id,confType} 去重(spec §6)。
|
||||
void checkedSourcesChanged(const QList<geopro::data::DataSource>& sources);
|
||||
// 右键菜单动作(action 取值见 .cpp;objectId/confType/typeId 为右键命中项,name 用于确认框/标题)。
|
||||
void contextActionRequested(const QString& action, const QString& objectId, int confType,
|
||||
const QString& typeId, const QString& name);
|
||||
|
||||
private:
|
||||
// GS 三态(停用 Qt::ItemIsAutoTristate,手动维护):据自身 ds 开关 + 子 TM 勾选聚合(spec §6)。
|
||||
void recomputeGsState(QTreeWidgetItem* gs);
|
||||
void setAllChildTmChecked(QTreeWidgetItem* gs, bool checked); // 批量设子 TM(内部 SignalBlocker)
|
||||
bool allTmChecked(QTreeWidgetItem* gs) const; // GS 下子 TM 是否全勾(无子 TM=false)
|
||||
// 收集勾选源并集(TM + GS 自身 ds + 项目根直挂 ds),去重后发 checkedSourcesChanged(兼发旧 checkedTmsChanged)。
|
||||
void emitCheckedSources();
|
||||
|
||||
QTreeWidget* tree_ = nullptr; // Qt 原生标准树(复选框/箭头由 Fusion 绘制,清晰可控)
|
||||
QLabel* hint_ = nullptr;
|
||||
bool checkPending_ = false; // 勾选合并发射防重入
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
#include <vector>
|
||||
#include "repo/RepoTypes.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
enum class GsCheck { Unchecked, Partial, Checked };
|
||||
|
||||
// GS 复选框三态 = [自身 ds 开关] ∨ [子 TM 勾选] 的聚合(spec §6)。
|
||||
// 无子 TM(totalTmCount==0)时退化为仅看 dsOn。
|
||||
inline GsCheck aggregateGsState(bool dsOn, int checkedTmCount, int totalTmCount) {
|
||||
const bool anyOn = dsOn || checkedTmCount > 0;
|
||||
if (!anyOn) return GsCheck::Unchecked;
|
||||
const bool tmAll = (totalTmCount == 0) || (checkedTmCount == totalTmCount);
|
||||
if (dsOn && tmAll) return GsCheck::Checked;
|
||||
return GsCheck::Partial;
|
||||
}
|
||||
|
||||
// 对象树勾选产出的数据源并集按 {id,confType} 去重保序(spec §6)。
|
||||
inline std::vector<geopro::data::DataSource> dedupeSources(std::vector<geopro::data::DataSource> in) {
|
||||
std::vector<geopro::data::DataSource> out;
|
||||
for (const auto& s : in) {
|
||||
bool dup = false;
|
||||
for (const auto& o : out)
|
||||
if (o.id == s.id && o.confType == s.confType) { dup = true; break; }
|
||||
if (!dup) out.push_back(s);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -21,6 +21,9 @@ struct DsRow {
|
|||
int structParentConfType = 0; // 1=GS/项目根 2=TM
|
||||
};
|
||||
struct DsPage { std::vector<DsRow> rows; int total = 0; };
|
||||
|
||||
// 对象树勾选产出的数据源(spec §6)。confType: 1=GS/项目根, 2=TM。
|
||||
struct DataSource { std::string id; int confType = 0; };
|
||||
struct TmNode { std::string id, name, confCode; std::vector<DsNode> dss; };
|
||||
struct GsNode { std::string id, name; std::vector<TmNode> tms; };
|
||||
struct Project { std::string id, name; std::vector<GsNode> gss; };
|
||||
|
|
|
|||
|
|
@ -174,6 +174,8 @@ target_sources(geopro_tests PRIVATE
|
|||
app/test_dataset_category.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/app/DatasetCategory.cpp
|
||||
)
|
||||
# 对象树勾选纯逻辑(GS 三态聚合 aggregateGsState + 勾选源去重 dedupeSources,header-only)。
|
||||
target_sources(geopro_tests PRIVATE app/test_object_tree_selection.cpp)
|
||||
# measurement 散点纯逻辑(值类型变换 / 显隐 id 收集 / 过滤体 / 另存体,Qt6::Core JSON + core model)。
|
||||
target_sources(geopro_tests PRIVATE
|
||||
app/test_scatter_data_ops.cpp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include "panels/ObjectTreeSelection.hpp"
|
||||
using namespace geopro::app;
|
||||
using geopro::data::DataSource;
|
||||
|
||||
TEST(AggregateGsState, AllOnIsChecked) {
|
||||
EXPECT_EQ(aggregateGsState(true, 3, 3), GsCheck::Checked);
|
||||
}
|
||||
TEST(AggregateGsState, AllOffIsUnchecked) {
|
||||
EXPECT_EQ(aggregateGsState(false, 0, 3), GsCheck::Unchecked);
|
||||
}
|
||||
TEST(AggregateGsState, DsOnTmNoneIsPartial) {
|
||||
EXPECT_EQ(aggregateGsState(true, 0, 3), GsCheck::Partial); // 只 GS 自身 ds
|
||||
}
|
||||
TEST(AggregateGsState, DsOffSomeTmIsPartial) {
|
||||
EXPECT_EQ(aggregateGsState(false, 1, 3), GsCheck::Partial); // 部分子 TM
|
||||
}
|
||||
TEST(AggregateGsState, DsOnSomeTmIsPartial) {
|
||||
EXPECT_EQ(aggregateGsState(true, 2, 3), GsCheck::Partial); // ds 开但 TM 未满
|
||||
}
|
||||
TEST(AggregateGsState, NoTmFallsBackToDsOnly) {
|
||||
EXPECT_EQ(aggregateGsState(true, 0, 0), GsCheck::Checked); // 无子 TM → 仅看 ds 开关
|
||||
EXPECT_EQ(aggregateGsState(false, 0, 0), GsCheck::Unchecked);
|
||||
}
|
||||
|
||||
TEST(DedupeSources, RemovesDuplicateByIdAndConfType) {
|
||||
std::vector<DataSource> in = {{"t1", 2}, {"g1", 1}, {"t1", 2}, {"g1", 1}, {"t2", 2}};
|
||||
const auto out = dedupeSources(in);
|
||||
ASSERT_EQ(out.size(), 3u);
|
||||
EXPECT_EQ(out[0].id, "t1"); EXPECT_EQ(out[0].confType, 2);
|
||||
EXPECT_EQ(out[1].id, "g1"); EXPECT_EQ(out[1].confType, 1);
|
||||
EXPECT_EQ(out[2].id, "t2");
|
||||
}
|
||||
TEST(DedupeSources, SameIdDifferentConfTypeKept) {
|
||||
std::vector<DataSource> in = {{"x", 1}, {"x", 2}};
|
||||
EXPECT_EQ(dedupeSources(in).size(), 2u);
|
||||
}
|
||||
Loading…
Reference in New Issue