diff --git a/assets/qml/PlatformDependent/Menubar.qml b/assets/qml/PlatformDependent/Menubar.qml
index e3340ade..8b20660c 100644
--- a/assets/qml/PlatformDependent/Menubar.qml
+++ b/assets/qml/PlatformDependent/Menubar.qml
@@ -44,11 +44,6 @@ MenuBar {
}
}
- //
- // Set this component as the application's default menubar upon creation
- //
- Component.onCompleted: app.mainWindow.menuBar = this
-
//
// File menu
//
diff --git a/assets/qml/Windows/MainWindow.qml b/assets/qml/Windows/MainWindow.qml
index 270f17f2..22f38df3 100644
--- a/assets/qml/Windows/MainWindow.qml
+++ b/assets/qml/Windows/MainWindow.qml
@@ -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 {}
}
//
diff --git a/assets/themes/2_Yaru.json b/assets/themes/2_Yaru.json
new file mode 100644
index 00000000..e1fbb81f
--- /dev/null
+++ b/assets/themes/2_Yaru.json
@@ -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"
+ ]
+ }
+}
diff --git a/assets/themes/rcc_themes.qrc b/assets/themes/rcc_themes.qrc
index de722d86..44fb2f71 100644
--- a/assets/themes/rcc_themes.qrc
+++ b/assets/themes/rcc_themes.qrc
@@ -2,5 +2,6 @@
1_Light.json
0_Dark.json
+ 2_Yaru.json
diff --git a/src/Widgets/Plot.cpp b/src/Widgets/Plot.cpp
index ae6909da..12437dda 100644
--- a/src/Widgets/Plot.cpp
+++ b/src/Widgets/Plot.cpp
@@ -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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+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
+{
+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 values;
+ QRectF boundingRect;
+
+ QMutex mutex; // protecting pendingValues
+ QVector 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 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(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(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(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(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 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);
+}
diff --git a/src/Widgets/Plot.h b/src/Widgets/Plot.h
index f13c7c73..54cd34ab 100644
--- a/src/Widgets/Plot.h
+++ b/src/Widgets/Plot.h
@@ -23,4 +23,51 @@
#ifndef WIDGETS_PLOT_H
#define WIDGETS_PLOT_H
+#include
+#include
+#include
+
+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
diff --git a/src/Widgets/WidgetLoader.cpp b/src/Widgets/WidgetLoader.cpp
index d55a0f47..0abd69d1 100644
--- a/src/Widgets/WidgetLoader.cpp
+++ b/src/Widgets/WidgetLoader.cpp
@@ -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());