feat(vtk): 段头响应式图标工具条 SectionIconBar(溢出折叠+单元测试)
This commit is contained in:
parent
e970ab428e
commit
b0da3c12fb
|
|
@ -84,6 +84,7 @@ add_executable(geopro_desktop WIN32
|
||||||
panels/columns/CategoryAnalysisTab.cpp
|
panels/columns/CategoryAnalysisTab.cpp
|
||||||
panels/columns/DateRangeEdit.cpp
|
panels/columns/DateRangeEdit.cpp
|
||||||
panels/columns/ColumnDrawer.cpp
|
panels/columns/ColumnDrawer.cpp
|
||||||
|
panels/columns/SectionIconBar.cpp
|
||||||
panels/AnomalyTablePanel.cpp
|
panels/AnomalyTablePanel.cpp
|
||||||
panels/LoadingOverlay.cpp
|
panels/LoadingOverlay.cpp
|
||||||
panels/DatasetDetailPage.cpp
|
panels/DatasetDetailPage.cpp
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
#include "panels/columns/SectionIconBar.hpp"
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QResizeEvent>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "Glyphs.hpp"
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
int visibleIconCount(int totalIcons, int availablePx, int iconPx, int overflowPx, int maxIcons) {
|
||||||
|
if (totalIcons <= 0 || iconPx <= 0) return 0;
|
||||||
|
const int ideal = std::min(totalIcons, std::max(0, maxIcons));
|
||||||
|
const bool overflowFromCap = totalIcons > ideal; // 超 max → 必有溢出位
|
||||||
|
// 先看理想数能否放下(若已因 cap 溢出,理想数也要含溢出位)
|
||||||
|
auto fits = [&](int n, bool withOverflow) {
|
||||||
|
return n * iconPx + (withOverflow ? overflowPx : 0) <= availablePx;
|
||||||
|
};
|
||||||
|
int n = ideal;
|
||||||
|
bool overflow = overflowFromCap;
|
||||||
|
while (n > 0 && !fits(n, overflow || (totalIcons > n))) {
|
||||||
|
--n;
|
||||||
|
overflow = true; // 一旦减少必有「…」
|
||||||
|
}
|
||||||
|
if (n < 0) n = 0;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int kGlyphPx = 18; // glyph 绘制像素;按钮本体宽用 iconPx_,与溢出计算口径一致
|
||||||
|
|
||||||
|
// glyphKey 字符串 → Glyph 枚举(C2 按段头操作填键;未知键回退中性图标)。
|
||||||
|
Glyph glyphFromKey(const QString& key) {
|
||||||
|
const QString k = key.toLower();
|
||||||
|
if (k == QStringLiteral("plus")) return Glyph::Plus;
|
||||||
|
if (k == QStringLiteral("filter")) return Glyph::Filter;
|
||||||
|
if (k == QStringLiteral("upload")) return Glyph::Upload;
|
||||||
|
if (k == QStringLiteral("download")) return Glyph::Download;
|
||||||
|
if (k == QStringLiteral("collapse")) return Glyph::Collapse;
|
||||||
|
if (k == QStringLiteral("fullscreen")) return Glyph::Fullscreen;
|
||||||
|
if (k == QStringLiteral("map")) return Glyph::Map;
|
||||||
|
if (k == QStringLiteral("detail")) return Glyph::Detail;
|
||||||
|
if (k == QStringLiteral("property")) return Glyph::Property;
|
||||||
|
if (k == QStringLiteral("gear")) return Glyph::Gear;
|
||||||
|
return Glyph::Property;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SectionIconBar::SectionIconBar(QWidget* parent) : QWidget(parent) {
|
||||||
|
auto* lay = new QHBoxLayout(this);
|
||||||
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
lay->setSpacing(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SectionIconBar::setActions(const std::vector<IconAction>& a) {
|
||||||
|
// 清旧按钮与溢出按钮
|
||||||
|
for (auto* b : btns_) {
|
||||||
|
if (b) b->deleteLater();
|
||||||
|
}
|
||||||
|
btns_.clear();
|
||||||
|
if (overflowBtn_) {
|
||||||
|
overflowBtn_->deleteLater();
|
||||||
|
overflowBtn_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
actions_ = a;
|
||||||
|
auto* lay = qobject_cast<QHBoxLayout*>(layout());
|
||||||
|
const QColor ic = tokenColor("text/secondary");
|
||||||
|
|
||||||
|
for (const IconAction& act : actions_) {
|
||||||
|
auto* b = new QToolButton(this);
|
||||||
|
b->setIcon(makeGlyph(glyphFromKey(act.glyphKey), ic, kGlyphPx));
|
||||||
|
b->setIconSize(QSize(kGlyphPx, kGlyphPx));
|
||||||
|
b->setAutoRaise(true);
|
||||||
|
b->setToolTip(act.tooltip);
|
||||||
|
b->setFixedSize(iconPx_, iconPx_);
|
||||||
|
if (act.popupBuilder) {
|
||||||
|
const auto builder = act.popupBuilder;
|
||||||
|
QToolButton* host = b;
|
||||||
|
connect(b, &QToolButton::clicked, this, [builder, host] { builder(host); });
|
||||||
|
} else if (act.onClick) {
|
||||||
|
const auto cb = act.onClick;
|
||||||
|
connect(b, &QToolButton::clicked, this, [cb] { cb(); });
|
||||||
|
}
|
||||||
|
if (lay) lay->addWidget(b);
|
||||||
|
btns_.push_back(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 末尾「…」溢出按钮:即点即弹菜单
|
||||||
|
overflowBtn_ = new QToolButton(this);
|
||||||
|
overflowBtn_->setText(QStringLiteral("…"));
|
||||||
|
overflowBtn_->setAutoRaise(true);
|
||||||
|
overflowBtn_->setToolTip(QStringLiteral("更多"));
|
||||||
|
overflowBtn_->setFixedSize(overflowPx_, iconPx_);
|
||||||
|
overflowBtn_->setPopupMode(QToolButton::InstantPopup);
|
||||||
|
overflowBtn_->setMenu(new QMenu(overflowBtn_));
|
||||||
|
if (lay) lay->addWidget(overflowBtn_);
|
||||||
|
|
||||||
|
relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SectionIconBar::resizeEvent(QResizeEvent* e) {
|
||||||
|
QWidget::resizeEvent(e);
|
||||||
|
relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SectionIconBar::relayout() {
|
||||||
|
const int total = static_cast<int>(actions_.size());
|
||||||
|
const int vis = visibleIconCount(total, width(), iconPx_, overflowPx_, maxIcons_);
|
||||||
|
|
||||||
|
QMenu* menu = overflowBtn_ ? overflowBtn_->menu() : nullptr;
|
||||||
|
if (menu) menu->clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < total; ++i) {
|
||||||
|
QToolButton* b = btns_[static_cast<size_t>(i)];
|
||||||
|
if (!b) continue;
|
||||||
|
if (i < vis) {
|
||||||
|
b->setVisible(true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
b->setVisible(false);
|
||||||
|
if (!menu) continue;
|
||||||
|
const IconAction& act = actions_[static_cast<size_t>(i)];
|
||||||
|
QAction* ma = menu->addAction(act.tooltip);
|
||||||
|
if (act.popupBuilder) {
|
||||||
|
const auto builder = act.popupBuilder;
|
||||||
|
QToolButton* host = overflowBtn_;
|
||||||
|
connect(ma, &QAction::triggered, this, [builder, host] { builder(host); });
|
||||||
|
} else if (act.onClick) {
|
||||||
|
const auto cb = act.onClick;
|
||||||
|
connect(ma, &QAction::triggered, this, [cb] { cb(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overflowBtn_) overflowBtn_->setVisible(vis < total);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QString>
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class QToolButton;
|
||||||
|
class QResizeEvent;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 纯逻辑:给定约束返回可见图标数(其余收进「…」)。见 spec §6。
|
||||||
|
int visibleIconCount(int totalIcons, int availablePx, int iconPx, int overflowPx, int maxIcons);
|
||||||
|
|
||||||
|
// 段头响应式图标工具条:≤maxIcons 且宽度够则全显;否则右侧依次收进末尾「…」下拉。
|
||||||
|
struct IconAction {
|
||||||
|
QString glyphKey; // 图标键(映射到 Glyph)
|
||||||
|
QString tooltip;
|
||||||
|
std::function<void()> onClick; // 直接动作;为空则用 popupBuilder
|
||||||
|
std::function<void(QToolButton*)> popupBuilder; // 弹 popup(z值/底图/筛选用)
|
||||||
|
};
|
||||||
|
|
||||||
|
class SectionIconBar : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit SectionIconBar(QWidget* parent = nullptr);
|
||||||
|
void setActions(const std::vector<IconAction>& actions); // 重建按钮
|
||||||
|
void setMaxIcons(int n) { maxIcons_ = n; relayout(); }
|
||||||
|
protected:
|
||||||
|
void resizeEvent(QResizeEvent* e) override;
|
||||||
|
private:
|
||||||
|
void relayout(); // 按当前宽度算可见数,多余进「…」菜单
|
||||||
|
std::vector<IconAction> actions_;
|
||||||
|
std::vector<QToolButton*> btns_;
|
||||||
|
QToolButton* overflowBtn_ = nullptr;
|
||||||
|
int maxIcons_ = 3;
|
||||||
|
int iconPx_ = 30;
|
||||||
|
int overflowPx_ = 30;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -184,6 +184,16 @@ target_sources(geopro_tests PRIVATE
|
||||||
)
|
)
|
||||||
# 对象树勾选纯逻辑(GS 三态聚合 aggregateGsState + 勾选源去重 dedupeSources,header-only)。
|
# 对象树勾选纯逻辑(GS 三态聚合 aggregateGsState + 勾选源去重 dedupeSources,header-only)。
|
||||||
target_sources(geopro_tests PRIVATE app/test_object_tree_selection.cpp)
|
target_sources(geopro_tests PRIVATE app/test_object_tree_selection.cpp)
|
||||||
|
# 段头响应式图标工具条溢出计算纯逻辑(visibleIconCount,见 spec §6)。SectionIconBar.cpp 还含
|
||||||
|
# QWidget 组件,其按钮构造依赖 makeGlyph(Glyphs.cpp) 与主题(Theme.cpp),故一并加入并链 Qt6::Widgets/Svg。
|
||||||
|
find_package(Qt6 COMPONENTS Widgets Svg REQUIRED)
|
||||||
|
target_sources(geopro_tests PRIVATE
|
||||||
|
app/test_section_icon_bar.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/app/panels/columns/SectionIconBar.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/app/Glyphs.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/app/Theme.cpp
|
||||||
|
)
|
||||||
|
target_link_libraries(geopro_tests PRIVATE Qt6::Widgets Qt6::Svg)
|
||||||
# measurement 散点纯逻辑(值类型变换 / 显隐 id 收集 / 过滤体 / 另存体,Qt6::Core JSON + core model)。
|
# measurement 散点纯逻辑(值类型变换 / 显隐 id 收集 / 过滤体 / 另存体,Qt6::Core JSON + core model)。
|
||||||
target_sources(geopro_tests PRIVATE
|
target_sources(geopro_tests PRIVATE
|
||||||
app/test_scatter_data_ops.cpp
|
app/test_scatter_data_ops.cpp
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include "panels/columns/SectionIconBar.hpp"
|
||||||
|
|
||||||
|
using geopro::app::visibleIconCount;
|
||||||
|
|
||||||
|
TEST(SectionIconBar, ShowsAllWhenWideEnoughAndUnderMax) {
|
||||||
|
// 3 图标, 宽 1000, 每个 30, 溢出 30, max 3 → 全显
|
||||||
|
EXPECT_EQ(visibleIconCount(3, 1000, 30, 30, 3), 3);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, CapsAtMaxIcons) {
|
||||||
|
// 5 图标但 max 3, 宽足够 → 显 3, 其余 2 进溢出(此时需留溢出位)
|
||||||
|
EXPECT_EQ(visibleIconCount(5, 1000, 30, 30, 3), 3);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, FoldsRightWhenNarrow) {
|
||||||
|
// 3 图标, max 3, 但宽只够 2 个 + 溢出: 75px, 30 each, overflow 30 → 2*30+30=90>75 → 1*30+30=60<=75 → 1
|
||||||
|
EXPECT_EQ(visibleIconCount(3, 75, 30, 30, 3), 1);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, NoOverflowReserveWhenAllFit) {
|
||||||
|
// 2 图标全显且 <=max, 不需溢出位: 宽 60 恰好 2*30
|
||||||
|
EXPECT_EQ(visibleIconCount(2, 60, 30, 30, 3), 2);
|
||||||
|
}
|
||||||
|
TEST(SectionIconBar, ZeroWhenTooNarrow) {
|
||||||
|
EXPECT_EQ(visibleIconCount(3, 20, 30, 30, 3), 0);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue