diff --git a/src/app/panels/ObjectTreePanel.cpp b/src/app/panels/ObjectTreePanel.cpp index 64c2593..c66911d 100644 --- a/src/app/panels/ObjectTreePanel.cpp +++ b/src/app/panels/ObjectTreePanel.cpp @@ -162,8 +162,9 @@ 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 会覆盖回正确三态。 + // GS 复选框点击:翻转全开/全关(spec §6)。Qt 因 ItemIsUserCheckable 在 itemClicked 发射前 + // 已临时 toggle 了 GS 自身 checkState——故此处**故意不读 item->checkState(0)**(它已是 toggle 后的脏值), + // 而是读 kRoleGsDsOn + 子 TM(二者均未被本次点击改写)重建"点击前"聚合判断方向;末尾 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; @@ -198,16 +199,7 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) { checkPending_ = true; QTimer::singleShot(0, this, [this]() { checkPending_ = false; - std::function recompAll = [&](QTreeWidgetItem* node) { - for (int i = 0; i < node->childCount(); ++i) { - QTreeWidgetItem* c = node->child(i); - if (c->data(0, kRoleConfType).toInt() == kConfTypeGs && - !c->data(0, kRoleIsRoot).toBool()) - recomputeGsState(c); // 内部 SignalBlocker,不会再触发 itemChanged - recompAll(c); - } - }; - recompAll(tree_->invisibleRootItem()); + recomputeAllGsStates(); // 按子 TM 重算各 GS 三态(内部 SignalBlocker,不再触发 itemChanged) emitCheckedSources(); }); }); @@ -326,6 +318,19 @@ void ObjectTreePanel::recomputeGsState(QTreeWidgetItem* gs) { : Qt::Unchecked); } +void ObjectTreePanel::recomputeAllGsStates() { + 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() == kConfTypeGs && !c->data(0, kRoleIsRoot).toBool()) + recomputeGsState(c); + walk(c); + } + }; + walk(tree_->invisibleRootItem()); +} + void ObjectTreePanel::setAllChildTmChecked(QTreeWidgetItem* gs, bool checked) { if (!gs) return; const Qt::CheckState st = checked ? Qt::Checked : Qt::Unchecked; @@ -382,8 +387,8 @@ void ObjectTreePanel::emitCheckedSources() { emit checkedTmsChanged(tmIds); } -// ── 快速筛选:遍历所有 TM 叶子,对其 setCheckState。批量改用 SignalBlocker 屏蔽逐项 itemChanged, -// 末尾手动触发一次 itemChanged 让既有 0ms 合并逻辑收集并发射 checkedTmsChanged。── +// ── 快速筛选:遍历所有 TM 叶子 setCheckState(SignalBlocker 屏蔽逐项 itemChanged), +// 末尾直接 recomputeAllGsStates + emitCheckedSources 同步 GS 三态并发射勾选源。── void ObjectTreePanel::setAllTmsChecked(bool checked) { if (!tree_) return; const Qt::CheckState st = checked ? Qt::Checked : Qt::Unchecked; @@ -398,7 +403,8 @@ void ObjectTreePanel::setAllTmsChecked(bool checked) { const QSignalBlocker block(tree_); walk(tree_->invisibleRootItem()); } - emit tree_->itemChanged(nullptr, 0); // 触发既有合并发射 + recomputeAllGsStates(); // 子 TM 批量变更后同步各 GS 三态 + emitCheckedSources(); } void ObjectTreePanel::invertTmChecks() { @@ -415,7 +421,8 @@ void ObjectTreePanel::invertTmChecks() { const QSignalBlocker block(tree_); walk(tree_->invisibleRootItem()); } - emit tree_->itemChanged(nullptr, 0); + recomputeAllGsStates(); + emitCheckedSources(); } QString ObjectTreePanel::currentParentForNew() const { diff --git a/src/app/panels/ObjectTreePanel.hpp b/src/app/panels/ObjectTreePanel.hpp index 1675f49..6b49421 100644 --- a/src/app/panels/ObjectTreePanel.hpp +++ b/src/app/panels/ObjectTreePanel.hpp @@ -59,6 +59,7 @@ signals: private: // GS 三态(停用 Qt::ItemIsAutoTristate,手动维护):据自身 ds 开关 + 子 TM 勾选聚合(spec §6)。 void recomputeGsState(QTreeWidgetItem* gs); + void recomputeAllGsStates(); // 遍历全树对每个非根 GS 调 recomputeGsState void setAllChildTmChecked(QTreeWidgetItem* gs, bool checked); // 批量设子 TM(内部 SignalBlocker) bool allTmChecked(QTreeWidgetItem* gs) const; // GS 下子 TM 是否全勾(无子 TM=false) // 收集勾选源并集(TM + GS 自身 ds + 项目根直挂 ds),去重后发 checkedSourcesChanged(兼发旧 checkedTmsChanged)。 diff --git a/src/app/panels/ObjectTreeSelection.hpp b/src/app/panels/ObjectTreeSelection.hpp index 56a687a..36a26fc 100644 --- a/src/app/panels/ObjectTreeSelection.hpp +++ b/src/app/panels/ObjectTreeSelection.hpp @@ -6,8 +6,11 @@ namespace geopro::app { enum class GsCheck { Unchecked, Partial, Checked }; -// GS 复选框三态 = [自身 ds 开关] ∨ [子 TM 勾选] 的聚合(spec §6)。 -// 无子 TM(totalTmCount==0)时退化为仅看 dsOn。 +// GS 复选框三态(spec §6): +// Unchecked = 自身 ds 关 且 无子 TM 勾选; +// Checked = 自身 ds 开 且 全部子 TM 勾选(注意是 AND,不是"父子都打钩即满"); +// Partial = 其余(只 ds 开 / 只部分 TM / ds 开但 TM 未满 等)。 +// 无子 TM(totalTmCount==0)时退化为仅看 dsOn(ds 开=Checked,关=Unchecked)。 inline GsCheck aggregateGsState(bool dsOn, int checkedTmCount, int totalTmCount) { const bool anyOn = dsOn || checkedTmCount > 0; if (!anyOn) return GsCheck::Unchecked;