Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 184 additions & 45 deletions src/gui/SpectrumWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,55 @@ static constexpr int kDbmReleaseHoldFrames = 10;
static constexpr int kDbmReleaseErrorSampleCount = 256;
static constexpr float kDbmReleasePreviewChangeThresholdDb = 0.05f;
static constexpr float kDbmReleaseRebaseMinImprovementDb = 0.75f;
static constexpr int kFilterEdgeGrabPx = 8;
static constexpr int kFilterPassbandMinBodyPx = 6;

static int filterInteriorGrabPx(int loX, int hiX, int grabPx)
{
const int widthPx = std::abs(hiX - loX);
if (widthPx <= kFilterPassbandMinBodyPx) {
return 0;
}
return std::min(grabPx, (widthPx - kFilterPassbandMinBodyPx - 1) / 2);
}

static int filterEdgeHitAtPixel(int mx, int loX, int hiX, int grabPx)
{
const int left = std::min(loX, hiX);
const int right = std::max(loX, hiX);
const bool insidePassband = mx >= left && mx <= right;
if (insidePassband && right - left <= kFilterPassbandMinBodyPx) {
return 0;
}

const int insideGrabPx = filterInteriorGrabPx(loX, hiX, grabPx);
const bool lowIsLeft = loX <= hiX;
const bool lowHit = lowIsLeft
? (mx >= loX - grabPx && mx <= loX + insideGrabPx)
: (mx >= loX - insideGrabPx && mx <= loX + grabPx);
const bool highHit = lowIsLeft
? (mx >= hiX - insideGrabPx && mx <= hiX + grabPx)
: (mx >= hiX - grabPx && mx <= hiX + insideGrabPx);

if (lowHit && highHit) {
return (std::abs(mx - loX) <= std::abs(mx - hiX)) ? -1 : 1;
}
if (lowHit) {
return -1;
}
if (highHit) {
return 1;
}
return 0;
}

static bool filterPassbandBodyHitAtPixel(int mx, int loX, int hiX, int grabPx)
{
const int left = std::min(loX, hiX);
const int right = std::max(loX, hiX);
return mx >= left && mx <= right
&& filterEdgeHitAtPixel(mx, loX, hiX, grabPx) == 0;
}

static constexpr int lineDurationToRatePercent(int lineDurationMs)
{
Expand Down Expand Up @@ -801,6 +850,7 @@ VfoWidget* SpectrumWidget::addVfoWidget(int sliceId)
return m_vfoWidgets[sliceId];

auto* w = new VfoWidget(this);
installVfoCursorEventFilter(w);
m_vfoWidgets[sliceId] = w;
w->show();
w->raise();
Expand All @@ -810,6 +860,19 @@ VfoWidget* SpectrumWidget::addVfoWidget(int sliceId)
return w;
}

void SpectrumWidget::installVfoCursorEventFilter(VfoWidget* widget)
{
if (!widget) {
return;
}

widget->installEventFilter(this);
const QList<QWidget*> children = widget->findChildren<QWidget*>();
for (QWidget* child : children) {
child->installEventFilter(this);
}
}

void SpectrumWidget::removeVfoWidget(int sliceId)
{
if (auto* w = m_vfoWidgets.take(sliceId)) {
Expand Down Expand Up @@ -3442,6 +3505,55 @@ void SpectrumWidget::setSpectrumCursor(Qt::CursorShape shape)
setCursor(shape);
}

bool SpectrumWidget::sliceCursorShapeAt(const QPoint& localPos,
Qt::CursorShape& shape) const
{
const int chromeH = freqScaleH() + DIVIDER_H;
const int contentH = height() - chromeH;
const int specH = static_cast<int>(contentH * m_spectrumFrac);
const int mx = localPos.x();
const int y = localPos.y();
if (y < 0 || y >= specH || mx < 0 || mx >= width() - DBM_STRIP_W) {
return false;
}

for (const auto& so : m_sliceOverlays) {
if (so.isActive) {
continue;
}
const int sliceX = mhzToX(so.freqMhz);
if ((mx >= sliceX - 8 && mx <= sliceX + 35 && y <= 25)
|| std::abs(mx - sliceX) <= 8) {
shape = Qt::PointingHandCursor;
return true;
}

const int loX = mhzToX(so.freqMhz + so.filterLowHz / 1.0e6);
const int hiX = mhzToX(so.freqMhz + so.filterHighHz / 1.0e6);
const int left = std::min(loX, hiX);
const int right = std::max(loX, hiX);
if (mx >= left && mx <= right) {
shape = Qt::OpenHandCursor;
return true;
}
}

if (const auto* ao = activeOverlay()) {
const int loX = mhzToX(ao->freqMhz + ao->filterLowHz / 1.0e6);
const int hiX = mhzToX(ao->freqMhz + ao->filterHighHz / 1.0e6);
if (filterEdgeHitAtPixel(mx, loX, hiX, kFilterEdgeGrabPx) != 0) {
shape = Qt::SizeHorCursor;
return true;
}
if (filterPassbandBodyHitAtPixel(mx, loX, hiX, kFilterEdgeGrabPx)) {
shape = Qt::OpenHandCursor;
return true;
}
}

return false;
}

// ─── Mouse ────────────────────────────────────────────────────────────────────

// Snap a frequency (MHz) to the nearest multiple of m_stepHz.
Expand Down Expand Up @@ -3912,7 +4024,7 @@ void SpectrumWidget::mousePressEvent(QMouseEvent* ev)
m_draggingVfo = true;
m_vfoDragOffsetHz = static_cast<int>(
std::round((xToMhz(mx) - so.freqMhz) * 1.0e6));
setSpectrumCursor(Qt::SizeHorCursor);
setSpectrumCursor(Qt::ClosedHandCursor);
ev->accept();
return;
}
Expand All @@ -3928,18 +4040,10 @@ void SpectrumWidget::mousePressEvent(QMouseEvent* ev)
const int mx = static_cast<int>(ev->position().x());
const int loX = mhzToX(ao->freqMhz + ao->filterLowHz / 1.0e6);
const int hiX = mhzToX(ao->freqMhz + ao->filterHighHz / 1.0e6);
constexpr int GRAB = 8;

