Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix waterfall tooltips and click to restore view #1392

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions src/applications/gqrx/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ MainWindow::MainWindow(const QString& cfgfile, bool edit_conf, QWidget *parent)
connect(ui->plotter, SIGNAL(newSize()), this, SLOT(setWfSize()));
connect(ui->plotter, SIGNAL(markerSelectA(qint64)), this, SLOT(setMarkerA(qint64)));
connect(ui->plotter, SIGNAL(markerSelectB(qint64)), this, SLOT(setMarkerB(qint64)));
connect(ui->plotter, SIGNAL(newCenterFrequency(qint64)), this, SLOT(setNewFrequency(qint64)));

// Bookmarks
connect(uiDockBookmarks, SIGNAL(newBookmarkActivated(qint64, QString, int)), this, SLOT(onBookmarkActivated(qint64, QString, int)));
Expand Down
155 changes: 101 additions & 54 deletions src/qtgui/plotter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ CPlotter::CPlotter(QWidget *parent) : QFrame(parent)
tlast_wf_ms = 0;
tlast_plot_drawn_ms = 0;
tlast_wf_drawn_ms = 0;
wf_valid_since_ms = 0;
msec_per_wfline = 0;
tlast_peaks_ms = 0;
wf_epoch = 0;
Expand Down Expand Up @@ -327,21 +326,20 @@ void CPlotter::mouseMoveEvent(QMouseEvent* event)
}
if (m_TooltipsEnabled)
{
const quint64 line_ms = msecFromY(py);
QString timeStr;
if (line_ms >= wf_valid_since_ms)
const WaterfallEntry waterfallEntry = getWaterfallEntry(py - h);
const quint64 ms = waterfallEntry.m_TimestampMs;
if (ms > 0)
{
QDateTime tt;
tt.setMSecsSinceEpoch(msecFromY(py));
timeStr = tt.toString("yyyy.MM.dd hh:mm:ss.zzz");
tt.setMSecsSinceEpoch(ms);
QString timeStr = tt.toString("yyyy.MM.dd hh:mm:ss.zzz");
const qreal kHz = waterfallFreqFromX(waterfallEntry, px) / 1.e3;
showToolTip(event, QString("%1\n%2 kHz").arg(timeStr).arg(kHz, 0, 'f', 3));
}
else{
timeStr = "[time not valid]";
else
{
QToolTip::hideText();
}

showToolTip(event, QString("%1\n%2 kHz")
.arg(timeStr)
.arg(freqFromX(px)/1.e3, 0, 'f', 3));
}
}
// process mouse moves while in cursor capture modes
Expand Down Expand Up @@ -592,7 +590,6 @@ void CPlotter::setWaterfallSpan(quint64 span_ms)
wf_count = 0;
msec_per_wfline = (double)wf_span / (qreal)m_WaterfallImage.height();
}
wf_valid_since_ms = tnow;
clearWaterfallBuf();
}

Expand All @@ -615,41 +612,78 @@ quint64 CPlotter::getWfTimeRes() const
void CPlotter::setFftRate(int rate_hz)
{
fft_rate = rate_hz;
wf_valid_since_ms = QDateTime::currentMSecsSinceEpoch();
clearWaterfallBuf();
}

