diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 9f06ef4..ef60b00 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -84,6 +84,7 @@ add_executable(geopro_desktop WIN32 panels/columns/CategoryAnalysisTab.cpp panels/columns/DateRangeEdit.cpp panels/columns/ColumnDrawer.cpp + panels/columns/SectionIconBar.cpp panels/AnomalyTablePanel.cpp panels/LoadingOverlay.cpp panels/DatasetDetailPage.cpp diff --git a/src/app/panels/columns/SectionIconBar.cpp b/src/app/panels/columns/SectionIconBar.cpp new file mode 100644 index 0000000..9a03dda --- /dev/null +++ b/src/app/panels/columns/SectionIconBar.cpp @@ -0,0 +1,143 @@ +#include "panels/columns/SectionIconBar.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#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& a) { + // 清旧按钮与溢出按钮 + for (auto* b : btns_) { + if (b) b->deleteLater(); + } + btns_.clear(); + if (overflowBtn_) { + overflowBtn_->deleteLater(); + overflowBtn_ = nullptr; + } + + actions_ = a; + auto* lay = qobject_cast(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(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(i)]; + if (!b) continue; + if (i < vis) { + b->setVisible(true); + continue; + } + b->setVisible(false); + if (!menu) continue; + const IconAction& act = actions_[static_cast(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 diff --git a/src/app/panels/columns/SectionIconBar.hpp b/src/app/panels/columns/SectionIconBar.hpp new file mode 100644 index 0000000..706a1bd --- /dev/null +++ b/src/app/panels/columns/SectionIconBar.hpp @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include +#include + +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 onClick; // 直接动作;为空则用 popupBuilder + std::function popupBuilder; // 弹 popup(z值/底图/筛选用) +}; + +class SectionIconBar : public QWidget { + Q_OBJECT +public: + explicit SectionIconBar(QWidget* parent = nullptr); + void setActions(const std::vector& actions); // 重建按钮 + void setMaxIcons(int n) { maxIcons_ = n; relayout(); } +protected: + void resizeEvent(QResizeEvent* e) override; +private: + void relayout(); // 按当前宽度算可见数,多余进「…」菜单 + std::vector actions_; + std::vector btns_; + QToolButton* overflowBtn_ = nullptr; + int maxIcons_ = 3; + int iconPx_ = 30; + int overflowPx_ = 30; +}; + +} // namespace geopro::app diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c4316c2..41454f2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -184,6 +184,16 @@ target_sources(geopro_tests PRIVATE ) # 对象树勾选纯逻辑(GS 三态聚合 aggregateGsState + 勾选源去重 dedupeSources,header-only)。 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)。 target_sources(geopro_tests PRIVATE app/test_scatter_data_ops.cpp diff --git a/tests/app/test_section_icon_bar.cpp b/tests/app/test_section_icon_bar.cpp new file mode 100644 index 0000000..980bcc1 --- /dev/null +++ b/tests/app/test_section_icon_bar.cpp @@ -0,0 +1,24 @@ +#include +#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); +}