mirror of
https://github.com/miek/inspectrum.git
synced 2026-02-20 01:31:35 +01:00
Enables mouse tracking on PlotView to get mouse move events even when not dragging. Passes through Leave events to handle the case where the cursor is near the edge of the widget and the mouse leaves the widget without generating a mouse move event that isn't on the cursor. Passes in mouse cursor shape to Cursor to define whether it should be a horizontal resize, vertical resize or move (resize all). Qt handles the case where the cursor is dragged off screen and does not generate a Leave event while draggin (<3 Qt).
407 lines
12 KiB
C++
407 lines
12 KiB
C++
/*
|
|
* Copyright (C) 2015-2016, Mike Walters <mike@flomp.net>
|
|
*
|
|
* This file is part of inspectrum.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "plotview.h"
|
|
#include <iostream>
|
|
#include <QApplication>
|
|
#include <QDebug>
|
|
#include <QMenu>
|
|
#include <QPainter>
|
|
#include <QScrollBar>
|
|
#include "plots.h"
|
|
|
|
PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0})
|
|
{
|
|
mainSampleSource = input;
|
|
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
|
|
setMouseTracking(true);
|
|
enableCursors(false);
|
|
enableTimeScale(true);
|
|
connect(&cursors, SIGNAL(cursorsMoved()), this, SLOT(cursorsMoved()));
|
|
|
|
spectrogramPlot = new SpectrogramPlot(std::shared_ptr<SampleSource<std::complex<float>>>(mainSampleSource));
|
|
auto tunerOutput = std::dynamic_pointer_cast<SampleSource<std::complex<float>>>(spectrogramPlot->output());
|
|
|
|
addPlot(spectrogramPlot);
|
|
|
|
viewport()->installEventFilter(this);
|
|
mainSampleSource->subscribe(this);
|
|
}
|
|
|
|
void PlotView::addPlot(Plot *plot)
|
|
{
|
|
plots.emplace_back(plot);
|
|
connect(plot, &Plot::repaint, this, &PlotView::repaint);
|
|
}
|
|
|
|
void PlotView::contextMenuEvent(QContextMenuEvent * event)
|
|
{
|
|
QMenu menu;
|
|
|
|
// Get selected plot
|
|
Plot *selectedPlot = nullptr;
|
|
int y = -verticalScrollBar()->value();
|
|
for (auto&& plot : plots) {
|
|
if (range_t<int>{y, y + plot->height()}.contains(event->pos().y()))
|
|
selectedPlot = plot.get();
|
|
y += plot->height();
|
|
}
|
|
if (selectedPlot == nullptr)
|
|
return;
|
|
|
|
// Add actions to add derived plots
|
|
// that are compatible with selectedPlot's output
|
|
QMenu *plotsMenu = menu.addMenu("Add derived plot");
|
|
auto src = selectedPlot->output();
|
|
auto compatiblePlots = as_range(Plots::plots.equal_range(src->sampleType()));
|
|
for (auto p : compatiblePlots) {
|
|
auto plotInfo = p.second;
|
|
auto action = new QAction(QString("Add %1").arg(plotInfo.name), plotsMenu);
|
|
auto plotCreator = plotInfo.creator;
|
|
connect(
|
|
action, &QAction::triggered,
|
|
this, [=]() {
|
|
addPlot(plotCreator(src));
|
|
}
|
|
);
|
|
plotsMenu->addAction(action);
|
|
}
|
|
|
|
// Add action to extract symbols from selected plot
|
|
auto extract = new QAction("Extract symbols...", &menu);
|
|
connect(
|
|
extract, &QAction::triggered,
|
|
this, [=]() {
|
|
extractSymbols(src);
|
|
}
|
|
);
|
|
extract->setEnabled(cursorsEnabled && (src->sampleType() == typeid(float)));
|
|
menu.addAction(extract);
|
|
|
|
if (menu.exec(event->globalPos()))
|
|
updateView(false);
|
|
}
|
|
|
|
void PlotView::cursorsMoved()
|
|
{
|
|
selectedSamples = {
|
|
horizontalScrollBar()->value() + cursors.selection().minimum * samplesPerLine(),
|
|
horizontalScrollBar()->value() + cursors.selection().maximum * samplesPerLine()
|
|
};
|
|
|
|
emitTimeSelection();
|
|
viewport()->update();
|
|
}
|
|
|
|
void PlotView::emitTimeSelection()
|
|
{
|
|
off_t sampleCount = selectedSamples.length();
|
|
float selectionTime = sampleCount / (float)mainSampleSource->rate();
|
|
emit timeSelectionChanged(selectionTime);
|
|
}
|
|
|
|
void PlotView::enableCursors(bool enabled)
|
|
{
|
|
cursorsEnabled = enabled;
|
|
if (enabled) {
|
|
int margin = viewport()->rect().width() / 3;
|
|
cursors.setSelection({viewport()->rect().left() + margin, viewport()->rect().right() - margin});
|
|
cursorsMoved();
|
|
}
|
|
viewport()->update();
|
|
}
|
|
|
|
bool PlotView::eventFilter(QObject * obj, 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) {
|
|
|
|
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
|
|
|
if (cursorsEnabled)
|
|
if (cursors.mouseEvent(event->type(), *mouseEvent))
|
|
return true;
|
|
|
|
int plotY = -verticalScrollBar()->value();
|
|
for (auto&& plot : plots) {
|
|
bool result = plot->mouseEvent(
|
|
event->type(),
|
|
QMouseEvent(
|
|
event->type(),
|
|
QPoint(mouseEvent->pos().x(), mouseEvent->pos().y() - plotY),
|
|
mouseEvent->button(),
|
|
mouseEvent->buttons(),
|
|
QApplication::keyboardModifiers()
|
|
)
|
|
);
|
|
if (result)
|
|
return true;
|
|
plotY += plot->height();
|
|
}
|
|
}
|
|
|
|
// Handle wheel events for zooming
|
|
if (event->type() == QEvent::Wheel) {
|
|
QWheelEvent *wheelEvent = (QWheelEvent*)event;
|
|
if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
|
|
if (wheelEvent->angleDelta().y() > 0) {
|
|
emit zoomIn();
|
|
} else if (wheelEvent->angleDelta().y() < 0) {
|
|
emit zoomOut();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void PlotView::extractSymbols(std::shared_ptr<AbstractSampleSource> src)
|
|
{
|
|
if (!cursorsEnabled)
|
|
return;
|
|
auto floatSrc = std::dynamic_pointer_cast<SampleSource<float>>(src);
|
|
if (!floatSrc)
|
|
return;
|
|
auto samples = floatSrc->getSamples(selectedSamples.minimum, selectedSamples.length());
|
|
auto step = (float)selectedSamples.length() / cursors.segments();
|
|
auto symbols = std::vector<float>();
|
|
for (auto i = step / 2; i < selectedSamples.length(); i += step)
|
|
{
|
|
symbols.push_back(samples[i]);
|
|
}
|
|
for (auto f : symbols)
|
|
std::cout << f << ", ";
|
|
std::cout << std::endl << std::flush;
|
|
}
|
|
|
|
void PlotView::invalidateEvent()
|
|
{
|
|
horizontalScrollBar()->setMinimum(0);
|
|
horizontalScrollBar()->setMaximum(mainSampleSource->count());
|
|
}
|
|
|
|
void PlotView::repaint()
|
|
{
|
|
viewport()->update();
|
|
}
|
|
|
|
void PlotView::setCursorSegments(int segments)
|
|
{
|
|
// Calculate number of samples per segment
|
|
float sampPerSeg = (float)selectedSamples.length() / cursors.segments();
|
|
|
|
// Alter selection to keep samples per segment the same
|
|
selectedSamples.maximum = selectedSamples.minimum + (segments * sampPerSeg + 0.5f);
|
|
|
|
cursors.setSegments(segments);
|
|
updateView();
|
|
emitTimeSelection();
|
|
}
|
|
|
|
void PlotView::setFFTAndZoom(int size, int zoom)
|
|
{
|
|
// Set new FFT size
|
|
fftSize = size;
|
|
if (spectrogramPlot != nullptr)
|
|
spectrogramPlot->setFFTSize(size);
|
|
|
|
// Set new zoom level
|
|
zoomLevel = zoom;
|
|
if (spectrogramPlot != nullptr)
|
|
spectrogramPlot->setZoomLevel(zoom);
|
|
|
|
// Update horizontal (time) scrollbar
|
|
horizontalScrollBar()->setSingleStep(size * 10 / zoomLevel);
|
|
horizontalScrollBar()->setPageStep(size * 100 / zoomLevel);
|
|
|
|
updateView(true);
|
|
}
|
|
|
|
void PlotView::setPowerMin(int power)
|
|
{
|
|
powerMin = power;
|
|
if (spectrogramPlot != nullptr)
|
|
spectrogramPlot->setPowerMin(power);
|
|
updateView();
|
|
}
|
|
|
|
void PlotView::setPowerMax(int power)
|
|
{
|
|
powerMax = power;
|
|
if (spectrogramPlot != nullptr)
|
|
spectrogramPlot->setPowerMax(power);
|
|
updateView();
|
|
}
|
|
|
|
void PlotView::paintEvent(QPaintEvent *event)
|
|
{
|
|
if (mainSampleSource == nullptr) return;
|
|
|
|
QRect rect = QRect(0, 0, width(), height());
|
|
QPainter painter(viewport());
|
|
painter.fillRect(rect, Qt::black);
|
|
|
|
|
|
#define PLOT_LAYER(paintFunc) \
|
|
{ \
|
|
int y = -verticalScrollBar()->value(); \
|
|
for (auto&& plot : plots) { \
|
|
QRect rect = QRect(0, y, width(), plot->height()); \
|
|
plot->paintFunc(painter, rect, viewRange); \
|
|
y += plot->height(); \
|
|
} \
|
|
}
|
|
|
|
PLOT_LAYER(paintBack);
|
|
PLOT_LAYER(paintMid);
|
|
PLOT_LAYER(paintFront);
|
|
if (cursorsEnabled)
|
|
cursors.paintFront(painter, rect, viewRange);
|
|
|
|
if (timeScaleEnabled)
|
|
paintTimeScale(painter, rect, viewRange);
|
|
|
|
#undef PLOT_LAYER
|
|
}
|
|
|
|
void PlotView::paintTimeScale(QPainter &painter, QRect &rect, range_t<off_t> sampleRange)
|
|
{
|
|
float startTime = (float)sampleRange.minimum / sampleRate;
|
|
float stopTime = (float)sampleRange.maximum / sampleRate;
|
|
float duration = stopTime - startTime;
|
|
|
|
painter.save();
|
|
|
|
QPen pen(Qt::white, 1, Qt::SolidLine);
|
|
painter.setPen(pen);
|
|
QFontMetrics fm(painter.font());
|
|
|
|
int tickWidth = 80;
|
|
int maxTicks = rect.width() / tickWidth;
|
|
|
|
double durationPerTick = 10 * pow(10, floor(log(duration / maxTicks) / log(10)));
|
|
|
|
double firstTick = int(startTime / durationPerTick) * durationPerTick;
|
|
|
|
double tick = firstTick;
|
|
|
|
while (tick <= stopTime) {
|
|
|
|
off_t tickSample = tick * sampleRate;
|
|
int tickLine = (tickSample - sampleRange.minimum) / samplesPerLine();
|
|
|
|
char buf[128];
|
|
snprintf(buf, sizeof(buf), "%.06f", tick);
|
|
painter.drawLine(tickLine, 0, tickLine, 30);
|
|
painter.drawText(tickLine + 2, 25, buf);
|
|
|
|
tick += durationPerTick;
|
|
}
|
|
|
|
// Draw small ticks
|
|
durationPerTick /= 10;
|
|
firstTick = int(startTime / durationPerTick) * durationPerTick;
|
|
tick = firstTick;
|
|
while (tick <= stopTime) {
|
|
|
|
off_t tickSample = tick * sampleRate;
|
|
int tickLine = (tickSample - sampleRange.minimum) / samplesPerLine();
|
|
|
|
painter.drawLine(tickLine, 0, tickLine, 10);
|
|
tick += durationPerTick;
|
|
}
|
|
|
|
painter.restore();
|
|
}
|
|
|
|
|
|
int PlotView::plotsHeight()
|
|
{
|
|
int height = 0;
|
|
for (auto&& plot : plots) {
|
|
height += plot->height();
|
|
}
|
|
return height;
|
|
}
|
|
|
|
void PlotView::resizeEvent(QResizeEvent * event)
|
|
{
|
|
updateView();
|
|
}
|
|
|
|
off_t PlotView::samplesPerLine()
|
|
{
|
|
return fftSize / zoomLevel;
|
|
}
|
|
|
|
void PlotView::scrollContentsBy(int dx, int dy)
|
|
{
|
|
updateView();
|
|
}
|
|
|
|
void PlotView::updateView(bool reCenter)
|
|
{
|
|
// Store old view for recentering
|
|
auto oldViewRange = viewRange;
|
|
|
|
// Update current view
|
|
viewRange = {
|
|
horizontalScrollBar()->value(),
|
|
horizontalScrollBar()->value() + width() * samplesPerLine()
|
|
};
|
|
|
|
// Adjust time offset to zoom around central sample
|
|
if (reCenter) {
|
|
horizontalScrollBar()->setValue(
|
|
horizontalScrollBar()->value() + (oldViewRange.length() - viewRange.length()) / 2
|
|
);
|
|
}
|
|
|
|
horizontalScrollBar()->setMaximum(mainSampleSource->count() - ((width() - 1) * samplesPerLine()));
|
|
|
|
verticalScrollBar()->setMaximum(std::max(0, plotsHeight() - viewport()->height()));
|
|
|
|
// Update cursors
|
|
QRect rect = viewport()->rect();
|
|
range_t<int> newSelection = {
|
|
(int)((selectedSamples.minimum - horizontalScrollBar()->value()) / samplesPerLine()),
|
|
(int)((selectedSamples.maximum - horizontalScrollBar()->value()) / samplesPerLine())
|
|
};
|
|
cursors.setSelection(newSelection);
|
|
|
|
// Re-paint
|
|
viewport()->update();
|
|
}
|
|
|
|
void PlotView::setSampleRate(off_t rate)
|
|
{
|
|
sampleRate = rate;
|
|
emitTimeSelection();
|
|
}
|
|
|
|
void PlotView::enableTimeScale(bool enabled)
|
|
{
|
|
timeScaleEnabled = enabled;
|
|
viewport()->update();
|
|
}
|
|
|