159 lines
7.5 KiB
C++
159 lines
7.5 KiB
C++
#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
|