Begin working on plots

This commit is contained in:
Alex Spataru 2021-10-05 03:45:34 -05:00
parent a2372303d0
commit 85da40e7b6
7 changed files with 592 additions and 11 deletions

View File

@ -44,11 +44,6 @@ MenuBar {
}
}
//
// Set this component as the application's default menubar upon creation
//
Component.onCompleted: app.mainWindow.menuBar = this
//
// File menu
//

View File

@ -30,6 +30,7 @@ import "../Panes"
import "../Windows"
import "../Widgets"
import "../JsonEditor"
import "../PlatformDependent" as PlatformDependent
ApplicationWindow {
id: root
@ -275,12 +276,14 @@ ApplicationWindow {
//
Loader {
asynchronous: false
source: {
if (Qt.platform.os === "osx")
return "qrc:/qml/PlatformDependent/MenubarMacOS.qml"
return "qrc:/qml/PlatformDependent/Menubar.qml"
active: Qt.platform.os !== "osx"
sourceComponent: PlatformDependent.Menubar {
Component.onCompleted: root.menuBar = this
}
} Loader {
asynchronous: false
active: Qt.platform.os === "osx"
sourceComponent: PlatformDependent.MenubarMacOS {}
}
//

79
assets/themes/2_Yaru.json Normal file
View File

@ -0,0 +1,79 @@
{
"name":"Yaru",
"author":"Alex Spataru",
"colors":{
"base":"#323030",
"link":"#105087",
"button":"#373737",
"window":"#2c2c2c",
"text":"#ffffff",
"midlight":"#2c2c2c",
"highlight":"#df4a17",
"brightText":"#ffffff",
"buttonText":"#ffffff",
"windowText":"#ffffff",
"toolTipBase":"#feffc6",
"toolTipText":"#000000",
"highlightedText":"#ffffff",
"highlightedTextAlternative":"#bebebe",
"placeholderText":"#666666",
"toolbarGradient2":"#323030",
"toolbarGradient1":"#292929",
"menubarGradient1":"#323030",
"menubarGradient2":"#323030",
"menubarText":"#ffffff",
"dialogBackground":"#2c2c2c",
"consoleText":"#ffffff",
"consoleBase":"#2d0922",
"consoleButton":"#373737",
"consoleWindow":"#1c1c1c",
"consoleHighlight":"#df4a17",
"consoleHighlightedText":"#ffffff",
"consolePlaceholderText":"#bebebe",
"windowBackground":"#6f5c69",
"windowGradient1":"#323030",
"windowGradient2":"#323030",
"alternativeHighlight":"#8b2782",
"setupPanelBackground":"#272727",
"datasetValue":"#dd3224",
"graphDialBorder":"#222222",
"datasetTextPrimary":"#24476a",
"datasetTextSecondary":"#666666",
"datasetWindowBackground":"#272727",
"datasetWindowBorder":"#8b2782",
"embeddedWindowBackground":"#2c2c2c",
"ledEnabled":"#df4a17",
"ledDisabled":"#686868",
"csvHighlight":"#2e895c",
"widgetForegroundPrimary":"#f94144",
"widgetForegroundSecondary":"#666666",
"widgetIndicator1":"#444444",
"widgetIndicator2":"#f94144",
"widgetIndicator3":"#90be6d",
"widgetAlternativeBackground":"#fafafa",
"widgetControlBackground":"#666666",
"gyroSky":"#5c93c5",
"gyroText":"#ffffff",
"gyroGround":"#7d5233",
"mapDotBackground":"#ff0000",
"mapDotForeground":"#ffffff",
"mapBorder":"#646464",
"mapHorizon":"#dedede",
"mapSkyLowAltitude":"#6ba9d1",
"mapSkyHighAltitude":"#283e51",
"connectButtonChecked":"#fe696e",
"connectButtonUnchecked":"#26cd40",
"widgetColors":[
"#f94144",
"#f3722c",
"#f8961e",
"#f9844a",
"#f9c74f",
"#90be6d",
"#43aa8b",
"#4d908e",
"#577590",
"#277da1"
]
}
}

View File

@ -2,5 +2,6 @@
<qresource prefix="/themes">
<file>1_Light.json</file>
<file>0_Dark.json</file>
<file>2_Yaru.json</file>
</qresource>
</RCC>

View File

@ -19,3 +19,459 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "Plot.h"
#include <QwtPlotGrid>
#include <QwtPlotLayout>
#include <QwtPlotCanvas>
#include <QwtPlotMarker>
#include <QwtPlotCurve>
#include <QwtScaleDiv>
#include <QwtScaleMap>
#include <QwtPlotDirectPainter>
#include <QwtPainter>
#include <QEvent>
#include <QRect>
#include <QVector>
#include <QMutex>
#include <QReadWriteLock>
#include <UI/Dashboard.h>
using namespace Widgets;
class SignalData
{
public:
static SignalData &instance();
void append(const QPointF &pos);
void clearStaleValues(double min);
int size() const;
QPointF value(int index) const;
QRectF boundingRect() const;
void lock();
void unlock();
private:
SignalData();
~SignalData();
Q_DISABLE_COPY(SignalData)
class PrivateData;
PrivateData *m_data;
};
class Canvas : public QwtPlotCanvas
{
public:
Canvas(QwtPlot *plot = NULL)
: QwtPlotCanvas(plot)
{
/*
The backing store is important, when working with widget
overlays ( f.e rubberbands for zooming ).
Here we don't have them and the internal
backing store of QWidget is good enough.
*/
setPaintAttribute(QwtPlotCanvas::BackingStore, false);
setBorderRadius(10);
if (QwtPainter::isX11GraphicsSystem())
{
#if QT_VERSION < 0x050000
/*
Qt::WA_PaintOutsidePaintEvent works on X11 and has a
nice effect on the performance.
*/
setAttribute(Qt::WA_PaintOutsidePaintEvent, true);
#endif
/*
Disabling the backing store of Qt improves the performance
for the direct painter even more, but the canvas becomes
a native window of the window system, receiving paint events
for resize and expose operations. Those might be expensive
when there are many points and the backing store of
the canvas is disabled. So in this application
we better don't disable both backing stores.
*/
if (testPaintAttribute(QwtPlotCanvas::BackingStore))
{
setAttribute(Qt::WA_PaintOnScreen, true);
setAttribute(Qt::WA_NoSystemBackground, true);
}
}
setupPalette();
}
private:
void setupPalette()
{
QPalette pal = palette();
QLinearGradient gradient;
gradient.setCoordinateMode(QGradient::StretchToDeviceMode);
gradient.setColorAt(0.0, QColor(0, 49, 110));
gradient.setColorAt(1.0, QColor(0, 87, 174));
pal.setBrush(QPalette::Window, QBrush(gradient));
// QPalette::WindowText is used for the curve color
pal.setColor(QPalette::WindowText, Qt::green);
setPalette(pal);
}
};
class CurveData : public QwtSeriesData<QPointF>
{
public:
const SignalData &values() const { return SignalData::instance(); }
SignalData &values() { return SignalData::instance(); }
virtual QPointF sample(size_t index) const QWT_OVERRIDE
{
return SignalData::instance().value(index);
}
virtual size_t size() const QWT_OVERRIDE { return SignalData::instance().size(); }
virtual QRectF boundingRect() const QWT_OVERRIDE
{
return SignalData::instance().boundingRect();
}
};
class SignalData::PrivateData
{
public:
PrivateData()
: boundingRect(1.0, 1.0, -2.0, -2.0) // invalid
{
values.reserve(1000);
}
inline void append(const QPointF &sample)
{
values.append(sample);
// adjust the bounding rectangle
if (boundingRect.width() < 0 || boundingRect.height() < 0)
{
boundingRect.setRect(sample.x(), sample.y(), 0.0, 0.0);
}
else
{
boundingRect.setRight(sample.x());
if (sample.y() > boundingRect.bottom())
boundingRect.setBottom(sample.y());
if (sample.y() < boundingRect.top())
boundingRect.setTop(sample.y());
}
}
QReadWriteLock lock;
QVector<QPointF> values;
QRectF boundingRect;
QMutex mutex; // protecting pendingValues
QVector<QPointF> pendingValues;
};
SignalData::SignalData()
{
m_data = new PrivateData();
}
SignalData::~SignalData()
{
delete m_data;
}
int SignalData::size() const
{
return m_data->values.size();
}
QPointF SignalData::value(int index) const
{
return m_data->values[index];
}
QRectF SignalData::boundingRect() const
{
return m_data->boundingRect;
}
void SignalData::lock()
{
m_data->lock.lockForRead();
}
void SignalData::unlock()
{
m_data->lock.unlock();
}
void SignalData::append(const QPointF &sample)
{
m_data->mutex.lock();
m_data->pendingValues += sample;
const bool isLocked = m_data->lock.tryLockForWrite();
if (isLocked)
{
const int numValues = m_data->pendingValues.size();
const QPointF *pendingValues = m_data->pendingValues.data();
for (int i = 0; i < numValues; i++)
m_data->append(pendingValues[i]);
m_data->pendingValues.clear();
m_data->lock.unlock();
}
m_data->mutex.unlock();
}
void SignalData::clearStaleValues(double limit)
{
m_data->lock.lockForWrite();
m_data->boundingRect = QRectF(1.0, 1.0, -2.0, -2.0); // invalid
const QVector<QPointF> values = m_data->values;
m_data->values.clear();
m_data->values.reserve(values.size());
int index;
for (index = values.size() - 1; index >= 0; index--)
{
if (values[index].x() < limit)
break;
}
if (index > 0)
m_data->append(values[index++]);
while (index < values.size() - 1)
m_data->append(values[index++]);
m_data->lock.unlock();
}
SignalData &SignalData::instance()
{
static SignalData valueVector;
return valueVector;
}
Plot::Plot(const int index)
: QwtPlot(nullptr)
, m_index(index)
, m_paintedPoints(0)
, m_interval(0.0, 10.0)
, m_timerId(-1)
{
// Invalid index, abort initialization
auto dash = UI::Dashboard::getInstance();
if (m_index < 0 || m_index >= dash->plotCount())
return;
m_directPainter = new QwtPlotDirectPainter();
setAutoReplot(false);
setCanvas(new Canvas());
plotLayout()->setAlignCanvasToScales(true);
setAxisTitle(QwtAxis::XBottom, "Time [s]");
setAxisScale(QwtAxis::XBottom, m_interval.minValue(), m_interval.maxValue());
setAxisScale(QwtAxis::YLeft, -1.0, 1.0);
QwtPlotGrid *grid = new QwtPlotGrid();
grid->setPen(Qt::gray, 0.0, Qt::DotLine);
grid->enableX(true);
grid->enableXMin(true);
grid->enableY(true);
grid->enableYMin(false);
grid->attach(this);
m_origin = new QwtPlotMarker();
m_origin->setLineStyle(QwtPlotMarker::Cross);
m_origin->setValue(m_interval.minValue() + m_interval.width() / 2.0, 0.0);
m_origin->setLinePen(Qt::gray, 0.0, Qt::DashLine);
m_origin->attach(this);
m_curve = new QwtPlotCurve();
m_curve->setStyle(QwtPlotCurve::Lines);
m_curve->setPen(canvas()->palette().color(QPalette::WindowText));
m_curve->setRenderHint(QwtPlotItem::RenderAntialiased, true);
m_curve->setPaintAttribute(QwtPlotCurve::ClipPolygons, false);
m_curve->setData(new CurveData());
m_curve->attach(this);
// React to dashboard events
connect(dash, SIGNAL(updated()), this, SLOT(updateData()));
start();
setIntervalLength(0.05);
}
Plot::~Plot()
{
delete m_directPainter;
}
void Plot::start()
{
m_elapsedTimer.start();
m_timerId = startTimer(10);
}
void Plot::replot()
{
CurveData *curveData = static_cast<CurveData *>(m_curve->data());
curveData->values().lock();
QwtPlot::replot();
m_paintedPoints = curveData->size();
curveData->values().unlock();
}
void Plot::updateData() {
auto dataset = UI::Dashboard::getInstance()->getPlot(m_index);
const QPointF s(static_cast<qreal>(m_elapsedTimer.elapsed()), dataset->value().toDouble());
SignalData::instance().append(s);
updateCurve();
}
void Plot::setIntervalLength(double interval)
{
if (interval > 0.0 && interval != m_interval.width())
{
m_interval.setMaxValue(m_interval.minValue() + interval);
setAxisScale(QwtAxis::XBottom, m_interval.minValue(), m_interval.maxValue());
replot();
}
}
void Plot::updateCurve()
{
CurveData *curveData = static_cast<CurveData *>(m_curve->data());
curveData->values().lock();
const int numPoints = curveData->size();
if (numPoints > m_paintedPoints)
{
const bool doClip = !canvas()->testAttribute(Qt::WA_PaintOnScreen);
if (doClip)
{
/*
Depending on the platform setting a clip might be an important
performance issue. F.e. for Qt Embedded this reduces the
part of the backing store that has to be copied out - maybe
to an unaccelerated frame buffer device.
*/
const QwtScaleMap xMap = canvasMap(m_curve->xAxis());
const QwtScaleMap yMap = canvasMap(m_curve->yAxis());
QRectF br = qwtBoundingRect(*curveData, m_paintedPoints - 1, numPoints - 1);
const QRect clipRect = QwtScaleMap::transform(xMap, yMap, br).toRect();
m_directPainter->setClipRegion(clipRect);
}
m_directPainter->drawSeries(m_curve, m_paintedPoints - 1, numPoints - 1);
m_paintedPoints = numPoints;
}
curveData->values().unlock();
}
void Plot::incrementInterval()
{
m_interval
= QwtInterval(m_interval.maxValue(), m_interval.maxValue() + m_interval.width());
CurveData *curveData = static_cast<CurveData *>(m_curve->data());
curveData->values().clearStaleValues(m_interval.minValue());
// To avoid, that the grid is jumping, we disable
// the autocalculation of the ticks and shift them
// manually instead.
QwtScaleDiv scaleDiv = axisScaleDiv(QwtAxis::XBottom);
scaleDiv.setInterval(m_interval);
for (int i = 0; i < QwtScaleDiv::NTickTypes; i++)
{
QList<double> ticks = scaleDiv.ticks(i);
for (int j = 0; j < ticks.size(); j++)
ticks[j] += m_interval.width();
scaleDiv.setTicks(i, ticks);
}
setAxisScaleDiv(QwtAxis::XBottom, scaleDiv);
m_origin->setValue(m_interval.minValue() + m_interval.width() / 2.0, 0.0);
m_paintedPoints = 0;
replot();
}
void Plot::timerEvent(QTimerEvent *event)
{
if (event->timerId() == m_timerId)
{
updateCurve();
const double elapsed = m_elapsedTimer.elapsed() / 1e3;
if (elapsed > m_interval.maxValue())
incrementInterval();
return;
}
QwtPlot::timerEvent(event);
}
void Plot::resizeEvent(QResizeEvent *event)
{
m_directPainter->reset();
QwtPlot::resizeEvent(event);
}
void Plot::showEvent(QShowEvent *)
{
replot();
}
bool Plot::eventFilter(QObject *object, QEvent *event)
{
if (object == canvas() && event->type() == QEvent::PaletteChange)
{
m_curve->setPen(canvas()->palette().color(QPalette::WindowText));
}
return QwtPlot::eventFilter(object, event);
}

View File

@ -23,4 +23,51 @@
#ifndef WIDGETS_PLOT_H
#define WIDGETS_PLOT_H
#include <QwtPlot>
#include <QwtInterval>
#include <QElapsedTimer>
class QwtPlotCurve;
class QwtPlotMarker;
class QwtPlotDirectPainter;
namespace Widgets
{
class Plot : public QwtPlot
{
Q_OBJECT
public:
Plot(const int index = -1);
~Plot();
void start();
virtual void replot() QWT_OVERRIDE;
virtual bool eventFilter(QObject *, QEvent *) QWT_OVERRIDE;
public slots:
void updateData();
void setIntervalLength(const double interval);
private:
void updateCurve();
void incrementInterval();
protected:
virtual void showEvent(QShowEvent *event) QWT_OVERRIDE;
virtual void resizeEvent(QResizeEvent *event) QWT_OVERRIDE;
virtual void timerEvent(QTimerEvent *event) QWT_OVERRIDE;
private:
int m_index;
QwtPlotMarker *m_origin;
QwtPlotCurve *m_curve;
int m_paintedPoints;
QwtPlotDirectPainter *m_directPainter;
QwtInterval m_interval;
int m_timerId;
QElapsedTimer m_elapsedTimer;
};
}
#endif

View File

@ -241,7 +241,7 @@ void WidgetLoader::setWidgetIndex(const int index)
m_widget = new QPushButton("Multi-Plot");
break;
case UI::Dashboard::WidgetType::Plot:
m_widget = new QPushButton("Plot");
m_widget = new Plot(relativeIndex());
break;
case UI::Dashboard::WidgetType::Bar:
m_widget = new Bar(relativeIndex());