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());