refactor(tree): 评审修复-抽 recomputeAllGsStates 去 nullptr 信号 hack + 注释精确化

This commit is contained in:
gaozheng 2026-06-24 18:15:03 +08:00
parent c5b3907fad
commit 40646f7d06
3 changed files with 29 additions and 18 deletions

View File

@ -162,8 +162,9 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
QObject::connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int) { QObject::connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int) {
if (pressOnCheckbox_) { // 点的是复选框:只切换勾选态,不当作「选中」(不重载数据集列表) if (pressOnCheckbox_) { // 点的是复选框:只切换勾选态,不当作「选中」(不重载数据集列表)
pressOnCheckbox_ = false; pressOnCheckbox_ = false;
// GS 复选框点击基于点击前聚合dsOn + 子 TM 均未被本次点击改写)翻转全开/全关spec §6 // GS 复选框点击:翻转全开/全关spec §6。Qt 因 ItemIsUserCheckable 在 itemClicked 发射前
// Qt 因 ItemIsUserCheckable 已临时 toggle 了 GS 自身 checkState但随后 recomputeGsState 会覆盖回正确三态。 // 已临时 toggle 了 GS 自身 checkState——故此处**故意不读 item->checkState(0)**(它已是 toggle 后的脏值),
// 而是读 kRoleGsDsOn + 子 TM二者均未被本次点击改写重建"点击前"聚合判断方向;末尾 recomputeGsState 覆盖回正确三态。
if (item->data(0, kRoleConfType).toInt() == kConfTypeGs && !item->data(0, kRoleIsRoot).toBool()) { if (item->data(0, kRoleConfType).toInt() == kConfTypeGs && !item->data(0, kRoleIsRoot).toBool()) {
const bool dsOn = item->data(0, kRoleGsDsOn).toBool(); const bool dsOn = item->data(0, kRoleGsDsOn).toBool();
int total = 0, checked = 0; int total = 0, checked = 0;
@ -198,16 +199,7 @@ ObjectTreePanel::ObjectTreePanel(QWidget* parent) : QWidget(parent) {
checkPending_ = true; checkPending_ = true;
QTimer::singleShot(0, this, [this]() { QTimer::singleShot(0, this, [this]() {
checkPending_ = false; checkPending_ = false;
std::function<void(QTreeWidgetItem*)> recompAll = [&](QTreeWidgetItem* node) { recomputeAllGsStates(); // 按子 TM 重算各 GS 三态(内部 SignalBlocker不再触发 itemChanged
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());
emitCheckedSources(); emitCheckedSources();
}); });
}); });
@ -326,6 +318,19 @@ void ObjectTreePanel::recomputeGsState(QTreeWidgetItem* gs) {
: Qt::Unchecked); : Qt::Unchecked);
} }
void ObjectTreePanel::recomputeAllGsStates() {
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() == kConfTypeGs && !c->data(0, kRoleIsRoot).toBool())
recomputeGsState(c);
walk(c);
}
};
walk(tree_->invisibleRootItem());
}
void ObjectTreePanel::setAllChildTmChecked(QTreeWidgetItem* gs, bool checked) { void ObjectTreePanel::setAllChildTmChecked(QTreeWidgetItem* gs, bool checked) {
if (!gs) return; if (!gs) return;
const Qt::CheckState st = checked ? Qt::Checked : Qt::Unchecked; const Qt::CheckState st = checked ? Qt::Checked : Qt::Unchecked;
@ -382,8 +387,8 @@ void ObjectTreePanel::emitCheckedSources() {
emit checkedTmsChanged(tmIds); emit checkedTmsChanged(tmIds);
} }
// ── 快速筛选:遍历所有 TM 叶子,对其 setCheckState。批量改用 SignalBlocker 屏蔽逐项 itemChanged // ── 快速筛选:遍历所有 TM 叶子 setCheckStateSignalBlocker 屏蔽逐项 itemChanged
// 末尾手动触发一次 itemChanged 让既有 0ms 合并逻辑收集并发射 checkedTmsChanged。── // 末尾直接 recomputeAllGsStates + emitCheckedSources 同步 GS 三态并发射勾选源。──
void ObjectTreePanel::setAllTmsChecked(bool checked) { void ObjectTreePanel::setAllTmsChecked(bool checked) {
if (!tree_) return; if (!tree_) return;
const Qt::CheckState st = checked ? Qt::Checked : Qt::Unchecked; const Qt::CheckState st = checked ? Qt::Checked : Qt::Unchecked;
@ -398,7 +403,8 @@ void ObjectTreePanel::setAllTmsChecked(bool checked) {
const QSignalBlocker block(tree_); const QSignalBlocker block(tree_);
walk(tree_->invisibleRootItem()); walk(tree_->invisibleRootItem());
} }
emit tree_->itemChanged(nullptr, 0); // 触发既有合并发射 recomputeAllGsStates(); // 子 TM 批量变更后同步各 GS 三态
emitCheckedSources();
} }
void ObjectTreePanel::invertTmChecks() { void ObjectTreePanel::invertTmChecks() {
@ -415,7 +421,8 @@ void ObjectTreePanel::invertTmChecks() {
const QSignalBlocker block(tree_); const QSignalBlocker block(tree_);
walk(tree_->invisibleRootItem()); walk(tree_->invisibleRootItem());
} }
emit tree_->itemChanged(nullptr, 0); recomputeAllGsStates();
emitCheckedSources();
} }
QString ObjectTreePanel::currentParentForNew() const { QString ObjectTreePanel::currentParentForNew() const {

View File

@ -59,6 +59,7 @@ signals:
private: private:
// GS 三态(停用 Qt::ItemIsAutoTristate手动维护据自身 ds 开关 + 子 TM 勾选聚合spec §6 // GS 三态(停用 Qt::ItemIsAutoTristate手动维护据自身 ds 开关 + 子 TM 勾选聚合spec §6
void recomputeGsState(QTreeWidgetItem* gs); void recomputeGsState(QTreeWidgetItem* gs);
void recomputeAllGsStates(); // 遍历全树对每个非根 GS 调 recomputeGsState
void setAllChildTmChecked(QTreeWidgetItem* gs, bool checked); // 批量设子 TM内部 SignalBlocker void setAllChildTmChecked(QTreeWidgetItem* gs, bool checked); // 批量设子 TM内部 SignalBlocker
bool allTmChecked(QTreeWidgetItem* gs) const; // GS 下子 TM 是否全勾(无子 TM=false bool allTmChecked(QTreeWidgetItem* gs) const; // GS 下子 TM 是否全勾(无子 TM=false
// 收集勾选源并集TM + GS 自身 ds + 项目根直挂 ds去重后发 checkedSourcesChanged兼发旧 checkedTmsChanged // 收集勾选源并集TM + GS 自身 ds + 项目根直挂 ds去重后发 checkedSourcesChanged兼发旧 checkedTmsChanged

View File

@ -6,8 +6,11 @@ namespace geopro::app {
enum class GsCheck { Unchecked, Partial, Checked }; enum class GsCheck { Unchecked, Partial, Checked };
// GS 复选框三态 = [自身 ds 开关] [子 TM 勾选] 的聚合spec §6 // GS 复选框三态spec §6
// 无子 TMtotalTmCount==0时退化为仅看 dsOn。 // Unchecked = 自身 ds 关 且 无子 TM 勾选;
// Checked = 自身 ds 开 且 全部子 TM 勾选(注意是 AND不是"父子都打钩即满"
// Partial = 其余(只 ds 开 / 只部分 TM / ds 开但 TM 未满 等)。
// 无子 TMtotalTmCount==0时退化为仅看 dsOnds 开=Checked关=Unchecked
inline GsCheck aggregateGsState(bool dsOn, int checkedTmCount, int totalTmCount) { inline GsCheck aggregateGsState(bool dsOn, int checkedTmCount, int totalTmCount) {
const bool anyOn = dsOn || checkedTmCount > 0; const bool anyOn = dsOn || checkedTmCount > 0;
if (!anyOn) return GsCheck::Unchecked; if (!anyOn) return GsCheck::Unchecked;