diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef929f88..5e750968 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,21 +16,35 @@ env: jobs: build: strategy: + fail-fast: false matrix: - os: ['macos-latest', 'ubuntu-22.04', 'ubuntu-20.04'] + os: ['macos-latest', 'ubuntu-24.04', 'ubuntu-22.04'] + qt: ['qt5', 'qt6'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 + - name: Set variables Qt5 + run: | + echo "QTPKG_MAC=qt@5" >> $GITHUB_ENV + echo "QTPKG_UBUNTU=qtbase5-dev" >> $GITHUB_ENV + if: matrix.qt == 'qt5' + + - name: Set variables Qt6 + run: | + echo "QTPKG_MAC=qt@6" >> $GITHUB_ENV + echo "QTPKG_UBUNTU=qt6-base-dev" >> $GITHUB_ENV + if: matrix.qt == 'qt6' + - name: Install dependencies (macOS) - run: brew install fftw liquid-dsp qt@5 + run: brew install fftw liquid-dsp ${{ env.QTPKG_MAC }} if: matrix.os == 'macos-latest' - name: Install dependencies (Ubuntu) run: | sudo apt update - sudo apt install libfftw3-dev libliquid-dev qtbase5-dev + sudo apt install libfftw3-dev libliquid-dev libgl1-mesa-dev ${{ env.QTPKG_UBUNTU }} if: startsWith(matrix.os, 'ubuntu-') - name: Create Build Environment diff --git a/CMakeLists.txt b/CMakeLists.txt index 538987ba..95e166c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.6) +cmake_minimum_required(VERSION 3.5) project(inspectrum CXX) enable_testing() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 298ff870..06b2383d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,8 +53,10 @@ list(APPEND inspectrum_sources util.cpp ) -find_package(Qt5Widgets REQUIRED) -find_package(Qt5Concurrent REQUIRED) +find_package(Qt6 COMPONENTS Core Concurrent Widgets) +if (NOT Qt6_FOUND) + find_package(Qt5 REQUIRED COMPONENTS Core Concurrent Widgets) +endif() find_package(FFTW REQUIRED) find_package(Liquid REQUIRED) @@ -65,11 +67,19 @@ include_directories( add_executable(inspectrum ${EXE_ARGS} ${inspectrum_sources}) -target_link_libraries(inspectrum - Qt5::Core Qt5::Widgets Qt5::Concurrent - ${FFTW_LIBRARIES} - ${LIQUID_LIBRARIES} -) +if (Qt6_FOUND) + target_link_libraries(inspectrum + Qt6::Core Qt6::Widgets Qt6::Concurrent + ${FFTW_LIBRARIES} + ${LIQUID_LIBRARIES} + ) +else() + target_link_libraries(inspectrum + Qt5::Core Qt5::Widgets Qt5::Concurrent + ${FFTW_LIBRARIES} + ${LIQUID_LIBRARIES} + ) +endif() set(INSTALL_DEFAULT_BINDIR "bin" CACHE STRING "Appended to CMAKE_INSTALL_PREFIX") diff --git a/src/cursor.cpp b/src/cursor.cpp index b9ac71d0..6352a954 100644 --- a/src/cursor.cpp +++ b/src/cursor.cpp @@ -37,24 +37,24 @@ bool Cursor::pointOverCursor(QPoint point) return range.contains(fromPoint(point)); } -bool Cursor::mouseEvent(QEvent::Type type, QMouseEvent event) +bool Cursor::mouseEvent(QEvent::Type type, QMouseEvent *event) { // If the mouse pointer moves over a cursor, display a resize pointer - if (pointOverCursor(event.pos()) && type != QEvent::Leave) { - if (!cursorOverrided) { - cursorOverrided = true; + if (pointOverCursor(event->pos())) { + if (!cursorOverriden) { + cursorOverriden = true; QApplication::setOverrideCursor(QCursor(cursorShape)); } - // Restore pointer if it moves off the cursor, or leaves the widget - } else if (cursorOverrided) { - cursorOverrided = false; + // Restore pointer if it moves off the cursor + } else if (cursorOverriden) { + cursorOverriden = false; QApplication::restoreOverrideCursor(); } // Start dragging on left mouse button press, if over a cursor if (type == QEvent::MouseButtonPress) { - if (event.button() == Qt::LeftButton) { - if (pointOverCursor(event.pos())) { + if (event->button() == Qt::LeftButton) { + if (pointOverCursor(event->pos())) { dragging = true; return true; } @@ -63,13 +63,13 @@ bool Cursor::mouseEvent(QEvent::Type type, QMouseEvent event) // Update current cursor position if we're dragging } else if (type == QEvent::MouseMove) { if (dragging) { - cursorPosition = fromPoint(event.pos()); + cursorPosition = fromPoint(event->pos()); emit posChanged(); } // Stop dragging on left mouse button release } else if (type == QEvent::MouseButtonRelease) { - if (event.button() == Qt::LeftButton && dragging) { + if (event->button() == Qt::LeftButton && dragging) { dragging = false; return true; } @@ -77,6 +77,14 @@ bool Cursor::mouseEvent(QEvent::Type type, QMouseEvent event) return false; } +void Cursor::leaveEvent() +{ + if (cursorOverriden) { + cursorOverriden = false; + QApplication::restoreOverrideCursor(); + } +} + int Cursor::pos() { return cursorPosition; diff --git a/src/cursor.h b/src/cursor.h index aceb6089..76451ed6 100644 --- a/src/cursor.h +++ b/src/cursor.h @@ -32,7 +32,8 @@ class Cursor : public QObject Cursor(Qt::Orientation orientation, Qt::CursorShape mouseCursorShape, QObject * parent); int pos(); void setPos(int newPos); - bool mouseEvent(QEvent::Type type, QMouseEvent event); + bool mouseEvent(QEvent::Type type, QMouseEvent *event); + void leaveEvent(); signals: void posChanged(); @@ -44,6 +45,6 @@ class Cursor : public QObject Qt::Orientation orientation; Qt::CursorShape cursorShape; bool dragging = false; - bool cursorOverrided = false; + bool cursorOverriden = false; int cursorPosition = 0; }; diff --git a/src/cursors.cpp b/src/cursors.cpp index 8087f031..66eaadf8 100644 --- a/src/cursors.cpp +++ b/src/cursors.cpp @@ -44,15 +44,15 @@ bool Cursors::pointWithinDragRegion(QPoint point) { return range.contains(point.x()); } -bool Cursors::mouseEvent(QEvent::Type type, QMouseEvent event) +bool Cursors::mouseEvent(QEvent::Type type, QMouseEvent *event) { if (minCursor->mouseEvent(type, event)) return true; if (maxCursor->mouseEvent(type, event)) - return true; + return true; // If the mouse pointer is between the cursors, display a resize pointer - if (pointWithinDragRegion(event.pos()) && type != QEvent::Leave) { + if (pointWithinDragRegion(event->pos()) ) { if (!cursorOverride) { cursorOverride = true; QApplication::setOverrideCursor(QCursor(Qt::SizeAllCursor)); @@ -66,25 +66,25 @@ bool Cursors::mouseEvent(QEvent::Type type, QMouseEvent event) } // Start dragging on left mouse button press, if between the cursors if (type == QEvent::MouseButtonPress) { - if (event.button() == Qt::LeftButton) { - if (pointWithinDragRegion(event.pos())) { + if (event->button() == Qt::LeftButton) { + if (pointWithinDragRegion(event->pos())) { dragging = true; - dragPos = event.pos(); + dragPos = event->pos(); return true; } } // Update both cursor positons if we're dragging } else if (type == QEvent::MouseMove) { if (dragging) { - int dx = event.pos().x() - dragPos.x(); + int dx = event->pos().x() - dragPos.x(); minCursor->setPos(minCursor->pos() + dx); maxCursor->setPos(maxCursor->pos() + dx); - dragPos = event.pos(); + dragPos = event->pos(); emit cursorsMoved(); } // Stop dragging on left mouse button release } else if (type == QEvent::MouseButtonRelease) { - if (event.button() == Qt::LeftButton && dragging) { + if (event->button() == Qt::LeftButton && dragging) { dragging = false; return true; } @@ -92,6 +92,17 @@ bool Cursors::mouseEvent(QEvent::Type type, QMouseEvent event) return false; } +void Cursors::leaveEvent() +{ + minCursor->leaveEvent(); + maxCursor->leaveEvent(); + + if (cursorOverride) { + cursorOverride = false; + QApplication::restoreOverrideCursor(); + } +} + void Cursors::paintFront(QPainter &painter, QRect &rect, range_t sampleRange) { painter.save(); diff --git a/src/cursors.h b/src/cursors.h index 9e7e5728..84356d1d 100644 --- a/src/cursors.h +++ b/src/cursors.h @@ -33,7 +33,8 @@ class Cursors : public QObject public: Cursors(QObject * parent); int segments(); - bool mouseEvent(QEvent::Type type, QMouseEvent event); + bool mouseEvent(QEvent::Type type, QMouseEvent *event); + void leaveEvent(); void paintFront(QPainter &painter, QRect &rect, range_t sampleRange); range_t selection(); void setSegments(int segments); diff --git a/src/inputsource.cpp b/src/inputsource.cpp index f1645908..edf14549 100644 --- a/src/inputsource.cpp +++ b/src/inputsource.cpp @@ -348,7 +348,9 @@ QJsonObject InputSource::readMetaData(const QString &filename) auto sigmf_color = sigmf_annotation["presentation:color"].toString(); // SigMF uses the format "#RRGGBBAA" for alpha-channel colors, QT uses "#AARRGGBB" - if ((sigmf_color.at(0) == '#') && (sigmf_color.length()) == 9) { + // Check length first so the empty/short-string case short-circuits before at(0): + // in Qt6 an empty QString has a null data pointer and at(0) would crash. + if ((sigmf_color.length() == 9) && (sigmf_color.at(0) == '#')) { sigmf_color = "#" + sigmf_color.mid(7,2) + sigmf_color.mid(1,6); } auto boxColor = QString::fromStdString("white"); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f872ee07..e96875bb 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -74,12 +74,13 @@ void MainWindow::openFile(QString fileName) // Try to parse osmocom_fft filenames and extract the sample rate and center frequency. // Example file name: "name-f2.411200e+09-s5.000000e+06-t20160807180210.cfile" - QRegExp rx("(.*)-f(.*)-s(.*)-.*\\.cfile"); + QRegularExpression rx(QRegularExpression::anchoredPattern("(.*)-f(.*)-s(.*)-.*\\.cfile")); QString basename = fileName.section('/',-1,-1); - if (rx.exactMatch(basename)) { - QString centerfreq = rx.cap(2); - QString samplerate = rx.cap(3); + auto match = rx.match(basename); + if (match.hasMatch()) { + QString centerfreq = match.captured(2); + QString samplerate = match.captured(3); std::stringstream ss(samplerate.toUtf8().constData()); diff --git a/src/plot.cpp b/src/plot.cpp index f2e327d1..af2fa031 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -34,11 +34,16 @@ void Plot::invalidateEvent() } -bool Plot::mouseEvent(QEvent::Type type, QMouseEvent event) +bool Plot::mouseEvent(QEvent::Type type, QMouseEvent *event) { return false; } +void Plot::leaveEvent() +{ + +} + std::shared_ptr Plot::output() { return sampleSource; diff --git a/src/plot.h b/src/plot.h index aac98417..94ccf607 100644 --- a/src/plot.h +++ b/src/plot.h @@ -33,7 +33,8 @@ class Plot : public QObject, public Subscriber Plot(std::shared_ptr src); ~Plot(); void invalidateEvent() override; - virtual bool mouseEvent(QEvent::Type type, QMouseEvent event); + virtual bool mouseEvent(QEvent::Type type, QMouseEvent *event); + virtual void leaveEvent(); virtual std::shared_ptr output(); virtual void paintBack(QPainter &painter, QRect &rect, range_t sampleRange); virtual void paintMid(QPainter &painter, QRect &rect, range_t sampleRange); diff --git a/src/plotview.cpp b/src/plotview.cpp index b79fab97..4fbf4b56 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -124,6 +124,10 @@ void PlotView::addPlot(Plot *plot) { plots.emplace_back(plot); connect(plot, &Plot::repaint, this, &PlotView::repaint); + // Seed any derived TracePlot with the global samples-per-pixel so it + // shares the spectrogram's x-axis from the moment it's added. + if (auto trace = dynamic_cast(plot)) + trace->setSamplesPerColumn(samplesPerColumn()); } void PlotView::addSpectrumPlot() @@ -210,7 +214,11 @@ void PlotView::updateAnnotationTooltip(QMouseEvent *event) } else { QString* comment = spectrogramPlot->mouseAnnotationComment(event); if (comment != nullptr) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QToolTip::showText(event->globalPosition().toPoint(), *comment); +#else QToolTip::showText(event->globalPos(), *comment); +#endif } else { QToolTip::hideText(); } @@ -442,22 +450,26 @@ bool PlotView::viewportEvent(QEvent *event) { // Pass mouse events to individual plot objects if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseMove || - event->type() == QEvent::MouseButtonRelease || - event->type() == QEvent::Leave) { + event->type() == QEvent::MouseButtonRelease) { QMouseEvent *mouseEvent = static_cast(event); int plotY = -verticalScrollBar()->value(); for (auto&& plot : plots) { + auto mouse_event = QMouseEvent( + event->type(), +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QPoint(mouseEvent->position().x(), mouseEvent->position().y() - plotY), +#else + QPoint(mouseEvent->pos().x(), mouseEvent->pos().y() - plotY), +#endif + mouseEvent->button(), + mouseEvent->buttons(), + QApplication::keyboardModifiers() + ); bool result = plot->mouseEvent( event->type(), - QMouseEvent( - event->type(), - QPoint(mouseEvent->pos().x(), mouseEvent->pos().y() - plotY), - mouseEvent->button(), - mouseEvent->buttons(), - QApplication::keyboardModifiers() - ) + &mouse_event ); if (result) return true; @@ -465,10 +477,19 @@ bool PlotView::viewportEvent(QEvent *event) { } if (cursorsEnabled) - if (cursors.mouseEvent(event->type(), *mouseEvent)) + if (cursors.mouseEvent(event->type(), mouseEvent)) return true; } + if (event->type() == QEvent::Leave) { + for (auto&& plot : plots) { + plot->leaveEvent(); + } + + if (cursorsEnabled) + cursors.leaveEvent(); + } + // Handle parent eveents return QGraphicsView::viewportEvent(event); } @@ -829,6 +850,14 @@ void PlotView::setFFTAndZoom(int size, int zoom) if (verticalScrollBar()->maximum() == 0) oldPlotCenter = 0.5; + // Pin the cursors to absolute SAMPLE positions across the zoom change. + // The cursors store their position in viewport pixels; after zoom those + // pixels span a different sample count, so the symbol-rate / period + // readout would silently drift just because the user zoomed. Save the + // samples now and re-place the cursors on the new pixel grid below. + bool reanchorCursors = cursorsEnabled; + range_t savedCursorSamples = selectedSamples; + // Set new FFT size fftSize = size; if (spectrogramPlot != nullptr) @@ -842,6 +871,13 @@ void PlotView::setFFTAndZoom(int size, int zoom) spectrogramPlot->setSkip(nfftSkip); } + // Propagate the new samples-per-pixel to any TracePlot (envelope, IFR, + // phase, threshold, etc.) so they redraw at the spectrogram's x-scale. + for (auto&& p : plots) { + if (auto trace = dynamic_cast(p.get())) + trace->setSamplesPerColumn(samplesPerColumn()); + } + // Update horizontal (time) scrollbar horizontalScrollBar()->setSingleStep(10); horizontalScrollBar()->setPageStep(100); @@ -851,6 +887,22 @@ void PlotView::setFFTAndZoom(int size, int zoom) // maintain the relative position of the vertical scroll bar if (verticalScrollBar()->maximum()) verticalScrollBar()->setValue((int )(oldPlotCenter * plotsHeight() - viewport()->height() / 2.0 + 0.5f)); + + // Re-place the cursors at their saved sample positions on the new pixel + // grid. cursors.setSelection() takes viewport-pixel coordinates, so we + // subtract the new scrollbar value to get there. updateView()'s own + // cursor refresh runs from selectedSamples too but can be perturbed by + // the cursorsMoved() signal cascade that fires during setSelection; this + // final write nails the canonical value back down. + if (reanchorCursors) { + int sb = horizontalScrollBar()->value(); + int minPx = static_cast(sampleToColumn(savedCursorSamples.minimum)) - sb; + int maxPx = static_cast(sampleToColumn(savedCursorSamples.maximum)) - sb; + cursors.setSelection({minPx, maxPx}); + selectedSamples = savedCursorSamples; + emitTimeSelection(); + viewport()->update(); + } } void PlotView::setPowerMin(int power) diff --git a/src/spectrogramplot.cpp b/src/spectrogramplot.cpp index b8469d1b..1492e07d 100644 --- a/src/spectrogramplot.cpp +++ b/src/spectrogramplot.cpp @@ -343,13 +343,30 @@ float* SpectrogramPlot::getFFTTile(size_t tile) void SpectrogramPlot::getLine(float *dest, size_t sample) { if (inputSource && fft) { - // Make sample be the midpoint of the FFT, unless this takes us - // past the beginning of the inputSource (if we remove the - // std::max(ยท, 0), then an ugly red bar appears at the beginning - // of the spectrogram with large zooms and FFT sizes). - const auto first_sample = std::max(static_cast(sample) - fftSize / 2, - static_cast(0)); - auto buffer = inputSource->getSamples(first_sample, fftSize); + // Centre the FFT on `sample`. Clamp at the head of the file (otherwise + // an ugly red bar appears with large zooms and FFT sizes) and at the + // tail too -- previously when first_sample + fftSize exceeded EOF, + // getSamples returned null and we filled the column with -inf. That + // made the spectrogram visibly end fftSize/2 samples short of the + // trace plots' right edge. Zero-pad the partial buffer instead so the + // spectrogram and the trace plots stop at the same x-pixel. + const ssize_t available = static_cast(inputSource->count()); + ssize_t first_sample = static_cast(sample) - fftSize / 2; + if (first_sample < 0) + first_sample = 0; + + if (first_sample >= available) { + auto neg_infinity = -1 * std::numeric_limits::infinity(); + for (int i = 0; i < fftSize; i++, dest++) + *dest = neg_infinity; + return; + } + + ssize_t want = fftSize; + if (first_sample + want > available) + want = available - first_sample; + + auto buffer = inputSource->getSamples(first_sample, want); if (buffer == nullptr) { auto neg_infinity = -1 * std::numeric_limits::infinity(); for (int i = 0; i < fftSize; i++, dest++) @@ -357,6 +374,17 @@ void SpectrogramPlot::getLine(float *dest, size_t sample) return; } + // Zero-pad to fftSize when getSamples returned a partial buffer at + // EOF. Cheaper than resizing in place -- we just need contiguous + // storage of length fftSize for the FFT. + if (want < fftSize) { + auto padded = std::make_unique[]>(fftSize); + std::copy(buffer.get(), buffer.get() + want, padded.get()); + for (ssize_t i = want; i < fftSize; i++) + padded[i] = std::complex(0.0f, 0.0f); + buffer = std::move(padded); + } + for (int i = 0; i < fftSize; i++) { buffer[i] *= window[i]; } @@ -457,7 +485,7 @@ int SpectrogramPlot::linesPerTile() return tileSize / fftSize; } -bool SpectrogramPlot::mouseEvent(QEvent::Type type, QMouseEvent event) +bool SpectrogramPlot::mouseEvent(QEvent::Type type, QMouseEvent *event) { if (tunerEnabled()) return tuner.mouseEvent(type, event); @@ -465,6 +493,12 @@ bool SpectrogramPlot::mouseEvent(QEvent::Type type, QMouseEvent event) return false; } +void SpectrogramPlot::leaveEvent() +{ + if (tunerEnabled()) + tuner.leaveEvent(); +} + std::shared_ptr SpectrogramPlot::output() { return tunerTransform; diff --git a/src/spectrogramplot.h b/src/spectrogramplot.h index a185c162..12ace4c2 100644 --- a/src/spectrogramplot.h +++ b/src/spectrogramplot.h @@ -33,9 +33,33 @@ #include #include -class TileCacheKey; class AnnotationLocation; + +class TileCacheKey +{ + +public: + TileCacheKey(int fftSize, int zoomLevel, int nfftSkip, size_t sample) { + this->fftSize = fftSize; + this->zoomLevel = zoomLevel; + this->nfftSkip = nfftSkip; + this->sample = sample; + } + + bool operator==(const TileCacheKey &k2) const { + return (this->fftSize == k2.fftSize) && + (this->zoomLevel == k2.zoomLevel) && + (this->nfftSkip == k2.nfftSkip) && + (this->sample == k2.sample); + } + + int fftSize; + int zoomLevel; + int nfftSkip; + size_t sample; +}; + class SpectrogramPlot : public Plot { Q_OBJECT @@ -46,7 +70,8 @@ class SpectrogramPlot : public Plot std::shared_ptr output() override; void paintFront(QPainter &painter, QRect &rect, range_t sampleRange) override; void paintMid(QPainter &painter, QRect &rect, range_t sampleRange) override; - bool mouseEvent(QEvent::Type type, QMouseEvent event) override; + bool mouseEvent(QEvent::Type type, QMouseEvent *event) override; + void leaveEvent(); std::shared_ptr>> input() { return inputSource; }; double getCenterFrequency(); double frequencyAt(int y); @@ -122,30 +147,6 @@ public slots: void paintDecimationOverlay(QPainter &painter, QRect &rect); }; -class TileCacheKey -{ - -public: - TileCacheKey(int fftSize, int zoomLevel, int nfftSkip, size_t sample) { - this->fftSize = fftSize; - this->zoomLevel = zoomLevel; - this->nfftSkip = nfftSkip; - this->sample = sample; - } - - bool operator==(const TileCacheKey &k2) const { - return (this->fftSize == k2.fftSize) && - (this->zoomLevel == k2.zoomLevel) && - (this->nfftSkip == k2.nfftSkip) && - (this->sample == k2.sample); - } - - int fftSize; - int zoomLevel; - int nfftSkip; - size_t sample; -}; - class AnnotationLocation { public: diff --git a/src/traceplot.cpp b/src/traceplot.cpp index 95b63476..239cb3a1 100644 --- a/src/traceplot.cpp +++ b/src/traceplot.cpp @@ -32,11 +32,19 @@ void TracePlot::paintMid(QPainter &painter, QRect &rect, range_t sampleR { if (sampleRange.length() == 0) return; - int samplesPerColumn = std::max(1UL, sampleRange.length() / rect.width()); - int samplesPerTile = tileWidth * samplesPerColumn; + // Prefer the explicit samples-per-column set by PlotView so we share the + // same x-axis scale as the spectrogram (governed by fftSize / zoomLevel). + // Computing it locally as sampleRange.length() / rect.width() goes wrong + // when viewRange is clamped to a small file -- the trace stretches across + // the full viewport while the spectrogram fills only the leading pixels, + // and the two panels visibly disagree on the time axis. + int spc = samplesPerColumn > 0 + ? samplesPerColumn + : std::max(1, int(sampleRange.length() / rect.width())); + int samplesPerTile = tileWidth * spc; size_t tileID = sampleRange.minimum / samplesPerTile; size_t tileOffset = sampleRange.minimum % samplesPerTile; // Number of samples to skip from first image tile - int xOffset = tileOffset / samplesPerColumn; // Number of columns to skip from first image tile + int xOffset = tileOffset / spc; // Number of columns to skip from first image tile // Paint first (possibly partial) tile painter.drawPixmap( @@ -64,7 +72,11 @@ QPixmap TracePlot::getTile(size_t tileID, size_t sampleCount) if (!tasks.contains(key)) { range_t sampleRange{tileID * sampleCount, (tileID + 1) * sampleCount}; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QtConcurrent::run(&TracePlot::drawTile, this, key, QRect(0, 0, tileWidth, height()), sampleRange); +#else QtConcurrent::run(this, &TracePlot::drawTile, key, QRect(0, 0, tileWidth, height()), sampleRange); +#endif tasks.insert(key); } pixmap.fill(Qt::transparent); @@ -82,25 +94,55 @@ void TracePlot::drawTile(QString key, const QRect &rect, range_t sampleR auto firstSample = sampleRange.minimum; auto length = sampleRange.length(); + // Clamp the request to what the source actually has, shrinking the draw + // rect proportionally. Without this, at high zoom samplesPerTile can + // exceed the file size; getSamples then returns nullptr, drawTile bails + // before emitting an image, and the tile stays fully transparent -- the + // "trace plot disappears at high zoom" symptom. The spectrogram already + // handles this case (zero-pads to -inf); TracePlot was the outlier. + QRect drawRect = rect; + auto clampToAvailable = [&](size_t available) -> bool { + if (firstSample >= available) + return false; + if (firstSample + length > available) { + size_t valid = available - firstSample; + drawRect.setWidth(int(double(rect.width()) * valid / length)); + length = valid; + } + return true; + }; + // Is it a 2-channel (complex) trace? if (auto src = dynamic_cast>*>(sampleSource.get())) { + if (!clampToAvailable(src->count())) { + emit imageReady(key, image); + return; + } auto samples = src->getSamples(firstSample, length); - if (samples == nullptr) + if (samples == nullptr) { + emit imageReady(key, image); return; + } painter.setPen(Qt::red); - plotTrace(painter, rect, reinterpret_cast(samples.get()), length, 2); + plotTrace(painter, drawRect, reinterpret_cast(samples.get()), length, 2); painter.setPen(Qt::blue); - plotTrace(painter, rect, reinterpret_cast(samples.get())+1, length, 2); + plotTrace(painter, drawRect, reinterpret_cast(samples.get())+1, length, 2); // Otherwise is it single channel? } else if (auto src = dynamic_cast*>(sampleSource.get())) { + if (!clampToAvailable(src->count())) { + emit imageReady(key, image); + return; + } auto samples = src->getSamples(firstSample, length); - if (samples == nullptr) + if (samples == nullptr) { + emit imageReady(key, image); return; + } painter.setPen(Qt::green); - plotTrace(painter, rect, samples.get(), length, 1); + plotTrace(painter, drawRect, samples.get(), length, 1); } else { throw std::runtime_error("TracePlot::paintMid: Unsupported source type"); } diff --git a/src/traceplot.h b/src/traceplot.h index fea74fcb..22f27fa3 100644 --- a/src/traceplot.h +++ b/src/traceplot.h @@ -32,6 +32,7 @@ class TracePlot : public Plot void paintMid(QPainter &painter, QRect &rect, range_t sampleRange); std::shared_ptr source() { return sampleSource; }; + void setSamplesPerColumn(int spc) { samplesPerColumn = std::max(1, spc); } signals: void imageReady(QString key, QImage image); @@ -42,6 +43,11 @@ public slots: private: QSet tasks; const int tileWidth = 1000; + // Samples per pixel, fed in by PlotView so we stay aligned with the + // spectrogram (which uses fftSize / zoomLevel). 0 falls back to the legacy + // "stretch sampleRange to fill rect" behaviour for back-compat with any + // future TracePlot not driven by PlotView. + int samplesPerColumn = 0; QPixmap getTile(size_t tileID, size_t sampleCount); void drawTile(QString key, const QRect &rect, range_t sampleRange); diff --git a/src/tuner.cpp b/src/tuner.cpp index 6ccac61c..2dbc144d 100644 --- a/src/tuner.cpp +++ b/src/tuner.cpp @@ -63,7 +63,7 @@ int Tuner::deviation() return _deviation; } -bool Tuner::mouseEvent(QEvent::Type type, QMouseEvent event) +bool Tuner::mouseEvent(QEvent::Type type, QMouseEvent *event) { if (cfCursor->mouseEvent(type, event)) return true; @@ -75,6 +75,13 @@ bool Tuner::mouseEvent(QEvent::Type type, QMouseEvent event) return false; } +void Tuner::leaveEvent() +{ + cfCursor->leaveEvent(); + minCursor->leaveEvent(); + maxCursor->leaveEvent(); +} + void Tuner::paintFront(QPainter &painter, QRect &rect, range_t sampleRange) { painter.save(); diff --git a/src/tuner.h b/src/tuner.h index 958d9a74..175dbba1 100644 --- a/src/tuner.h +++ b/src/tuner.h @@ -34,7 +34,8 @@ class Tuner : public QObject Tuner(int height, QObject * parent); int centre(); int deviation(); - bool mouseEvent(QEvent::Type, QMouseEvent event); + bool mouseEvent(QEvent::Type, QMouseEvent *event); + void leaveEvent(); void paintFront(QPainter &painter, QRect &rect, range_t sampleRange); void setCentre(int centre); void setDeviation(int dev);