const bool loHit = std::abs(mx - loX) <= GRAB;
const bool hiHit = std::abs(mx - hiX) <= GRAB;
if (loHit || hiHit) {
// When both edges are within grab range, pick the closer one (#764)
if (loHit && hiHit)
m_draggingFilter = (std::abs(mx - loX) <= std::abs(mx - hiX))
? FilterEdge::Low : FilterEdge::High;
else
m_draggingFilter = loHit ? FilterEdge::Low : FilterEdge::High;

const int edgeHit = filterEdgeHitAtPixel(mx, loX, hiX, kFilterEdgeGrabPx);
if (edgeHit != 0) {
m_draggingFilter = edgeHit < 0 ? FilterEdge::Low : FilterEdge::High;
// Store anchor offset so the edge doesn't snap to cursor (#764)
const int edgeHz = (m_draggingFilter == FilterEdge::Low) ? ao->filterLowHz : ao->filterHighHz;
m_filterDragStartX = mx;
Expand All @@ -3951,12 +4055,10 @@ void SpectrumWidget::mousePressEvent(QMouseEvent* ev)
}

// Click inside the filter passband → start VFO drag (#404)
const int left = std::min(loX, hiX);
const int right = std::max(loX, hiX);
if (mx > left + GRAB && mx < right - GRAB) {
if (filterPassbandBodyHitAtPixel(mx, loX, hiX, kFilterEdgeGrabPx)) {
m_draggingVfo = true;
m_vfoDragOffsetHz = static_cast<int>(std::round((xToMhz(mx) - ao->freqMhz) * 1.0e6));
setSpectrumCursor(Qt::SizeHorCursor);
setSpectrumCursor(Qt::ClosedHandCursor);
ev->accept();
return;
}
Expand Down Expand Up @@ -3986,20 +4088,23 @@ void SpectrumWidget::mouseMoveEvent(QMouseEvent* ev)
const auto dragStatePublisher = makeScopeExit([this] { publishPerfDragState(); });
(void)dragStatePublisher;

// Let child widgets (overlay menu buttons) own the pointer while hovered so
// their tooltips are not killed by SpectrumWidget's QToolTip::hideText() calls.
// Guard is skipped during active drags — those always start in the spectrum
// area, not over child widgets. (#2355)
if (!anyDragActive() && childAt(ev->position().toPoint())) {
ev->ignore();
return;
}

const int chromeH = freqScaleH() + DIVIDER_H;
const int contentH = height() - chromeH;
const int specH = static_cast<int>(contentH * m_spectrumFrac);
const int y = static_cast<int>(ev->position().y());
const int mx = static_cast<int>(ev->position().x());
const QPoint mousePos = ev->position().toPoint();
Qt::CursorShape sliceCursorShape = Qt::ArrowCursor;
const bool overSliceCursorTarget = sliceCursorShapeAt(mousePos, sliceCursorShape);

// Let child widgets (overlay menu buttons) own the pointer while hovered so
// their tooltips are not killed by SpectrumWidget's QToolTip::hideText() calls.
// Slice passband/edge zones are still owned by the spectrum because their
// cursor advertises the drag action before click. (#2355)
if (!anyDragActive() && childAt(mousePos) && !overSliceCursorTarget) {
ev->ignore();
return;
}

// TNF drag
if (m_draggingTnfId >= 0) {
Expand Down Expand Up @@ -4279,27 +4384,11 @@ void SpectrumWidget::mouseMoveEvent(QMouseEvent* ev)
foundCursor = true;
}
}
if (const auto* ao = activeOverlay()) {
const int loX = mhzToX(ao->freqMhz + ao->filterLowHz / 1.0e6);
const int hiX = mhzToX(ao->freqMhz + ao->filterHighHz / 1.0e6);
constexpr int GRAB = 5;
if (!foundCursor
&& (std::abs(mx - loX) <= GRAB || std::abs(mx - hiX) <= GRAB)) {
setSpectrumCursor(Qt::SizeHorCursor);
foundCursor = true;
}
}
if (!foundCursor) {
// Check inactive slice markers + badges
for (const auto& so : m_sliceOverlays) {
if (so.isActive) continue;
int sliceX = mhzToX(so.freqMhz);
if ((mx >= sliceX - 8 && mx <= sliceX + 35 && y <= 25)
|| std::abs(mx - sliceX) <= 8) {
setSpectrumCursor(Qt::PointingHandCursor);
foundCursor = true;
break;
}
Qt::CursorShape cursorShape = Qt::ArrowCursor;
if (sliceCursorShapeAt(pos, cursorShape)) {
setSpectrumCursor(cursorShape);
foundCursor = true;
}
}
if (!foundCursor && m_showSpots) {
Expand Down Expand Up @@ -4723,6 +4812,50 @@ bool SpectrumWidget::event(QEvent* ev)
return SPECTRUM_BASE_CLASS::event(ev);
}

bool SpectrumWidget::eventFilter(QObject* watched, QEvent* event)
{
QWidget* widget = qobject_cast<QWidget*>(watched);
if (!widget || anyDragActive()) {
return SPECTRUM_BASE_CLASS::eventFilter(watched, event);
}

bool vfoDescendant = false;
for (QWidget* current = widget; current && current != this;
current = current->parentWidget()) {
if (qobject_cast<VfoWidget*>(current)) {
vfoDescendant = true;
break;
}
}

if (!vfoDescendant) {
return SPECTRUM_BASE_CLASS::eventFilter(watched, event);
}

QPoint localPos;
bool hasPosition = false;
if (event->type() == QEvent::MouseMove) {
auto* mouseEvent = static_cast<QMouseEvent*>(event);
localPos = mapFromGlobal(mouseEvent->globalPosition().toPoint());
hasPosition = true;
} else if (event->type() == QEvent::Enter) {
auto* enterEvent = static_cast<QEnterEvent*>(event);
localPos = mapFromGlobal(enterEvent->globalPosition().toPoint());
hasPosition = true;
}

if (hasPosition) {
Qt::CursorShape cursorShape = Qt::ArrowCursor;
if (sliceCursorShapeAt(localPos, cursorShape)) {
setSpectrumCursor(cursorShape);
} else {
setSpectrumCursor(Qt::CrossCursor);
}
}

return SPECTRUM_BASE_CLASS::eventFilter(watched, event);
}

// ─── Starstruck easter egg ────────────────────────────────────────────────────

void SpectrumWidget::ensureStarstruckSoundLoaded()
Expand Down Expand Up @@ -5349,6 +5482,12 @@ void SpectrumWidget::renderGpuFrame(QRhiCommandBuffer* cb)
QPoint localPos = mapFromGlobal(QCursor::pos());
if (rect().contains(localPos)) {
updateTrackedCursorState(localPos, true);
if (!anyDragActive()) {
Qt::CursorShape cursorShape = Qt::ArrowCursor;
if (sliceCursorShapeAt(localPos, cursorShape)) {
setSpectrumCursor(cursorShape);
}
}
} else if (m_cursorPos.x() >= 0 || m_hoveredTnfId >= 0 || m_tuneGuideVisible) {
// Mouse left the widget without a leaveEvent
updateTrackedCursorState(QPoint(-1, -1), false);
Expand Down
4 changes: 3 additions & 1 deletion src/gui/SpectrumWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ class SpectrumWidget : public SPECTRUM_BASE_CLASS {
void mouseDoubleClickEvent(QMouseEvent* event) override;
void wheelEvent(QWheelEvent* event) override;
bool event(QEvent* event) override;
bool eventFilter(QObject* watched, QEvent* event) override;
void leaveEvent(QEvent* event) override;

public:
Expand All @@ -552,6 +553,8 @@ class SpectrumWidget : public SPECTRUM_BASE_CLASS {
QColor tnfFillColor(const TnfMarker& tnf) const;
QColor tnfLineColor(const TnfMarker& tnf) const;
int tnfAtPixel(int x, int preferredId = -1) const;
bool sliceCursorShapeAt(const QPoint& localPos, Qt::CursorShape& shape) const;
void installVfoCursorEventFilter(VfoWidget* widget);
void setSpectrumCursor(Qt::CursorShape shape);
void updateTrackedCursorState(const QPoint& localPos, bool insideWidget);
void updateTnfHoverPopup();
Expand Down Expand Up @@ -1100,4 +1103,3 @@ class SpectrumWidget : public SPECTRUM_BASE_CLASS {
};

} // namespace AetherSDR

Loading