mirror of
https://github.com/Serial-Studio/Serial-Studio.git
synced 2025-01-15 05:22:53 +08:00
Begin working on plots
This commit is contained in:
parent
a2372303d0
commit
85da40e7b6
@ -44,11 +44,6 @@ MenuBar {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Set this component as the application's default menubar upon creation
|
||||
//
|
||||
Component.onCompleted: app.mainWindow.menuBar = this
|
||||
|
||||
//
|
||||
// File menu
|
||||
//
|
||||
|
@ -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
79
assets/themes/2_Yaru.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
@ -2,5 +2,6 @@
|
||||
<qresource prefix="/themes">
|
||||
<file>1_Light.json</file>
|
||||
<file>0_Dark.json</file>
|
||||
<file>2_Yaru.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
|
Loading…
x
Reference in New Issue
Block a user