From b323daca93a5b44781333914a0116382b5527b37 Mon Sep 17 00:00:00 2001 From: Alex Spataru Date: Thu, 21 Oct 2021 22:37:23 -0500 Subject: [PATCH] Implement LED panel #47 --- Serial-Studio.pro | 4 + assets/Resources.qrc | 1 + assets/icons/led.svg | 1 + assets/qml/Dashboard/ViewOptions.qml | 11 + assets/qml/JsonEditor/JsonDatasetDelegate.qml | 12 + assets/qml/JsonEditor/JsonGroupDelegate.qml | 2 +- src/JSON/Dataset.cpp | 13 +- src/JSON/Dataset.h | 4 +- src/JSON/Editor.cpp | 34 +++ src/JSON/Editor.h | 2 + src/JSON/Group.h | 6 + src/UI/Dashboard.cpp | 87 +++++- src/UI/Dashboard.h | 15 + src/UI/WidgetLoader.cpp | 4 + src/Widgets/Common/KLed.cpp | 274 ++++++++++++++++++ src/Widgets/Common/KLed.h | 258 +++++++++++++++++ src/Widgets/LEDPanel.cpp | 274 ++++++++++++++++++ src/Widgets/LEDPanel.h | 68 +++++ 18 files changed, 1054 insertions(+), 16 deletions(-) create mode 100644 assets/icons/led.svg create mode 100644 src/Widgets/Common/KLed.cpp create mode 100644 src/Widgets/Common/KLed.h create mode 100644 src/Widgets/LEDPanel.cpp create mode 100644 src/Widgets/LEDPanel.h diff --git a/Serial-Studio.pro b/Serial-Studio.pro index 98efbab3..cd1d2ab0 100644 --- a/Serial-Studio.pro +++ b/Serial-Studio.pro @@ -180,12 +180,14 @@ HEADERS += \ src/Widgets/Common/AnalogGauge.h \ src/Widgets/Common/AttitudeIndicator.h \ src/Widgets/Common/BaseWidget.h \ + src/Widgets/Common/KLed.h \ src/Widgets/Compass.h \ src/Widgets/DataGroup.h \ src/Widgets/FFTPlot.h \ src/Widgets/GPS.h \ src/Widgets/Gauge.h \ src/Widgets/Gyroscope.h \ + src/Widgets/LEDPanel.h \ src/Widgets/MultiPlot.h \ src/Widgets/Plot.h \ src/Widgets/Terminal.h @@ -219,12 +221,14 @@ SOURCES += \ src/Widgets/Common/AnalogGauge.cpp \ src/Widgets/Common/AttitudeIndicator.cpp \ src/Widgets/Common/BaseWidget.cpp \ + src/Widgets/Common/KLed.cpp \ src/Widgets/Compass.cpp \ src/Widgets/DataGroup.cpp \ src/Widgets/FFTPlot.cpp \ src/Widgets/GPS.cpp \ src/Widgets/Gauge.cpp \ src/Widgets/Gyroscope.cpp \ + src/Widgets/LEDPanel.cpp \ src/Widgets/MultiPlot.cpp \ src/Widgets/Plot.cpp \ src/Widgets/Terminal.cpp \ diff --git a/assets/Resources.qrc b/assets/Resources.qrc index 4484f2f6..8374259e 100644 --- a/assets/Resources.qrc +++ b/assets/Resources.qrc @@ -140,5 +140,6 @@ qml/JsonEditor/TreeView.qml icons/gps.svg qml/Widgets/Terminal.qml + icons/led.svg diff --git a/assets/icons/led.svg b/assets/icons/led.svg new file mode 100644 index 00000000..4c6fe453 --- /dev/null +++ b/assets/icons/led.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/qml/Dashboard/ViewOptions.qml b/assets/qml/Dashboard/ViewOptions.qml index 8e2a41ad..6db72c68 100644 --- a/assets/qml/Dashboard/ViewOptions.qml +++ b/assets/qml/Dashboard/ViewOptions.qml @@ -173,6 +173,17 @@ Widgets.Window { onCheckedChanged: (index, checked) => Cpp_UI_Dashboard.setMultiplotVisible(index, checked) } + // + // LEDs + // + ViewOptionsDelegate { + title: qsTr("LED Panels") + icon: "qrc:/icons/led.svg" + count: Cpp_UI_Dashboard.ledCount + titles: Cpp_UI_Dashboard.ledTitles + onCheckedChanged: (index, checked) => Cpp_UI_Dashboard.setLedVisible(index, checked) + } + // // FFT // diff --git a/assets/qml/JsonEditor/JsonDatasetDelegate.qml b/assets/qml/JsonEditor/JsonDatasetDelegate.qml index 1948cb15..9b09f0f3 100644 --- a/assets/qml/JsonEditor/JsonDatasetDelegate.qml +++ b/assets/qml/JsonEditor/JsonDatasetDelegate.qml @@ -112,6 +112,18 @@ Widgets.Window { } } + // + // Dataset LED + // + Label { + text: qsTr("Display LED:") + } Switch { + id: led + Layout.leftMargin: -app.spacing + checked: Cpp_JSON_Editor.datasetLED(group, dataset) + onCheckedChanged: Cpp_JSON_Editor.setDatasetLED(group, dataset, checked) + } + // // Dataset graph // diff --git a/assets/qml/JsonEditor/JsonGroupDelegate.qml b/assets/qml/JsonEditor/JsonGroupDelegate.qml index 45637345..c3714ec4 100644 --- a/assets/qml/JsonEditor/JsonGroupDelegate.qml +++ b/assets/qml/JsonEditor/JsonGroupDelegate.qml @@ -204,7 +204,7 @@ Widgets.Window { delegate: Item { Layout.fillWidth: true Layout.minimumWidth: 320 - Layout.minimumHeight: 400 + 2 * app.spacing + Layout.minimumHeight: 420 + 2 * app.spacing JsonDatasetDelegate { id: datasetDelegate diff --git a/src/JSON/Dataset.cpp b/src/JSON/Dataset.cpp index 57e3f0bc..1161f2b5 100644 --- a/src/JSON/Dataset.cpp +++ b/src/JSON/Dataset.cpp @@ -28,8 +28,9 @@ using namespace JSON; Dataset::Dataset(QObject *parent) : QObject(parent) , m_fft(false) - , m_graph(false) + , m_led(false) , m_log(false) + , m_graph(false) , m_title("") , m_value("") , m_units("") @@ -50,6 +51,14 @@ bool Dataset::fft() const return m_fft; } +/** + * @return @c true if the UI should generate a LED of this dataset + */ +bool Dataset::led() const +{ + return m_led; +} + /** * @return @c true if the UI should generate a logarithmic plot of this dataset */ @@ -151,6 +160,7 @@ bool Dataset::read(const QJsonObject &object) if (!object.isEmpty()) { auto fft = object.value("fft").toVariant().toBool(); + auto led = object.value("led").toVariant().toBool(); auto log = object.value("log").toVariant().toBool(); auto graph = object.value("g").toVariant().toBool(); auto title = object.value("t").toVariant().toString(); @@ -182,6 +192,7 @@ bool Dataset::read(const QJsonObject &object) m_min = min; m_max = max; m_fft = fft; + m_led = led; m_log = log; m_graph = graph; m_title = title; diff --git a/src/JSON/Dataset.h b/src/JSON/Dataset.h index 2e617e83..d7c1e7ea 100644 --- a/src/JSON/Dataset.h +++ b/src/JSON/Dataset.h @@ -91,6 +91,7 @@ public: Dataset(QObject *parent = nullptr); bool fft() const; + bool led() const; bool log() const; bool graph() const; double min() const; @@ -107,8 +108,9 @@ public: private: bool m_fft; - bool m_graph; + bool m_led; bool m_log; + bool m_graph; QString m_title; QString m_value; diff --git a/src/JSON/Editor.cpp b/src/JSON/Editor.cpp index ef8020d8..c07ab6fb 100644 --- a/src/JSON/Editor.cpp +++ b/src/JSON/Editor.cpp @@ -324,6 +324,7 @@ bool Editor::saveJsonFile() dataset.insert("t", datasetTitle(i, j)); dataset.insert("u", datasetUnits(i, j)); dataset.insert("g", datasetGraph(i, j)); + dataset.insert("led", datasetLED(i, j)); dataset.insert("w", datasetWidget(i, j)); dataset.insert("fft", datasetFftPlot(i, j)); dataset.insert("log", datasetLogPlot(i, j)); @@ -458,6 +459,22 @@ int Editor::datasetIndex(const int group, const int dataset) return 0; } +/** + * Returns @c true if Serial Studio should generate a LED with the given + * @a dataset (which is contained by the specified @a group). + * + * @param group index of the group in which the dataset belongs + * @param dataset index of the dataset + */ +bool Editor::datasetLED(const int group, const int dataset) +{ + auto set = getDataset(group, dataset); + if (set) + return set->led(); + + return false; +} + /** * Returns @c true if Serial Studio should graph the data of the given * @a dataset (which is contained by the specified @a group). @@ -758,6 +775,7 @@ void Editor::openJsonFile(const QString &path) // Register dataset with C++ model addDataset(group); setDatasetGraph(group, dataset, jsonDataset.value("g").toBool()); + setDatasetLED(group, dataset, jsonDataset.value("led").toBool()); setDatasetTitle(group, dataset, jsonDataset.value("t").toString()); setDatasetUnits(group, dataset, jsonDataset.value("u").toString()); setDatasetFftPlot(group, dataset, jsonDataset.value("fft").toBool()); @@ -1153,6 +1171,22 @@ void Editor::setDatasetIndex(const int group, const int dataset, const int frame } } +/** + * Updates the @a generateLED flag of the given @a dataset. + * + * @param group index of the group in which the dataset belongs + * @param dataset index of the dataset + */ +void Editor::setDatasetLED(const int group, const int dataset, const bool generateLED) +{ + auto set = getDataset(group, dataset); + if (set) + { + set->m_led = generateLED; + emit datasetChanged(group, dataset); + } +} + /** * Updates the @a generateGraph flag of the given @a dataset. * diff --git a/src/JSON/Editor.h b/src/JSON/Editor.h index c68e837b..8a92ec70 100644 --- a/src/JSON/Editor.h +++ b/src/JSON/Editor.h @@ -115,6 +115,7 @@ public: Q_INVOKABLE int groupWidgetIndex(const int group); Q_INVOKABLE int datasetIndex(const int group, const int dataset); + Q_INVOKABLE bool datasetLED(const int group, const int dataset); Q_INVOKABLE bool datasetGraph(const int group, const int dataset); Q_INVOKABLE bool datasetFftPlot(const int group, const int dataset); Q_INVOKABLE bool datasetLogPlot(const int group, const int dataset); @@ -151,6 +152,7 @@ public slots: void setDatasetTitle(const int group, const int dataset, const QString &title); void setDatasetUnits(const int group, const int dataset, const QString &units); void setDatasetIndex(const int group, const int dataset, const int frameIndex); + void setDatasetLED(const int group, const int dataset, const bool generateLED); void setDatasetGraph(const int group, const int dataset, const bool generateGraph); void setDatasetFftPlot(const int group, const int dataset, const bool generateFft); void setDatasetLogPlot(const int group, const int dataset, const bool generateLog); diff --git a/src/JSON/Group.h b/src/JSON/Group.h index 06e1c77b..6f759b2c 100644 --- a/src/JSON/Group.h +++ b/src/JSON/Group.h @@ -31,6 +31,11 @@ #include "Dataset.h" +namespace UI +{ +class Dashboard; +} + namespace JSON { /** @@ -89,6 +94,7 @@ private: QVector m_datasets; friend class Editor; + friend class UI::Dashboard; }; } diff --git a/src/UI/Dashboard.cpp b/src/UI/Dashboard.cpp index 099eac13..79f19d44 100644 --- a/src/UI/Dashboard.cpp +++ b/src/UI/Dashboard.cpp @@ -77,6 +77,7 @@ QFont Dashboard::monoFont() const } // clang-format off +JSON::Group *Dashboard::getLED(const int index) { return getGroupWidget(m_ledWidgets, index); } JSON::Group *Dashboard::getGPS(const int index) { return getGroupWidget(m_gpsWidgets, index); } JSON::Dataset *Dashboard::getBar(const int index) { return getDatasetWidget(m_barWidgets, index); } JSON::Dataset *Dashboard::getFFT(const int index) { return getDatasetWidget(m_fftWidgets, index); } @@ -143,7 +144,9 @@ bool Dashboard::frameValid() const int Dashboard::totalWidgetCount() const { // clang-format off - const int count = gpsCount() + + const int count = + gpsCount() + + ledCount() + barCount() + fftCount() + plotCount() + @@ -160,6 +163,7 @@ int Dashboard::totalWidgetCount() const // clang-format off int Dashboard::gpsCount() const { return m_gpsWidgets.count(); } +int Dashboard::ledCount() const { return m_ledWidgets.count(); } int Dashboard::barCount() const { return m_barWidgets.count(); } int Dashboard::fftCount() const { return m_fftWidgets.count(); } int Dashboard::plotCount() const { return m_plotWidgets.count(); } @@ -188,15 +192,16 @@ QVector Dashboard::widgetTitles() const // clang-format off return groupTitles() + - multiPlotTitles() + - fftTitles() + - plotTitles() + - barTitles() + - gaugeTitles() + - compassTitles() + - gyroscopeTitles() + - accelerometerTitles() + - gpsTitles(); + multiPlotTitles() + + ledTitles() + + fftTitles() + + plotTitles() + + barTitles() + + gaugeTitles() + + compassTitles() + + gyroscopeTitles() + + accelerometerTitles() + + gpsTitles(); // clang-format on } @@ -235,8 +240,13 @@ int Dashboard::relativeIndex(const int globalIndex) const if (index < multiPlotCount()) return index; - // Check if we should return FFT widget + // Check if we should return LED widget index -= multiPlotCount(); + if (index < ledCount()) + return index; + + // Check if we should return FFT widget + index -= ledCount(); if (index < fftCount()) return index; @@ -333,6 +343,9 @@ bool Dashboard::widgetVisible(const int globalIndex) const case WidgetType::GPS: visible = gpsVisible(index); break; + case WidgetType::LED: + visible = ledVisible(index); + break; default: visible = false; break; @@ -391,6 +404,9 @@ QString Dashboard::widgetIcon(const int globalIndex) const case WidgetType::GPS: return "qrc:/icons/gps.svg"; break; + case WidgetType::LED: + return "qrc:/icons/led.svg"; + break; default: return "qrc:/icons/close.svg"; break; @@ -411,7 +427,8 @@ QString Dashboard::widgetIcon(const int globalIndex) const * - @c WidgetType::Compass * - @c WidgetType::Gyroscope * - @c WidgetType::Accelerometer - * - @c WidgetType::Map + * - @c WidgetType::GPS + * - @c WidgetType::LED * * To simplify operations and user interface generation in QML, this class represents * the widgets in two manners: @@ -447,8 +464,13 @@ UI::Dashboard::WidgetType Dashboard::widgetType(const int globalIndex) const if (index < multiPlotCount()) return WidgetType::MultiPlot; - // Check if we should return FFT widget + // Check if we should return LED widget index -= multiPlotCount(); + if (index < ledCount()) + return WidgetType::LED; + + // Check if we should return FFT widget + index -= ledCount(); if (index < fftCount()) return WidgetType::FFT; @@ -499,6 +521,7 @@ UI::Dashboard::WidgetType Dashboard::widgetType(const int globalIndex) const bool Dashboard::barVisible(const int index) const { return getVisibility(m_barVisibility, index); } bool Dashboard::fftVisible(const int index) const { return getVisibility(m_fftVisibility, index); } bool Dashboard::gpsVisible(const int index) const { return getVisibility(m_gpsVisibility, index); } +bool Dashboard::ledVisible(const int index) const { return getVisibility(m_ledVisibility, index); } bool Dashboard::plotVisible(const int index) const { return getVisibility(m_plotVisibility, index); } bool Dashboard::groupVisible(const int index) const { return getVisibility(m_groupVisibility, index); } bool Dashboard::gaugeVisible(const int index) const { return getVisibility(m_gaugeVisibility, index); } @@ -514,6 +537,7 @@ bool Dashboard::accelerometerVisible(const int index) const { return getVisibili // clang-format off QVector Dashboard::gpsTitles() const { return groupTitles(m_gpsWidgets); } +QVector Dashboard::ledTitles() const { return groupTitles(m_ledWidgets); } QVector Dashboard::groupTitles() const { return groupTitles(m_groupWidgets); } QVector Dashboard::barTitles() const { return datasetTitles(m_barWidgets); } QVector Dashboard::fftTitles() const { return datasetTitles(m_fftWidgets); } @@ -559,6 +583,7 @@ void Dashboard::setPoints(const int points) void Dashboard::setBarVisible(const int i, const bool v) { setVisibility(m_barVisibility, i, v); } void Dashboard::setFFTVisible(const int i, const bool v) { setVisibility(m_fftVisibility, i, v); } void Dashboard::setGpsVisible(const int i, const bool v) { setVisibility(m_gpsVisibility, i, v); } +void Dashboard::setLedVisible(const int i, const bool v) { setVisibility(m_ledVisibility, i, v); } void Dashboard::setPlotVisible(const int i, const bool v) { setVisibility(m_plotVisibility, i, v); } void Dashboard::setGroupVisible(const int i, const bool v) { setVisibility(m_groupVisibility, i, v); } void Dashboard::setGaugeVisible(const int i, const bool v) { setVisibility(m_gaugeVisibility, i, v); } @@ -590,6 +615,7 @@ void Dashboard::resetData() m_barWidgets.clear(); m_fftWidgets.clear(); m_gpsWidgets.clear(); + m_ledWidgets.clear(); m_plotWidgets.clear(); m_gaugeWidgets.clear(); m_groupWidgets.clear(); @@ -602,6 +628,7 @@ void Dashboard::resetData() m_barVisibility.clear(); m_fftVisibility.clear(); m_gpsVisibility.clear(); + m_ledVisibility.clear(); m_plotVisibility.clear(); m_gaugeVisibility.clear(); m_groupVisibility.clear(); @@ -634,6 +661,7 @@ void Dashboard::updateData() int barC = barCount(); int fftC = fftCount(); int mapC = gpsCount(); + int ledC = ledCount(); int plotC = plotCount(); int groupC = groupCount(); int gaugeC = gaugeCount(); @@ -654,6 +682,7 @@ void Dashboard::updateData() m_barWidgets.clear(); m_fftWidgets.clear(); m_gpsWidgets.clear(); + m_ledWidgets.clear(); m_plotWidgets.clear(); m_gaugeWidgets.clear(); m_groupWidgets.clear(); @@ -670,6 +699,7 @@ void Dashboard::updateData() // Update widget vectors m_fftWidgets = getFFTWidgets(); + m_ledWidgets = getLEDWidgets(); m_plotWidgets = getPlotWidgets(); m_groupWidgets = getWidgetGroups(""); m_gpsWidgets = getWidgetGroups("map"); @@ -697,6 +727,7 @@ void Dashboard::updateData() regenerateWidgets |= (barC != barCount()); regenerateWidgets |= (fftC != fftCount()); regenerateWidgets |= (mapC != gpsCount()); + regenerateWidgets |= (ledC != ledCount()); regenerateWidgets |= (plotC != plotCount()); regenerateWidgets |= (gaugeC != gaugeCount()); regenerateWidgets |= (groupC != groupCount()); @@ -711,6 +742,7 @@ void Dashboard::updateData() m_barVisibility.clear(); m_fftVisibility.clear(); m_gpsVisibility.clear(); + m_ledVisibility.clear(); m_plotVisibility.clear(); m_gaugeVisibility.clear(); m_groupVisibility.clear(); @@ -726,6 +758,8 @@ void Dashboard::updateData() m_fftVisibility.append(true); for (i = 0; i < gpsCount(); ++i) m_gpsVisibility.append(true); + for (i = 0; i < ledCount(); ++i) + m_ledVisibility.append(true); for (i = 0; i < plotCount(); ++i) m_plotVisibility.append(true); for (i = 0; i < gaugeCount(); ++i) @@ -843,6 +877,33 @@ void Dashboard::processLatestJSON(const JFI_Object &frameInfo) // Widget utility functions //-------------------------------------------------------------------------------------------------- +/** + * Returns a group with all the datasets that need to be shown in the LED status panel. + * + * @note We return a vector with a single group item because we want to display a title on + * the window without breaking the current software architecture. + */ +QVector Dashboard::getLEDWidgets() const +{ + QVector widgets; + foreach (auto group, m_latestFrame.groups()) + { + foreach (auto dataset, group->datasets()) + { + if (dataset->led()) + widgets.append(dataset); + } + } + + JSON::Group *group = new JSON::Group(); + group->m_title = tr("Status Panel"); + group->m_datasets = widgets; + + QVector groups; + groups.append(group); + return groups; +} + /** * Returns a vector with all the datasets that need to be shown in the FFT widgets. */ diff --git a/src/UI/Dashboard.h b/src/UI/Dashboard.h index 81a7a5b5..b7db1561 100644 --- a/src/UI/Dashboard.h +++ b/src/UI/Dashboard.h @@ -78,6 +78,9 @@ class Dashboard : public QObject Q_PROPERTY(int gpsCount READ gpsCount NOTIFY widgetCountChanged) + Q_PROPERTY(int ledCount + READ ledCount + NOTIFY widgetCountChanged) Q_PROPERTY(int barCount READ barCount NOTIFY widgetCountChanged) @@ -108,6 +111,9 @@ class Dashboard : public QObject Q_PROPERTY(QVector gpsTitles READ gpsTitles NOTIFY widgetCountChanged) + Q_PROPERTY(QVector ledTitles + READ ledTitles + NOTIFY widgetCountChanged) Q_PROPERTY(QVector barTitles READ barTitles NOTIFY widgetCountChanged) @@ -158,6 +164,7 @@ public: Gyroscope, Accelerometer, GPS, + LED, Unknown }; Q_ENUM(WidgetType) @@ -165,6 +172,7 @@ public: static Dashboard *getInstance(); QFont monoFont() const; + JSON::Group *getLED(const int index); JSON::Group *getGPS(const int index); JSON::Dataset *getFFT(const int index); JSON::Dataset *getBar(const int index); @@ -182,6 +190,7 @@ public: int totalWidgetCount() const; int gpsCount() const; + int ledCount() const; int fftCount() const; int barCount() const; int plotCount() const; @@ -202,6 +211,7 @@ public: Q_INVOKABLE bool barVisible(const int index) const; Q_INVOKABLE bool fftVisible(const int index) const; Q_INVOKABLE bool gpsVisible(const int index) const; + Q_INVOKABLE bool ledVisible(const int index) const; Q_INVOKABLE bool plotVisible(const int index) const; Q_INVOKABLE bool groupVisible(const int index) const; Q_INVOKABLE bool gaugeVisible(const int index) const; @@ -213,6 +223,7 @@ public: QVector barTitles() const; QVector fftTitles() const; QVector gpsTitles() const; + QVector ledTitles() const; QVector plotTitles() const; QVector groupTitles() const; QVector gaugeTitles() const; @@ -230,6 +241,7 @@ public slots: void setBarVisible(const int index, const bool visible); void setFFTVisible(const int index, const bool visible); void setGpsVisible(const int index, const bool visible); + void setLedVisible(const int index, const bool visible); void setPlotVisible(const int index, const bool visible); void setGroupVisible(const int index, const bool visible); void setGaugeVisible(const int index, const bool visible); @@ -247,6 +259,7 @@ private slots: private: Dashboard(); + QVector getLEDWidgets() const; QVector getFFTWidgets() const; QVector getPlotWidgets() const; QVector getWidgetGroups(const QString &handle) const; @@ -272,6 +285,7 @@ private: QVector m_barVisibility; QVector m_fftVisibility; QVector m_gpsVisibility; + QVector m_ledVisibility; QVector m_plotVisibility; QVector m_groupVisibility; QVector m_gaugeVisibility; @@ -286,6 +300,7 @@ private: QVector m_gaugeWidgets; QVector m_compassWidgets; + QVector m_ledWidgets; QVector m_gpsWidgets; QVector m_groupWidgets; QVector m_multiPlotWidgets; diff --git a/src/UI/WidgetLoader.cpp b/src/UI/WidgetLoader.cpp index 03567589..3c02dafe 100644 --- a/src/UI/WidgetLoader.cpp +++ b/src/UI/WidgetLoader.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -276,6 +277,9 @@ void WidgetLoader::setWidgetIndex(const int index) case UI::Dashboard::WidgetType::GPS: m_widget = new GPS(relativeIndex()); break; + case UI::Dashboard::WidgetType::LED: + m_widget = new LEDPanel(relativeIndex()); + break; default: break; } diff --git a/src/Widgets/Common/KLed.cpp b/src/Widgets/Common/KLed.cpp new file mode 100644 index 00000000..70a407d0 --- /dev/null +++ b/src/Widgets/Common/KLed.cpp @@ -0,0 +1,274 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 1998 Jörg Habenicht + SPDX-FileCopyrightText: 2010 Christoph Feck + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kled.h" + +#include +#include +#include +#include + +class KLedPrivate +{ +public: + int darkFactor = 300; + QColor color; + KLed::State state = KLed::On; + KLed::Look look = KLed::Raised; + KLed::Shape shape = KLed::Circular; + + QPixmap cachedPixmap[2]; // for both states +}; + +KLed::KLed(QWidget *parent) + : QWidget(parent) + , d(new KLedPrivate) +{ + setColor(Qt::green); + updateAccessibleName(); +} + +KLed::KLed(const QColor &color, QWidget *parent) + : QWidget(parent) + , d(new KLedPrivate) +{ + setColor(color); + updateAccessibleName(); +} + +KLed::KLed(const QColor &color, State state, Look look, Shape shape, QWidget *parent) + : QWidget(parent) + , d(new KLedPrivate) +{ + d->state = (state == Off ? Off : On); + d->look = look; + d->shape = shape; + + setColor(color); + updateAccessibleName(); +} + +KLed::~KLed() = default; + +KLed::State KLed::state() const +{ + return d->state; +} + +KLed::Shape KLed::shape() const +{ + return d->shape; +} + +QColor KLed::color() const +{ + return d->color; +} + +KLed::Look KLed::look() const +{ + return d->look; +} + +void KLed::setState(State state) +{ + if (d->state == state) + { + return; + } + + d->state = (state == Off ? Off : On); + updateCachedPixmap(); + updateAccessibleName(); +} + +void KLed::setShape(Shape shape) +{ + if (d->shape == shape) + { + return; + } + + d->shape = shape; + updateCachedPixmap(); +} + +void KLed::setColor(const QColor &color) +{ + if (d->color == color) + { + return; + } + + d->color = color; + updateCachedPixmap(); +} + +void KLed::setDarkFactor(int darkFactor) +{ + if (d->darkFactor == darkFactor) + { + return; + } + + d->darkFactor = darkFactor; + updateCachedPixmap(); +} + +int KLed::darkFactor() const +{ + return d->darkFactor; +} + +void KLed::setLook(Look look) +{ + if (d->look == look) + { + return; + } + + d->look = look; + updateCachedPixmap(); +} + +void KLed::toggle() +{ + d->state = (d->state == On ? Off : On); + updateCachedPixmap(); + updateAccessibleName(); +} + +void KLed::on() +{ + setState(On); +} + +void KLed::off() +{ + setState(Off); +} + +void KLed::resizeEvent(QResizeEvent *) +{ + updateCachedPixmap(); +} + +QSize KLed::sizeHint() const +{ + QStyleOption option; + option.initFrom(this); + int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize, &option, this); + return QSize(iconSize, iconSize); +} + +QSize KLed::minimumSizeHint() const +{ + return QSize(32, 32); +} + +void KLed::updateAccessibleName() +{ +#ifndef QT_NO_ACCESSIBILITY + QString onName = tr("LED on", "Accessible name of a Led whose state is on"); + QString offName = tr("LED off", "Accessible name of a Led whose state is off"); + QString lastName = accessibleName(); + + if (lastName.isEmpty() || lastName == onName || lastName == offName) + { + // Accessible name has not been manually set. + + setAccessibleName(d->state == On ? onName : offName); + } +#endif +} + +void KLed::updateCachedPixmap() +{ + d->cachedPixmap[Off] = QPixmap(); + d->cachedPixmap[On] = QPixmap(); + update(); +} + +void KLed::paintEvent(QPaintEvent *) +{ + if (!d->cachedPixmap[d->state].isNull()) + { + QPainter painter(this); + painter.drawPixmap(1, 1, d->cachedPixmap[d->state]); + return; + } + + QSize size(width() - 2, height() - 2); + if (d->shape == Circular) + { + // Make sure the LED is round + const int dim = qMin(width(), height()) - 2; + size = QSize(dim, dim); + } + QPointF center(size.width() / 2.0, size.height() / 2.0); + const int smallestSize = qMin(size.width(), size.height()); + QPainter painter; + + QImage image(size, QImage::Format_ARGB32_Premultiplied); + image.fill(0); + + QRadialGradient fillGradient(center, smallestSize / 2.0, + QPointF(center.x(), size.height() / 3.0)); + const QColor fillColor = d->state != Off ? d->color : d->color.darker(d->darkFactor); + fillGradient.setColorAt(0.0, fillColor.lighter(250)); + fillGradient.setColorAt(0.5, fillColor.lighter(130)); + fillGradient.setColorAt(1.0, fillColor); + + QConicalGradient borderGradient(center, d->look == Sunken ? 90 : -90); + QColor borderColor = palette().color(QPalette::Dark); + if (d->state == On) + { + QColor glowOverlay = fillColor; + glowOverlay.setAlpha(80); + + // This isn't the fastest way, but should be "fast enough". + // It's also the only safe way to use QPainter::CompositionMode + QImage img(1, 1, QImage::Format_ARGB32_Premultiplied); + QPainter p(&img); + QColor start = borderColor; + start.setAlpha(255); // opaque + p.fillRect(0, 0, 1, 1, start); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + p.fillRect(0, 0, 1, 1, glowOverlay); + p.end(); + + borderColor = img.pixel(0, 0); + } + borderGradient.setColorAt(0.2, borderColor); + borderGradient.setColorAt(0.5, palette().color(QPalette::Light)); + borderGradient.setColorAt(0.8, borderColor); + + painter.begin(&image); + painter.setRenderHint(QPainter::Antialiasing); + painter.setBrush(d->look == Flat ? QBrush(fillColor) : QBrush(fillGradient)); + const QBrush penBrush + = (d->look == Flat) ? QBrush(borderColor) : QBrush(borderGradient); + const qreal penWidth = smallestSize / 8.0; + painter.setPen(QPen(penBrush, penWidth)); + QRectF r(penWidth / 2.0, penWidth / 2.0, size.width() - penWidth, + size.height() - penWidth); + if (d->shape == Rectangular) + { + painter.drawRect(r); + } + else + { + painter.drawEllipse(r); + } + painter.end(); + + d->cachedPixmap[d->state] = QPixmap::fromImage(image); + painter.begin(this); + painter.drawPixmap(1, 1, d->cachedPixmap[d->state]); + painter.end(); +} diff --git a/src/Widgets/Common/KLed.h b/src/Widgets/Common/KLed.h new file mode 100644 index 00000000..003ff5e5 --- /dev/null +++ b/src/Widgets/Common/KLed.h @@ -0,0 +1,258 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 1998 Jörg Habenicht + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KLED_H +#define KLED_H + +#include +#include + +class QColor; + +/** + * @class KLed kled.h KLed + * + * @short An LED widget. + * + * Displays a round or rectangular light emitting diode. + * + * It is configurable to arbitrary colors, the two on/off states and three + * styles (or "looks"); + * + * It may display itself in a performant flat view, a round view with + * light spot or a round view sunken in the screen. + * + * \image html kled.png "KLed Widget" + * + * @author Joerg Habenicht, Richard J. Moore (rich@kde.org) 1998, 1999 + */ +class KLed : public QWidget +{ + Q_OBJECT + Q_PROPERTY(State state READ state WRITE setState) + Q_PROPERTY(Shape shape READ shape WRITE setShape) + Q_PROPERTY(Look look READ look WRITE setLook) + Q_PROPERTY(QColor color READ color WRITE setColor) + Q_PROPERTY(int darkFactor READ darkFactor WRITE setDarkFactor) + +public: + /** + * Status of the light is on/off. + * @short LED on/off. + */ + enum State + { + Off, + On + }; + Q_ENUM(State) + + /** + * Shades of the lamp. + * @short LED shape + */ + enum Shape + { + Rectangular, + Circular + }; + Q_ENUM(Shape) + + /** + * Displays a flat, round or sunken LED. + * + * @short LED look. + */ + enum Look + { + Flat, + Raised, + Sunken, + }; + Q_ENUM(Look) + + /** + * Constructs a green, round LED widget which will initially + * be turned on. + * + * @param parent The parent widget. + */ + explicit KLed(QWidget *parent = nullptr); + + /** + * Constructs a round LED widget with the supplied color which will + * initially be turned on. + * + * @param color Initial color of the LED. + * @param parent The parent widget. + * @short Constructor + */ + explicit KLed(const QColor &color, QWidget *parent = nullptr); + + /** + * Constructor with the color, state and look. + * + * Differs from above only in the parameters, which configure all settings. + * + * @param color Initial color of the LED. + * @param state Sets the State. + * @param look Sets the Look. + * @param shape Sets the Shape (rectangular or circular). + * @param parent The parent widget. + * @short Constructor + */ + KLed(const QColor &color, KLed::State state, KLed::Look look, KLed::Shape shape, + QWidget *parent = nullptr); + + /** + * Destroys the LED widget. + * @short Destructor + */ + ~KLed() override; + + /** + * Returns the current color of the widget. + * + * @returns LED color + * @see setColor() + */ + QColor color() const; + + /** + * Returns the current state of the widget (on/off). + * @returns LED state + * + * @see State + */ + State state() const; + + /** + * Returns the current look of the widget. + * @returns LED look + * + * @see Look + */ + Look look() const; + + /** + * Returns the current shape of the widget. + * @returns LED shape + * + * @see Shape + */ + Shape shape() const; + + /** + * Returns the factor to darken the LED. + * @returns dark factor + * + * @see setDarkFactor() + */ + int darkFactor() const; + + /** + * Set the color of the widget. + * + * The LED is shown with @p color when in the KLed::On state + * or with the darken color in KLed::Off state. + * + * The widget calls the update() method, so it will + * be updated when entering the main event loop. + * + * @param color New color of the LED. + * + * @see color() darkFactor() + */ + void setColor(const QColor &color); + + /** + * Sets the state of the widget to On or Off. + * + * @param state The LED state: on or off. + * + * @see on() off() toggle() + */ + void setState(State state); + + /** + * Sets the look of the widget. + * + * The look may be Flat, Raised or Sunken. + * + * The widget calls the update() method, so it will + * be updated when entering the main event loop. + * + * @param look New look of the LED. + * + * @see Look + */ + void setLook(Look look); + + /** + * Set the shape of the LED. + * + * @param shape The LED shape. + * @short Set LED shape. + */ + void setShape(Shape shape); + + /** + * Sets the factor to darken the LED in KLed::Off state. + * + * The @p darkFactor should be greater than 100, otherwise the LED + * becomes lighter in KLed::Off state. + * + * Defaults to 300. + * + * @param darkFactor Sets the factor to darken the LED. + * + * @see setColor + */ + void setDarkFactor(int darkFactor); + + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + +public Q_SLOTS: + + /** + * Toggles the state of the led from Off to On or vice versa. + */ + void toggle(); + + /** + * Sets the state of the widget to On. + * + * @see off() toggle() setState() + */ + void on(); + + /** + * Sets the state of the widget to Off. + * + * @see on() toggle() setState() + */ + void off(); + +protected: + void paintEvent(QPaintEvent *) override; + void resizeEvent(QResizeEvent *) override; + + /** + * @internal + * invalidates caches after property changes and calls update() + */ + void updateCachedPixmap(); + +private: + void updateAccessibleName(); + +private: + std::unique_ptr const d; +}; + +#endif diff --git a/src/Widgets/LEDPanel.cpp b/src/Widgets/LEDPanel.cpp new file mode 100644 index 00000000..4e713113 --- /dev/null +++ b/src/Widgets/LEDPanel.cpp @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2020-2021 Alex Spataru + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "LEDPanel.h" +#include "UI/Dashboard.h" +#include "Misc/ThemeManager.h" + +#include +#include +#include + +using namespace Widgets; + +/** + * Generates the user interface elements & layout + */ +LEDPanel::LEDPanel(const int index) + : m_index(index) +{ + // Get pointers to serial studio modules + auto dash = UI::Dashboard::getInstance(); + auto theme = Misc::ThemeManager::getInstance(); + + // Invalid index, abort initialization + if (m_index < 0 || m_index >= dash->ledCount()) + return; + + // Get group pointer + auto group = dash->getLED(m_index); + if (!group) + return; + + // Generate widget stylesheets + auto titleQSS = QSS("color:%1", theme->widgetTextPrimary()); + + // Set window palette + QPalette windowPalette; + windowPalette.setColor(QPalette::Base, theme->widgetWindowBackground()); + windowPalette.setColor(QPalette::Window, theme->widgetWindowBackground()); + setPalette(windowPalette); + + // Configure scroll area container + m_dataContainer = new QWidget(this); + + // Make the value label larger + auto valueFont = dash->monoFont(); + valueFont.setPixelSize(dash->monoFont().pixelSize() * 1.3); + + // Get LED color list + QString color; + auto colors = QStringList { "#ff3333", "#ff9933", "#ffff33", "#99ff33", + "#33ff33", "#33ff99", "#33ffff", "#3399ff", + "#3333ff", "#9933ff", "#ff33ff", "#ff3399" }; + + // Configure grid layout + m_gridLayout = new QGridLayout(m_dataContainer); + m_gridLayout->setSpacing(8); + for (int dataset = 0; dataset < group->datasetCount(); ++dataset) + { + // Create labels + m_leds.append(new KLed(m_dataContainer)); + m_titles.append(new QLabel(m_dataContainer)); + + // Get pointers to labels + auto led = m_leds.last(); + auto title = m_titles.last(); + + // Set label styles & fonts + title->setStyleSheet(titleQSS); + title->setFont(dash->monoFont()); + title->setText(group->getDataset(dataset)->title()); + + // Get LED color + if (colors.count() > dataset) + color = colors.at(dataset); + else + color = colors.at(colors.count() % dataset); + + // Set LED color & style + led->setLook(KLed::Flat); + led->setShape(KLed::Rectangular); + led->setColor(QColor(color)); + + // Calculate column and row + int column = 0; + int row = dataset; + int count = dataset + 1; + while (count > 3) + { + count -= 3; + row -= 3; + column += 2; + } + + // Add label and LED to grid layout + m_gridLayout->addWidget(led, row, column); + m_gridLayout->addWidget(title, row, column + 1); + m_gridLayout->setAlignment(title, Qt::AlignLeft | Qt::AlignVCenter); + m_gridLayout->setAlignment(led, Qt::AlignHCenter | Qt::AlignVCenter); + } + + // Load layout into container widget + m_dataContainer->setLayout(m_gridLayout); + + // Configure scroll area + m_scrollArea = new QScrollArea(this); + m_scrollArea->setWidgetResizable(true); + m_scrollArea->setWidget(m_dataContainer); + m_scrollArea->setFrameShape(QFrame::NoFrame); + m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + // Configure main layout + m_mainLayout = new QVBoxLayout(this); + m_mainLayout->addWidget(m_scrollArea); + m_mainLayout->setContentsMargins(0, 0, 0, 0); + setLayout(m_mainLayout); + + // React to dashboard events + connect(dash, SIGNAL(updated()), this, SLOT(updateData())); +} + +/** + * Frees the memory allocated for each label and LED that represents a dataset + */ +LEDPanel::~LEDPanel() +{ + foreach (auto led, m_leds) + delete led; + + foreach (auto title, m_titles) + delete title; + + delete m_gridLayout; + delete m_scrollArea; + delete m_mainLayout; +} + +/** + * Checks if the widget is enabled, if so, the widget shall be updated + * to display the latest data frame. + * + * If the widget is disabled (e.g. the user hides it, or the external + * window is hidden), then the widget shall ignore the update request. + */ +void LEDPanel::updateData() +{ + // Widget not enabled, do nothing + if (!isEnabled()) + return; + + // Get group pointer + auto dash = UI::Dashboard::getInstance(); + auto group = dash->getLED(m_index); + if (!group) + return; + + // Update labels + JSON::Dataset *dataset; + for (int i = 0; i < group->datasetCount(); ++i) + { + dataset = group->getDataset(i); + if (dataset) + { +#ifndef LAZY_WIDGETS + m_titles.at(i)->setText(set->title()); +#endif + // Get dataset value (we compare with 0.1 for low voltages) + auto value = dataset->value().toDouble(); + if (qAbs(value) < 0.10) + m_leds.at(i)->off(); + else + m_leds.at(i)->on(); + } + } +} + +/** + * Changes the size of the labels when the widget is resized + */ +void LEDPanel::resizeEvent(QResizeEvent *event) +{ + auto width = event->size().width(); + QFont font = UI::Dashboard::getInstance()->monoFont(); + font.setPixelSize(qMax(8, width / 24)); + auto fHeight = QFontMetrics(font).height() * 1.5; + + for (int i = 0; i < m_titles.count(); ++i) + { + m_titles.at(i)->setFont(font); + m_leds.at(i)->setMinimumSize(fHeight, fHeight); + } + + event->accept(); +} + +void LEDPanel::wheelEvent(QWheelEvent *event) +{ + class Hack : public QScrollArea + { + public: + using QWidget::wheelEvent; + }; + + auto hack = static_cast(m_scrollArea); + hack->wheelEvent(event); +} + +void LEDPanel::mouseMoveEvent(QMouseEvent *event) +{ + class Hack : public QScrollArea + { + public: + using QWidget::mouseMoveEvent; + }; + + auto hack = static_cast(m_scrollArea); + hack->mouseMoveEvent(event); +} + +void LEDPanel::mousePressEvent(QMouseEvent *event) +{ + class Hack : public QScrollArea + { + public: + using QWidget::mousePressEvent; + }; + + auto hack = static_cast(m_scrollArea); + hack->mousePressEvent(event); +} + +void LEDPanel::mouseReleaseEvent(QMouseEvent *event) +{ + class Hack : public QScrollArea + { + public: + using QWidget::mouseReleaseEvent; + }; + + auto hack = static_cast(m_scrollArea); + hack->mouseReleaseEvent(event); +} + +void LEDPanel::mouseDoubleClickEvent(QMouseEvent *event) +{ + class Hack : public QScrollArea + { + public: + using QWidget::mouseDoubleClickEvent; + }; + + auto hack = static_cast(m_scrollArea); + hack->mouseDoubleClickEvent(event); +} diff --git a/src/Widgets/LEDPanel.h b/src/Widgets/LEDPanel.h new file mode 100644 index 00000000..0bd1cdb2 --- /dev/null +++ b/src/Widgets/LEDPanel.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020-2021 Alex Spataru + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef WIDGETS_LEDPANEL_H +#define WIDGETS_LEDPANEL_H + +#include +#include +#include +#include +#include + +#include "Common/KLed.h" + +namespace Widgets +{ +class LEDPanel : public QWidget +{ + Q_OBJECT + +public: + LEDPanel(const int index = -1); + ~LEDPanel(); + +private slots: + void updateData(); + +protected: + void resizeEvent(QResizeEvent *event); + void wheelEvent(QWheelEvent *event); + void mouseMoveEvent(QMouseEvent *event); + void mousePressEvent(QMouseEvent *event); + void mouseReleaseEvent(QMouseEvent *event); + void mouseDoubleClickEvent(QMouseEvent *event); + +private: + int m_index; + + QVector m_leds; + QVector m_titles; + + QWidget *m_dataContainer; + QVBoxLayout *m_mainLayout; + QGridLayout *m_gridLayout; + QScrollArea *m_scrollArea; +}; +} + +#endif