// Called when a mouse button is pressed
void CPlotter::mousePressEvent(QMouseEvent * event)
{
QPoint pt = event->pos();
int h = m_OverlayPixmap.height();
int px = qRound((qreal)pt.x() * m_DPR);
int py = qRound((qreal)pt.y() * m_DPR);
QPoint ppos = QPoint(px, py);

if (NOCAP == m_CursorCaptured)
{
if (isPointCloseTo(px, m_DemodFreqX, m_CursorCaptureDelta))
const int dy = py - h;
if (dy > 0)
{
// move demod box center frequency region
// left click in waterfall resets view at point in history
const WaterfallEntry waterfallEntry = getWaterfallEntry(dy);
if (event->buttons() != Qt::LeftButton || waterfallEntry.m_TimestampMs == 0)
{
return;
}
if (m_CenterFreq != waterfallEntry.m_CenterFreq)
{
emit newCenterFrequency(waterfallEntry.m_CenterFreq + (m_DemodCenterFreq - m_CenterFreq));
}
m_DemodCenterFreq = roundFreq(waterfallFreqFromX(waterfallEntry, px), m_ClickResolution);
bool invalidate = false;
if (m_FftCenter != waterfallEntry.m_FftCenter)
{
invalidate = true;
m_FftCenter = waterfallEntry.m_FftCenter;
}
if (m_Span != waterfallEntry.m_Span)
{
invalidate = true;
m_Span = waterfallEntry.m_Span;
double zoom = (double)m_SampleFreq / (double)m_Span;
emit newZoomLevel(zoom);
}
if (invalidate) {
m_MaxHoldValid = false;
m_MinHoldValid = false;
m_histIIRValid = false;
}
emit newDemodFreq(m_DemodCenterFreq, m_DemodCenterFreq - m_CenterFreq);
m_CursorCaptured = CENTER;
m_GrabPosition = px - m_DemodFreqX;
}
else if (isPointCloseTo(px, m_DemodLowCutFreqX, m_CursorCaptureDelta))
{
// filter low cut
m_CursorCaptured = LEFT;
m_GrabPosition = px - m_DemodLowCutFreqX;
}
else if (isPointCloseTo(px, m_DemodHiCutFreqX, m_CursorCaptureDelta))
{
// filter high cut
m_CursorCaptured = RIGHT;
m_GrabPosition = px - m_DemodHiCutFreqX;
m_GrabPosition = 1;
updateOverlay();
}
else
{
if (event->buttons() == Qt::LeftButton)
if (isPointCloseTo(px, m_DemodFreqX, m_CursorCaptureDelta))
{
// move demod box center frequency region
m_CursorCaptured = CENTER;
m_GrabPosition = px - m_DemodFreqX;
}
else if (isPointCloseTo(px, m_DemodLowCutFreqX, m_CursorCaptureDelta))
{
// filter low cut
m_CursorCaptured = LEFT;
m_GrabPosition = px - m_DemodLowCutFreqX;
}
else if (isPointCloseTo(px, m_DemodHiCutFreqX, m_CursorCaptureDelta))
{
// filter high cut
m_CursorCaptured = RIGHT;
m_GrabPosition = px - m_DemodHiCutFreqX;
}
else if (event->buttons() == Qt::LeftButton)
{
// {shift|ctrl|ctrl-shift}-left-click: set ab markers around signal at cursor
quint32 mods = event->modifiers() & (Qt::ShiftModifier|Qt::ControlModifier);
Expand Down Expand Up @@ -869,7 +903,7 @@ void CPlotter::zoomStepX(float step, int x)
// Explicitly set m_Span instead of calling setSpanFreq(), which also calls
// setFftCenterFreq() and updateOverlay() internally. Span needs to be set
// before frequency limits can be checked in setFftCenterFreq().
m_Span = new_span;
m_Span = new_span_int;
setFftCenterFreq(qRound64((f_max + f_min) / 2.0f));

m_MaxHoldValid = false;
Expand Down Expand Up @@ -1032,6 +1066,7 @@ void CPlotter::resizeEvent(QResizeEvent* )
if (wfHeight == 0)
{
m_WaterfallImage = QImage();
m_WaterfallEntries = std::vector<WaterfallEntry>(0);
}

// New waterfall, create blank area
Expand All @@ -1040,6 +1075,7 @@ void CPlotter::resizeEvent(QResizeEvent* )
m_WaterfallImage.setDevicePixelRatio(m_DPR);
m_WaterfallImage.fill(Qt::black);
m_WaterfallOffset = wfHeight;
m_WaterfallEntries = std::vector<WaterfallEntry>(wfHeight);
}

// Existing waterfall, rescale width but no height as that would
Expand All @@ -1053,13 +1089,19 @@ void CPlotter::resizeEvent(QResizeEvent* )
m_WaterfallImage = QImage(w, wfHeight, QImage::Format_RGB32);
m_WaterfallImage.setDevicePixelRatio(m_DPR);
m_WaterfallImage.fill(Qt::black);
std::vector<WaterfallEntry> newEntries(wfHeight);
const int firstHeight = std::min(wfHeight, wfHeightOld - m_WaterfallOffset);
memcpy(m_WaterfallImage.scanLine(0), oldWaterfall.scanLine(m_WaterfallOffset),
m_WaterfallImage.bytesPerLine() * firstHeight);
memcpy(&newEntries[0], &m_WaterfallEntries[m_WaterfallOffset],
sizeof(WaterfallEntry) * firstHeight);
const int secondHeight = std::min(wfHeight - firstHeight, m_WaterfallOffset);
memcpy(m_WaterfallImage.scanLine(firstHeight), oldWaterfall.scanLine(0),
m_WaterfallImage.bytesPerLine() * secondHeight);
memcpy(&newEntries[firstHeight], &m_WaterfallEntries[0],
sizeof(WaterfallEntry) * secondHeight);
m_WaterfallOffset = wfHeight;
m_WaterfallEntries = newEntries;
}

// Invalidate on resize
Expand Down Expand Up @@ -1422,17 +1464,20 @@ void CPlotter::draw(bool newData)

// cursor times are relative to last time drawn
tlast_wf_ms = tnow_ms;
if (wf_valid_since_ms == 0)
wf_valid_since_ms = tnow_ms;
tlast_wf_drawn_ms = tnow_ms;

// move the offset "up"
// this changes how the resulting waterfall is drawn
// it is more efficient than moving all of the image scan lines
m_WaterfallOffset--;
// draw new line of fft data at top of waterfall bitmap
// draw black areas where data will not be draw
// draw black areas where data will not be drawn
memset(m_WaterfallImage.scanLine(m_WaterfallOffset), 0, m_WaterfallImage.bytesPerLine());
WaterfallEntry& waterfallEntry = m_WaterfallEntries[m_WaterfallOffset];
waterfallEntry.m_TimestampMs = tnow_ms;
waterfallEntry.m_CenterFreq = m_CenterFreq;
waterfallEntry.m_FftCenter = m_FftCenter;
waterfallEntry.m_Span = m_Span;

const bool useWfBuf = msec_per_wfline > 0;
float _lineFactor;
Expand Down Expand Up @@ -1987,17 +2032,17 @@ void CPlotter::drawOverlay()
static const qreal nLevels = h / (levelHeight + slant);
if (m_BookmarksEnabled)
{
tags = Bookmarks::Get().getBookmarksInRange(m_CenterFreq + m_FftCenter - m_Span / 2,
m_CenterFreq + m_FftCenter + m_Span / 2);
tags = Bookmarks::Get().getBookmarksInRange(getMinFrequency(),
getMaxFrequency());
}
else
{
tags.clear();
}
if (m_DXCSpotsEnabled)
{
QList<DXCSpotInfo> dxcspots = DXCSpots::Get().getDXCSpotsInRange(m_CenterFreq + m_FftCenter - m_Span / 2,
m_CenterFreq + m_FftCenter + m_Span / 2);
QList<DXCSpotInfo> dxcspots = DXCSpots::Get().getDXCSpotsInRange(getMinFrequency(),
getMaxFrequency());
QListIterator<DXCSpotInfo> iter(dxcspots);
while(iter.hasNext())
{
Expand Down Expand Up @@ -2059,8 +2104,8 @@ void CPlotter::drawOverlay()

if (m_BandPlanEnabled)
{
QList<BandInfo> bands = BandPlan::Get().getBandsInRange(m_CenterFreq + m_FftCenter - m_Span / 2,
m_CenterFreq + m_FftCenter + m_Span / 2);
QList<BandInfo> bands = BandPlan::Get().getBandsInRange(getMinFrequency(),
getMaxFrequency());

m_BandPlanHeight = metrics.height() + VER_MARGIN;
for (auto & band : bands)
Expand Down Expand Up @@ -2125,7 +2170,7 @@ void CPlotter::drawOverlay()
}

// Frequency grid
qint64 StartFreq = m_CenterFreq + m_FftCenter - m_Span / 2;
qint64 StartFreq = getMinFrequency();
QString label;
label.setNum(float((StartFreq + m_Span) / m_FreqUnits), 'f', m_FreqDigits);
calcDivSize(StartFreq, StartFreq + m_Span,
Expand Down Expand Up @@ -2314,21 +2359,23 @@ qint64 CPlotter::freqFromX(int x)
return f;
}

/** Calculate time offset of a given line on the waterfall */
quint64 CPlotter::msecFromY(int y)
WaterfallEntry CPlotter::getWaterfallEntry(int waterfallY)
{
int h = m_OverlayPixmap.height();

// ensure we are in the waterfall region
if (y < h)
return 0;

qreal dy = (qreal)y - (qreal)h;
int idx = m_WaterfallOffset + waterfallY;
int waterfallHeight = m_WaterfallImage.height();
if (idx >= waterfallHeight)
{
idx -= waterfallHeight;
}
return m_WaterfallEntries[idx];
}

if (msec_per_wfline > 0)
return tlast_wf_drawn_ms - dy * msec_per_wfline;
else
return tlast_wf_drawn_ms - dy * getWfTimeRes();
qint64 CPlotter::waterfallFreqFromX(WaterfallEntry waterfallEntry, int x) {
const qreal ratio = (qreal) x / (qreal) m_WaterfallImage.width();
const qint64 centerFrequency = waterfallEntry.m_CenterFreq + waterfallEntry.m_FftCenter;
const qint64 frequencySpan = waterfallEntry.m_Span;
const qint64 minFrequency = centerFrequency - frequencySpan / 2;
return qRound(minFrequency + ratio * frequencySpan);
}

// Round frequency to click resolution value
Expand Down
22 changes: 20 additions & 2 deletions src/qtgui/plotter.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@

#define MARKER_OFF std::numeric_limits<qint64>::min()

struct WaterfallEntry {
quint64 m_TimestampMs;
qint64 m_CenterFreq;
qint64 m_FftCenter;
qint64 m_Span;
};

class CPlotter : public QFrame
{
Q_OBJECT
Expand Down Expand Up @@ -103,6 +110,7 @@ class CPlotter : public QFrame
if (rate > 0.0f)
{
m_SampleFreq = rate;
m_WaterfallEntries = std::vector<WaterfallEntry>(m_WaterfallImage.height());
updateOverlay();
}
}
Expand Down Expand Up @@ -156,6 +164,7 @@ class CPlotter : public QFrame
void newSize();
void markerSelectA(qint64 freq);
void markerSelectB(qint64 freq);
void newCenterFrequency(qint64 freq);

public slots:
// zoom functions
Expand Down Expand Up @@ -213,13 +222,22 @@ public slots:
MARKER_B
};

qint64 getMinFrequency() const {
return m_CenterFreq + m_FftCenter - m_Span / 2;
}

qint64 getMaxFrequency() const {
return m_CenterFreq + m_FftCenter + m_Span / 2;
}

void drawOverlay();
void makeFrequencyStrs();
int xFromFreq(qint64 freq);
qint64 freqFromX(int x);
void zoomStepX(float factor, int x);
static qint64 roundFreq(qint64 freq, int resolution);
quint64 msecFromY(int y);
WaterfallEntry getWaterfallEntry(int waterfallY);
qint64 waterfallFreqFromX(WaterfallEntry waterfallEntry, int x);
void clampDemodParameters();
static QColor blend(QColor base, QColor over, int alpha255)
{
Expand Down Expand Up @@ -273,6 +291,7 @@ public slots:
QPixmap m_PeakPixmap;
QImage m_WaterfallImage;
int m_WaterfallOffset;
std::vector<WaterfallEntry> m_WaterfallEntries;
QColor m_ColorTbl[256];
QSize m_Size;
qreal m_DPR{};
Expand Down Expand Up @@ -352,7 +371,6 @@ public slots:
quint64 tlast_wf_ms; // last time waterfall has been updated
quint64 tlast_plot_drawn_ms;// last time the plot was drawn
quint64 tlast_wf_drawn_ms; // last time waterfall was drawn
quint64 wf_valid_since_ms; // last time before action that invalidates time line
double msec_per_wfline{}; // milliseconds between waterfall updates
quint64 wf_epoch; // msec time of last waterfal rate change
quint64 wf_count; // waterfall lines drawn since last rate change
Expand Down
Loading