From 456ce147f3af4e1fb191b422954eeeaa9b807fdb Mon Sep 17 00:00:00 2001 From: Mike Walters Date: Sun, 30 Nov 2025 23:41:58 +0000 Subject: [PATCH 01/14] Bump CMake minimum --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 462a6826..95e166c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.5) project(inspectrum CXX) enable_testing() From 7fcf01294013c653745b1edf47daf39b62d99b4e Mon Sep 17 00:00:00 2001 From: Mike Walters Date: Sun, 30 Nov 2025 23:45:26 +0000 Subject: [PATCH 02/14] actions: remove Ubuntu 20.04, add 24.04 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef929f88..75a3a15d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: build: strategy: matrix: - os: ['macos-latest', 'ubuntu-22.04', 'ubuntu-20.04'] + os: ['macos-latest', 'ubuntu-24.04', 'ubuntu-22.04'] runs-on: ${{ matrix.os }} steps: From 896de03d87aa3e2d708ba110bf4f3f2560c21469 Mon Sep 17 00:00:00 2001 From: Mike Walters Date: Fri, 15 Aug 2025 07:35:30 +0100 Subject: [PATCH 03/14] Switch from QRegExp to QRegularExpression --- src/mainwindow.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index dc62d769..edebe382 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -72,12 +72,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()); From e7eb32689d7da80573bcd284c546013017951f03 Mon Sep 17 00:00:00 2001 From: Mike Walters Date: Fri, 15 Aug 2025 08:12:30 +0100 Subject: [PATCH 04/14] Define TileCacheKey earlier --- src/spectrogramplot.h | 44 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/spectrogramplot.h b/src/spectrogramplot.h index 2e52cb4b..58ae5a24 100644 --- a/src/spectrogramplot.h +++ b/src/spectrogramplot.h @@ -33,9 +33,30 @@ #include #include -class TileCacheKey; class AnnotationLocation; + +class TileCacheKey +{ + +public: + TileCacheKey(int fftSize, int zoomLevel, size_t sample) { + this->fftSize = fftSize; + this->zoomLevel = zoomLevel; + this->sample = sample; + } + + bool operator==(const TileCacheKey &k2) const { + return (this->fftSize == k2.fftSize) && + (this->zoomLevel == k2.zoomLevel) && + (this->sample == k2.sample); + } + + int fftSize; + int zoomLevel; + size_t sample; +}; + class SpectrogramPlot : public Plot { Q_OBJECT @@ -96,27 +117,6 @@ public slots: void paintAnnotations(QPainter &painter, QRect &rect, range_t sampleRange); }; -class TileCacheKey -{ - -public: - TileCacheKey(int fftSize, int zoomLevel, size_t sample) { - this->fftSize = fftSize; - this->zoomLevel = zoomLevel; - this->sample = sample; - } - - bool operator==(const TileCacheKey &k2) const { - return (this->fftSize == k2.fftSize) && - (this->zoomLevel == k2.zoomLevel) && - (this->sample == k2.sample); - } - - int fftSize; - int zoomLevel; - size_t sample; -}; - class AnnotationLocation { public: From 64360eabb4457cef30b92d09e396c0e1e833d4b3 Mon Sep 17 00:00:00 2001 From: Mike Walters Date: Sun, 30 Nov 2025 18:00:50 +0000 Subject: [PATCH 05/14] Pass QMouseEvents as pointers --- src/cursor.cpp | 12 ++++++------ src/cursor.h | 2 +- src/cursors.cpp | 18 +++++++++--------- src/cursors.h | 2 +- src/plot.cpp | 2 +- src/plot.h | 2 +- src/plotview.cpp | 17 +++++++++-------- src/spectrogramplot.cpp | 2 +- src/spectrogramplot.h | 2 +- src/tuner.cpp | 2 +- src/tuner.h | 2 +- 11 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/cursor.cpp b/src/cursor.cpp index b9ac71d0..e68845e4 100644 --- a/src/cursor.cpp +++ b/src/cursor.cpp @@ -37,10 +37,10 @@ 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 (pointOverCursor(event->pos()) && type != QEvent::Leave) { if (!cursorOverrided) { cursorOverrided = true; QApplication::setOverrideCursor(QCursor(cursorShape)); @@ -53,8 +53,8 @@ bool Cursor::mouseEvent(QEvent::Type type, QMouseEvent event) // 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; } diff --git a/src/cursor.h b/src/cursor.h index aceb6089..97f4844b 100644 --- a/src/cursor.h +++ b/src/cursor.h @@ -32,7 +32,7 @@ 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); signals: void posChanged(); diff --git a/src/cursors.cpp b/src/cursors.cpp index 8087f031..fbf157c1 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()) && type != QEvent::Leave) { 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; } diff --git a/src/cursors.h b/src/cursors.h index 9e7e5728..78192c81 100644 --- a/src/cursors.h +++ b/src/cursors.h @@ -33,7 +33,7 @@ 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 paintFront(QPainter &painter, QRect &rect, range_t sampleRange); range_t selection(); void setSegments(int segments); diff --git a/src/plot.cpp b/src/plot.cpp index f2e327d1..e3e7267c 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -34,7 +34,7 @@ void Plot::invalidateEvent() } -bool Plot::mouseEvent(QEvent::Type type, QMouseEvent event) +bool Plot::mouseEvent(QEvent::Type type, QMouseEvent *event) { return false; } diff --git a/src/plot.h b/src/plot.h index aac98417..028fbe2a 100644 --- a/src/plot.h +++ b/src/plot.h @@ -33,7 +33,7 @@ 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 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 839ecfff..df790179 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -260,15 +260,16 @@ bool PlotView::viewportEvent(QEvent *event) { int plotY = -verticalScrollBar()->value(); for (auto&& plot : plots) { + auto mouse_event = QMouseEvent( + event->type(), + QPoint(mouseEvent->position().x(), mouseEvent->position().y() - plotY), + 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; @@ -276,7 +277,7 @@ bool PlotView::viewportEvent(QEvent *event) { } if (cursorsEnabled) - if (cursors.mouseEvent(event->type(), *mouseEvent)) + if (cursors.mouseEvent(event->type(), mouseEvent)) return true; } diff --git a/src/spectrogramplot.cpp b/src/spectrogramplot.cpp index 3d42c0be..c20e2427 100644 --- a/src/spectrogramplot.cpp +++ b/src/spectrogramplot.cpp @@ -351,7 +351,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); diff --git a/src/spectrogramplot.h b/src/spectrogramplot.h index 58ae5a24..91fca7a7 100644 --- a/src/spectrogramplot.h +++ b/src/spectrogramplot.h @@ -67,7 +67,7 @@ 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; std::shared_ptr>> input() { return inputSource; }; void setSampleRate(double sampleRate); bool tunerEnabled(); diff --git a/src/tuner.cpp b/src/tuner.cpp index 6ccac61c..7d167d58 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; diff --git a/src/tuner.h b/src/tuner.h index 958d9a74..89a9f133 100644 --- a/src/tuner.h +++ b/src/tuner.h @@ -34,7 +34,7 @@ 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 paintFront(QPainter &painter, QRect &rect, range_t sampleRange); void setCentre(int centre); void setDeviation(int dev); From 3d136fa968c38e6c871f72ebe30fd6edbe5b1093 Mon Sep 17 00:00:00 2001 From: Mike Walters Date: Sun, 30 Nov 2025 23:16:10 +0000 Subject: [PATCH 06/14] spelling --- src/cursor.cpp | 8 ++++---- src/cursor.h | 2 +- src/traceplot.cpp | 4 ++++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/cursor.cpp b/src/cursor.cpp index e68845e4..0d5dd3ab 100644 --- a/src/cursor.cpp +++ b/src/cursor.cpp @@ -41,13 +41,13 @@ 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 (!cursorOverriden) { + cursorOverriden = true; QApplication::setOverrideCursor(QCursor(cursorShape)); } // Restore pointer if it moves off the cursor, or leaves the widget - } else if (cursorOverrided) { - cursorOverrided = false; + } else if (cursorOverriden) { + cursorOverriden = false; QApplication::restoreOverrideCursor(); } diff --git a/src/cursor.h b/src/cursor.h index 97f4844b..f25609d5 100644 --- a/src/cursor.h +++ b/src/cursor.h @@ -44,6 +44,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/traceplot.cpp b/src/traceplot.cpp index 95b63476..7c540f36 100644 --- a/src/traceplot.cpp +++ b/src/traceplot.cpp @@ -64,7 +64,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); From 531402f4e962892b6c1e87ecd72351cf923955c8 Mon Sep 17 00:00:00 2001 From: Mike Walters Date: Sun, 30 Nov 2025 23:26:25 +0000 Subject: [PATCH 07/14] Handle LeaveEvent separately as it shouldn't be cast to a MouseEvent --- src/cursor.cpp | 12 ++++++++++-- src/cursor.h | 1 + src/cursors.cpp | 13 ++++++++++++- src/cursors.h | 1 + src/plot.cpp | 5 +++++ src/plot.h | 1 + src/plotview.cpp | 12 ++++++++++-- src/spectrogramplot.cpp | 6 ++++++ src/spectrogramplot.h | 1 + src/tuner.cpp | 7 +++++++ src/tuner.h | 1 + 11 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/cursor.cpp b/src/cursor.cpp index 0d5dd3ab..6352a954 100644 --- a/src/cursor.cpp +++ b/src/cursor.cpp @@ -40,12 +40,12 @@ bool Cursor::pointOverCursor(QPoint point) 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 (pointOverCursor(event->pos())) { if (!cursorOverriden) { cursorOverriden = true; QApplication::setOverrideCursor(QCursor(cursorShape)); } - // Restore pointer if it moves off the cursor, or leaves the widget + // Restore pointer if it moves off the cursor } else if (cursorOverriden) { cursorOverriden = false; QApplication::restoreOverrideCursor(); @@ -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 f25609d5..76451ed6 100644 --- a/src/cursor.h +++ b/src/cursor.h @@ -33,6 +33,7 @@ class Cursor : public QObject int pos(); void setPos(int newPos); bool mouseEvent(QEvent::Type type, QMouseEvent *event); + void leaveEvent(); signals: void posChanged(); diff --git a/src/cursors.cpp b/src/cursors.cpp index fbf157c1..66eaadf8 100644 --- a/src/cursors.cpp +++ b/src/cursors.cpp @@ -52,7 +52,7 @@ bool Cursors::mouseEvent(QEvent::Type type, QMouseEvent *event) 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)); @@ -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 78192c81..84356d1d 100644 --- a/src/cursors.h +++ b/src/cursors.h @@ -34,6 +34,7 @@ class Cursors : public QObject Cursors(QObject * parent); int segments(); 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/plot.cpp b/src/plot.cpp index e3e7267c..af2fa031 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -39,6 +39,11 @@ 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 028fbe2a..94ccf607 100644 --- a/src/plot.h +++ b/src/plot.h @@ -34,6 +34,7 @@ class Plot : public QObject, public Subscriber ~Plot(); void invalidateEvent() override; 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 df790179..100055b4 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -253,8 +253,7 @@ 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); @@ -281,6 +280,15 @@ bool PlotView::viewportEvent(QEvent *event) { return true; } + if (event->type() == QEvent::Leave) { + for (auto&& plot : plots) { + plot->leaveEvent(); + } + + if (cursorsEnabled) + cursors.leaveEvent(); + } + // Handle parent eveents return QGraphicsView::viewportEvent(event); } diff --git a/src/spectrogramplot.cpp b/src/spectrogramplot.cpp index c20e2427..c71763e7 100644 --- a/src/spectrogramplot.cpp +++ b/src/spectrogramplot.cpp @@ -359,6 +359,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 91fca7a7..c75f193c 100644 --- a/src/spectrogramplot.h +++ b/src/spectrogramplot.h @@ -68,6 +68,7 @@ class SpectrogramPlot : public Plot 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; + void leaveEvent(); std::shared_ptr>> input() { return inputSource; }; void setSampleRate(double sampleRate); bool tunerEnabled(); diff --git a/src/tuner.cpp b/src/tuner.cpp index 7d167d58..2dbc144d 100644 --- a/src/tuner.cpp +++ b/src/tuner.cpp @@ -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 89a9f133..175dbba1 100644 --- a/src/tuner.h +++ b/src/tuner.h @@ -35,6 +35,7 @@ class Tuner : public QObject int centre(); int deviation(); 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); From 302981ebdc77a488e9d49c8887cd83ce133cd66f Mon Sep 17 00:00:00 2001 From: Mike Walters Date: Sun, 30 Nov 2025 23:41:36 +0000 Subject: [PATCH 08/14] Support Qt6 --- src/CMakeLists.txt | 24 +++++++++++++++++------- src/plotview.cpp | 8 ++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 05b20e76..8bc0bc23 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -52,8 +52,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) @@ -64,11 +66,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/plotview.cpp b/src/plotview.cpp index 100055b4..798d2ab5 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -91,7 +91,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(); } @@ -261,7 +265,11 @@ bool PlotView::viewportEvent(QEvent *event) { 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() From 1e782b7d199f895e769f1aaadbf3a9b394cc6bc9 Mon Sep 17 00:00:00 2001 From: Mike Walters Date: Sun, 30 Nov 2025 23:50:55 +0000 Subject: [PATCH 09/14] actions: add Qt versions to matrix --- .github/workflows/build.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 75a3a15d..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-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 From 033834688754846c6a1778319a2b6ad419e25905 Mon Sep 17 00:00:00 2001 From: Theleifless Date: Fri, 19 Jun 2026 21:49:22 -0400 Subject: [PATCH 10/14] TracePlot: share spectrogram's samples-per-column TracePlot::paintMid was computing its own samples-per-column locally from sampleRange.length() / rect.width(). When the view range gets clamped to a small file (smaller than the natural fftSize/zoomLevel span of the viewport), the spectrogram still uses fftSize/zoomLevel while TracePlot stretches its samples across the whole viewport -- so the two panels visibly disagree on the time axis. Push the view's samples-per-column into each TracePlot (via a new TracePlot::setSamplesPerColumn) when it is added and whenever setFFTAndZoom recomputes the time scale. Falls back to the legacy local computation when no value has been set so existing standalone users keep their previous behaviour. Reproduces with any short SigMF capture at non-default zoom: open the file, right-click -> Add derived plot -> Add amplitude, then zoom in. Before this change the trace fills the viewport while the spectrogram occupies a fraction of it; after, both panels stop at the same x-pixel. --- src/plotview.cpp | 11 +++++++++++ src/traceplot.cpp | 14 +++++++++++--- src/traceplot.h | 6 ++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/plotview.cpp b/src/plotview.cpp index b79fab97..2ad43d11 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() @@ -842,6 +846,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); diff --git a/src/traceplot.cpp b/src/traceplot.cpp index 95b63476..653c4edc 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( 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); From b3ab9289de6efc1ee723be5a9659a2cff32ab7ec Mon Sep 17 00:00:00 2001 From: Theleifless Date: Fri, 19 Jun 2026 21:51:10 -0400 Subject: [PATCH 11/14] TracePlot: clamp tile request to available samples at EOF InputSource::getSamples returns nullptr when start + length exceeds sampleCount. TracePlot::drawTile would then bail without emitting an image, so the tile remained fully transparent. Symptom: when zoomed in, samplesPerTile can be larger than the entire file, so even the first tile gets a null buffer and the trace plot vanishes entirely. The spectrogram handles this exact case by filling with -inf (renders black); TracePlot was the outlier. Clamp the request to what is actually available on the source, shrink the draw rect proportionally so the visible samples occupy their natural pixel range, and always emit the image so QPixmapCache caches it (no busy-loop retry). --- src/traceplot.cpp | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/traceplot.cpp b/src/traceplot.cpp index 653c4edc..55591780 100644 --- a/src/traceplot.cpp +++ b/src/traceplot.cpp @@ -90,25 +90,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"); } From 56a76385a3f3a9bbc6a3914b2cc20aba6e575be4 Mon Sep 17 00:00:00 2001 From: Theleifless Date: Fri, 19 Jun 2026 21:52:08 -0400 Subject: [PATCH 12/14] SpectrogramPlot: zero-pad the FFT at EOF instead of bailing to -inf getLine() centred the FFT on `sample` but bailed to a column of -inf whenever first_sample + fftSize exceeded the file size. That made the spectrogram visibly stop fftSize/2 samples short of the trace plots' right edge, since the last fully-centred FFT lives at sample EOF - fftSize/2. Get whatever samples are available, zero-pad the rest of the FFT buffer, then run the same windowing / FFT / dB pipeline. The last columns get slight spectral leakage from the padding boundary, but the spectrogram now ends at the same x-pixel as the trace plot and the user can actually see what's happening at the end of a short capture. The pre-existing head-of-file clamp (and its -inf bail-out when first_sample >= EOF) is preserved. --- src/spectrogramplot.cpp | 42 ++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/spectrogramplot.cpp b/src/spectrogramplot.cpp index b8469d1b..cd262d98 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]; } From c7b6617f2cf49a5b3e9880bea146c10b326a1469 Mon Sep 17 00:00:00 2001 From: Theleifless Date: Fri, 19 Jun 2026 21:53:20 -0400 Subject: [PATCH 13/14] PlotView: preserve cursor sample range across FFT/zoom changes The cursors store their position in viewport pixels. setFFTAndZoom changes samplesPerColumn but did not re-anchor the cursors, so the same pixel pair now spanned a different sample count. The "Symbol rate" / "Period" readouts in the Time selection panel would silently drift just because the user zoomed -- a sharp foot-gun when the readout is being used to measure something. Snapshot the cursor's absolute sample range at the top of setFFTAndZoom and re-place the cursors on the new pixel grid at the end, after updateView() has already settled the scrollbars. The final write also overrides any stale value left by the cursorsMoved signal cascade that fires when setSelection() temporarily reads back the rounded-to-integer-column pixel position. --- src/plotview.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/plotview.cpp b/src/plotview.cpp index 2ad43d11..87e49d83 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -833,6 +833,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) @@ -862,6 +870,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) From 4d752ccccced87dea873c350216aec909e303417 Mon Sep 17 00:00:00 2001 From: John Leifels Date: Sat, 20 Jun 2026 23:45:27 -0400 Subject: [PATCH 14/14] InputSource: guard SigMF color parse against empty string (Qt6 crash) Annotations without a presentation:color field yield an empty QString. The color-format check called sigmf_color.at(0) before testing length; under Qt6 an empty QString has a null data pointer, so at(0) dereferenced null and crashed when opening such a recording. Check length() == 9 first so the && short-circuits before at(0). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/inputsource.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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");