geopro/src/app/panels/columns/CategoryAnalysisTab.cpp

159 lines
7.5 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/columns/CategoryAnalysisTab.hpp"
#include <QAbstractItemView>
#include <QPointer>
#include <QScrollArea>
#include <QScrollBar>
#include <QTimer>
#include <QTreeWidget>
#include <QVBoxLayout>
#include "Theme.hpp"
#include "panels/columns/CategorySection.hpp"
namespace geopro::app {
CategoryAnalysisTab::CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* dict, QWidget* parent)
: QWidget(parent) {
auto* outer = new QVBoxLayout(this);
outer->setContentsMargins(0, 0, 0, 0);
outer->setSpacing(0);
auto* scroll = new QScrollArea(this);
scroll_ = scroll;
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // 内容随面板宽自适应,不出横向滚动条
outer->addWidget(scroll, 1);
auto* content = new QWidget(scroll);
content_ = content;
auto* col = new QVBoxLayout(content);
col_ = col;
col->setContentsMargins(0, space::kSm, 0, space::kSm); // 顶部留白:首段段头不贴顶
col->setSpacing(space::kSm);
for (const CategorySpec& spec : categoryConfigs()) {
auto* sec = new CategorySection(spec, dict, content);
sections_[spec.id] = sec;
ordered_.push_back(sec);
connect(sec, &CategorySection::collapsedChanged, this,
&CategoryAnalysisTab::relayoutSections); // 折叠/展开 → 重排 stretch向上收
const std::string segId = spec.id;
connect(sec, &CategorySection::checkedDatasetsChanged, this,
[this, segId](const QStringList& ids) {
checkedBySeg_[segId] = ids;
recomputeCheckedUnion();
});
connect(sec, &CategorySection::generateVolumeRequested, this,
&CategoryAnalysisTab::generateVolumeRequested);
connect(sec, &CategorySection::detailRequested, this, &CategoryAnalysisTab::detailRequested);
connect(sec, &CategorySection::deleteDatasetRequested, this,
&CategoryAnalysisTab::deleteDatasetRequested);
connect(sec, &CategorySection::sliceRequested, this, &CategoryAnalysisTab::sliceRequested);
connect(sec, &CategorySection::colorScaleRequested, this, &CategoryAnalysisTab::colorScaleRequested);
connect(sec, &CategorySection::sliceSaveRequested, this, &CategoryAnalysisTab::sliceSaveRequested);
connect(sec, &CategorySection::sliceSaveAsRequested, this, &CategoryAnalysisTab::sliceSaveAsRequested);
connect(sec, &CategorySection::sliceExportImageRequested, this,
&CategoryAnalysisTab::sliceExportImageRequested);
connect(sec, &CategorySection::sliceExportDatRequested, this,
&CategoryAnalysisTab::sliceExportDatRequested);
connect(sec, &CategorySection::anomalyVisibilityChanged, this,
&CategoryAnalysisTab::anomalyVisibilityChanged);
connect(sec, &CategorySection::datasetSelected, this, &CategoryAnalysisTab::datasetSelected);
// #7各段等分 stretch → 内容都少时四段平分高度填满面板(初始与 VTK 区等高、不出滚动条)
// 某段内容增多时其最小高度(=内容总高)撑大,超出视口则由外层 QScrollArea 统一出纵向滚动条。
col->addWidget(sec, 1);
}
// 尾部弹簧(末项):默认 0全部段折叠时由 relayoutSections 置 1吸收余量把段头顶到顶部。
col->addStretch(0);
scroll->setWidget(content);
}
void CategoryAnalysisTab::relayoutSections() {
if (!col_) return;
int expanded = 0;
for (auto* sec : ordered_)
if (sec->isExpanded()) ++expanded;
// 展开段 stretch=1吸收余量、铺满折叠段 stretch=0只占段头高下方不再留空
for (auto* sec : ordered_) col_->setStretchFactor(sec, sec->isExpanded() ? 1 : 0);
// 尾部弹簧:仅当全部折叠时=1把所有段头顶到顶部有任一展开段时=0由展开段吸收余量
col_->setStretch(col_->count() - 1, expanded == 0 ? 1 : 0);
}
void CategoryAnalysisTab::setBuckets(const CategoryBuckets& b) {
const auto& cfg = categoryConfigs();
for (std::size_t i = 0; i < cfg.size() && i < b.segments.size(); ++i) {
// voxel(三维体) 段数据来自 mock voxelTree(体/切片/异常),由调用方单独 section("voxel")->setDatasets
// 注入splitByCategory 对它永远是空桶,若在此用空桶覆盖会先清掉其勾选(随后重填但勾选已丢) →
// 表现为「创建切片/异常后体/切片选择被清空」(用户 issue 1)。故跳过 voxel勿覆盖。
if (cfg[i].id == "voxel") continue;
if (auto* sec = section(cfg[i].id)) sec->setDatasets(b.segments[i]);
}
}
void CategoryAnalysisTab::setStructure(const std::vector<geopro::data::StructNode>& nodes) {
for (auto& [id, sec] : sections_) sec->setStructure(nodes);
}
void CategoryAnalysisTab::refreshArrayFilters() {
for (auto& [id, sec] : sections_) sec->refreshArrayFilter();
}
CategorySection* CategoryAnalysisTab::section(const std::string& id) const {
const auto it = sections_.find(id);
return it != sections_.end() ? it->second : nullptr;
}
// ds 归属唯一段未知 → 广播到各段,仅含该 ds 的段会命中生效(其余 no-op
void CategoryAnalysisTab::setItemChecked(const QString& dsId, bool on) {
for (auto* sec : ordered_) sec->setChecked(dsId, on);
}
void CategoryAnalysisTab::setItemBusy(const QString& dsId, bool busy) {
for (auto* sec : ordered_) sec->setBusy(dsId, busy);
}
void CategoryAnalysisTab::clearAllBusy() {
for (auto* sec : ordered_) sec->clearAllBusy();
}
void CategoryAnalysisTab::scrollItemToTop(const QString& dsId) {
// 先就地展开所在段(同步),再进入多拍重试定位(等布局/滚动条范围结算)。
for (auto* sec : ordered_)
if (sec->itemFor(dsId)) { sec->ensureExpanded(); break; }
scrollItemToTopRetry(dsId, /*attemptsLeft=*/5);
}
void CategoryAnalysisTab::scrollItemToTopRetry(const QString& dsId, int attemptsLeft) {
if (!scroll_ || !content_) return;
CategorySection* sec = nullptr;
QTreeWidgetItem* item = nullptr;
for (auto* s : ordered_)
if ((item = s->itemFor(dsId)) != nullptr) { sec = s; break; }
if (sec && item) {
sec->ensureExpanded();
for (QTreeWidgetItem* p = item->parent(); p; p = p->parent())
p->setExpanded(true); // 展开树内父节点,使目标行有有效几何
QTreeWidget* tree = sec->listWidget();
tree->scrollToItem(item, QAbstractItemView::PositionAtTop); // 内层树(若有内滚动)
// 行顶映射到滚动内容坐标 → 设外层滚动条把该行顶到面板最上方。
const int y = tree->viewport()->mapTo(content_, tree->visualItemRect(item).topLeft()).y();
scroll_->verticalScrollBar()->setValue(y);
}
// 多拍重试:每拍布局更趋稳定(滚动条 range 长够、行几何更新),末拍稳定到位 → 根治"有时滚不到位"。
if (attemptsLeft > 0) {
QPointer<CategoryAnalysisTab> self(this);
const QString id = dsId;
QTimer::singleShot(16, this, [self, id, attemptsLeft]() {
if (self) self->scrollItemToTopRetry(id, attemptsLeft - 1);
});
}
}
void CategoryAnalysisTab::recomputeCheckedUnion() {
QStringList all; // ds 归属唯一段,跨段不重复,直接拼接
for (const auto& [id, ids] : checkedBySeg_) all += ids;
emit checkedDatasetsChanged(all);
}
} // namespace geopro::app