diff --git a/src/gui/DxClusterDialog.cpp b/src/gui/DxClusterDialog.cpp index cb86c0596..0ce5e42e9 100644 --- a/src/gui/DxClusterDialog.cpp +++ b/src/gui/DxClusterDialog.cpp @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include #include #include @@ -55,6 +57,71 @@ class ResetOnDoubleClickSlider : public GuardedSlider { int m_resetValue{0}; }; +// Left-to-right layout that wraps to a new row when it runs out of +// horizontal space, rather than compressing children to illegibility +// (#4157). Used for the Spot List band-filter checkboxes so the +// checked state stays readable when SpotHub is narrow. +class FlowLayout : public QLayout { +public: + explicit FlowLayout(int margin, int hSpacing, int vSpacing) + : m_hSpace(hSpacing), m_vSpace(vSpacing) { + setContentsMargins(margin, margin, margin, margin); + } + ~FlowLayout() override { + while (QLayoutItem* item = takeAt(0)) + delete item; + } + + void addItem(QLayoutItem* item) override { m_items.append(item); } + Qt::Orientations expandingDirections() const override { return {}; } + bool hasHeightForWidth() const override { return true; } + int heightForWidth(int width) const override { return doLayout(QRect(0, 0, width, 0), true); } + int count() const override { return m_items.size(); } + QLayoutItem* itemAt(int index) const override { return m_items.value(index); } + QLayoutItem* takeAt(int index) override { + return (index >= 0 && index < m_items.size()) ? m_items.takeAt(index) : nullptr; + } + QSize sizeHint() const override { return minimumSize(); } + QSize minimumSize() const override { + QSize size; + for (QLayoutItem* item : m_items) + size = size.expandedTo(item->minimumSize()); + const QMargins m = contentsMargins(); + return size + QSize(m.left() + m.right(), m.top() + m.bottom()); + } + void setGeometry(const QRect& rect) override { + QLayout::setGeometry(rect); + doLayout(rect, false); + } + +private: + int doLayout(const QRect& rect, bool testOnly) const { + const QMargins m = contentsMargins(); + QRect effectiveRect = rect.adjusted(m.left(), m.top(), -m.right(), -m.bottom()); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + for (QLayoutItem* item : m_items) { + int nextX = x + item->sizeHint().width() + m_hSpace; + if (nextX - m_hSpace > effectiveRect.right() && lineHeight > 0) { + x = effectiveRect.x(); + y += lineHeight + m_vSpace; + nextX = x + item->sizeHint().width() + m_hSpace; + lineHeight = 0; + } + if (!testOnly) + item->setGeometry(QRect(QPoint(x, y), item->sizeHint())); + x = nextX; + lineHeight = qMax(lineHeight, item->sizeHint().height()); + } + return y + lineHeight - rect.y() + m.bottom(); + } + + QList m_items; + int m_hSpace; + int m_vSpace; +}; + // Shared DSP-style toggle for every checkable button in SpotHub // (matches kDspToggle in VfoWidget.cpp so the chrome reads the same as // the NB / NR / ANF buttons in the VFO panel). Dark inset by default, @@ -274,7 +341,11 @@ DxClusterDialog::DxClusterDialog(DxClusterClient* clusterClient, DxClusterClient m_radioModel(radioModel), m_dxccProvider(dxccProvider) { theme::setContainer(this, QStringLiteral("dialog/dxCluster")); - setMinimumSize(680, 560); + // Width floor lowered from 680 (#4157) so SpotHub can be dragged down + // toward the Spot List tab's visible-column width once columns are + // hidden; the other tabs' own layouts still enforce their own wider + // minimums via Qt's normal layout-constraint sizing. + setMinimumSize(360, 560); resize(760, 640); // Capture source log paths up front so the per-tab Clear handlers (built @@ -1857,9 +1928,9 @@ void DxClusterDialog::buildSpotListTab(QTabWidget* tabs) auto* layout = new QVBoxLayout(page); layout->setSpacing(4); - // Band filter checkboxes - auto* filterRow = new QHBoxLayout; - filterRow->setSpacing(2); + // Band filter checkboxes: wraps to additional rows instead of being + // compressed unreadable when SpotHub is narrow (#4157). + auto* filterRow = new FlowLayout(0, 8, 4); auto* filterLabel = new QLabel("Bands:"); AetherSDR::ThemeManager::instance().applyStyleSheet(filterLabel, "QLabel { color: {{color.text.label}}; font-size: 13px; }"); filterRow->addWidget(filterLabel); @@ -1891,7 +1962,7 @@ void DxClusterDialog::buildSpotListTab(QTabWidget* tabs) s.setValue(key, on ? "True" : "False"); s.save(); }); - filterRow->addWidget(cb, 1); // equal stretch across row + filterRow->addWidget(cb); } layout->addLayout(filterRow); @@ -1938,6 +2009,39 @@ void DxClusterDialog::buildSpotListTab(QTabWidget* tabs) // No default sort — insertion order is newest-first m_spotTable->horizontalHeader()->setSortIndicatorShown(false); + // Column visibility (#4157): Time/Freq/DX Call stay always-on as the + // core columns; the rest can be hidden via a right-click header menu, + // with the hidden set persisted in AppSettings across dialog reopen. + static const struct { SpotTableModel::Column col; const char* key; } kToggleCols[] = { + { SpotTableModel::ColComment, "SpotListColVisible_Comment" }, + { SpotTableModel::ColSpotter, "SpotListColVisible_Spotter" }, + { SpotTableModel::ColBand, "SpotListColVisible_Band" }, + { SpotTableModel::ColMode, "SpotListColVisible_Mode" }, + { SpotTableModel::ColSource, "SpotListColVisible_Source" }, + }; + auto& colVisSettings = AppSettings::instance(); + for (const auto& tc : kToggleCols) { + bool visible = colVisSettings.value(tc.key, "True").toString() == "True"; + m_spotTable->setColumnHidden(tc.col, !visible); + } + m_spotTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_spotTable->horizontalHeader(), &QWidget::customContextMenuRequested, this, + [this](const QPoint& pos) { + QMenu menu(this); + for (const auto& tc : kToggleCols) { + auto* action = menu.addAction(m_spotModel->headerData(tc.col, Qt::Horizontal, Qt::DisplayRole).toString()); + action->setCheckable(true); + action->setChecked(!m_spotTable->isColumnHidden(tc.col)); + connect(action, &QAction::toggled, this, [this, tc](bool on) { + m_spotTable->setColumnHidden(tc.col, !on); + auto& s = AppSettings::instance(); + s.setValue(tc.key, on ? "True" : "False"); + s.save(); + }); + } + menu.exec(m_spotTable->horizontalHeader()->mapToGlobal(pos)); + }); + // Double-click to tune (#2298: also forward mode hints so the receiver // switches CW/SSB to match the spot rather than only changing frequency). connect(m_spotTable, &QTableView::doubleClicked, this, [this](const QModelIndex& idx) {