cae61ee4ca
Instead of using hardcore numbers, it would be great to use the enums provided by KIconLoader whenever we use sizes for icons. Made the change for the obvious ones replacing "16" by KIconLoader::SizeSmall, "22" by KIconLoader::SizeSmallMedium and "32" by KIconLoader::SizeMedium. svn path=/trunk/KDE/kdelibs/; revision=926544
1102 lines
34 KiB
C++
1102 lines
34 KiB
C++
/*
|
|
* KSysGuard, the KDE System Guard
|
|
*
|
|
* Copyright 1999 - 2002 Chris Schlaeger <cs@kde.org>
|
|
* Copyright 2006 John Tapsell <tapsell@kde.org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU Library General Public License as
|
|
* published by the Free Software Foundation; either version 2, or
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "signalplotter.h"
|
|
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
#include <QList>
|
|
#include <QPalette>
|
|
#include <QtGui/QPainter>
|
|
#include <QtGui/QPixmap>
|
|
#include <QtGui/QPainterPath>
|
|
#include <QtGui/QPolygon>
|
|
|
|
#include <kdebug.h>
|
|
#include <kglobal.h>
|
|
#include <klocale.h>
|
|
#include <kapplication.h>
|
|
#include <kstandarddirs.h>
|
|
#include <kiconloader.h>
|
|
|
|
#include <plasma/svg.h>
|
|
#include <plasma/theme.h>
|
|
|
|
namespace Plasma
|
|
{
|
|
|
|
class SignalPlotterPrivate
|
|
{
|
|
public:
|
|
SignalPlotterPrivate()
|
|
: svgBackground(0)
|
|
{ }
|
|
|
|
~SignalPlotterPrivate()
|
|
{
|
|
}
|
|
|
|
void themeChanged()
|
|
{
|
|
Plasma::Theme *theme = Plasma::Theme::defaultTheme();
|
|
backgroundColor = theme->color(Theme::BackgroundColor);
|
|
verticalLinesColor = theme->color(Theme::TextColor);
|
|
verticalLinesColor.setAlphaF(0.4);
|
|
horizontalLinesColor = theme->color(Theme::TextColor);
|
|
horizontalLinesColor.setAlphaF(0.4);
|
|
}
|
|
|
|
int precision;
|
|
uint samples;
|
|
uint bezierCurveOffset;
|
|
|
|
double scaledBy;
|
|
double verticalMin;
|
|
double verticalMax;
|
|
double niceVertMin;
|
|
double niceVertMax;
|
|
double niceVertRange;
|
|
|
|
bool fillPlots;
|
|
bool showLabels;
|
|
bool showTopBar;
|
|
bool stackPlots;
|
|
bool useAutoRange;
|
|
bool showThinFrame;
|
|
|
|
bool showVerticalLines;
|
|
bool verticalLinesScroll;
|
|
uint verticalLinesOffset;
|
|
uint verticalLinesDistance;
|
|
QColor verticalLinesColor;
|
|
|
|
bool showHorizontalLines;
|
|
uint horizontalScale;
|
|
uint horizontalLinesCount;
|
|
QColor horizontalLinesColor;
|
|
|
|
Svg *svgBackground;
|
|
QString svgFilename;
|
|
|
|
QColor fontColor;
|
|
QColor backgroundColor;
|
|
QPixmap backgroundPixmap;
|
|
|
|
QFont font;
|
|
QString title;
|
|
QString unit;
|
|
|
|
QList<PlotColor> plotColors;
|
|
QList<QList<double> > plotData;
|
|
};
|
|
|
|
SignalPlotter::SignalPlotter(QGraphicsItem *parent)
|
|
: QGraphicsWidget(parent),
|
|
d(new SignalPlotterPrivate)
|
|
{
|
|
d->precision = 0;
|
|
d->bezierCurveOffset = 0;
|
|
d->samples = 0;
|
|
d->verticalMin = d->verticalMax = 0.0;
|
|
d->niceVertMin = d->niceVertMax = 0.0;
|
|
d->niceVertRange = 0;
|
|
d->useAutoRange = true;
|
|
d->scaledBy = 1;
|
|
d->showThinFrame = true;
|
|
|
|
// Anything smaller than this does not make sense.
|
|
setMinimumSize(QSizeF(KIconLoader::SizeSmall, KIconLoader::SizeSmall));
|
|
|
|
d->showVerticalLines = true;
|
|
d->verticalLinesDistance = 30;
|
|
d->verticalLinesScroll = true;
|
|
d->verticalLinesOffset = 0;
|
|
d->horizontalScale = 1;
|
|
|
|
d->showHorizontalLines = true;
|
|
d->horizontalLinesCount = 5;
|
|
|
|
d->showLabels = true;
|
|
d->showTopBar = true;
|
|
d->stackPlots = true;
|
|
d->fillPlots = true;
|
|
|
|
setSvgBackground("widgets/plot-background");
|
|
|
|
connect(Plasma::Theme::defaultTheme(), SIGNAL(themeChanged()), SLOT(themeChanged()));
|
|
d->themeChanged();
|
|
|
|
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
}
|
|
|
|
SignalPlotter::~SignalPlotter()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
QString SignalPlotter::unit() const
|
|
{
|
|
return d->unit;
|
|
}
|
|
void SignalPlotter::setUnit(const QString &unit)
|
|
{
|
|
d->unit= unit;
|
|
}
|
|
|
|
void SignalPlotter::addPlot(const QColor &color)
|
|
{
|
|
// When we add a new plot, go back and set the data for this plot to 0 for
|
|
// all the other times. This is because it makes it easier for moveSensors.
|
|
foreach (QList<double> data, d->plotData) {
|
|
data.append(0);
|
|
}
|
|
PlotColor newColor;
|
|
newColor.color = color;
|
|
newColor.darkColor = color.dark(150);
|
|
d->plotColors.append(newColor);
|
|
}
|
|
|
|
void SignalPlotter::addSample(const QList<double>& sampleBuf)
|
|
{
|
|
if (d->samples < 4) {
|
|
// It might be possible, under some race conditions, for addSample
|
|
// to be called before d->samples is set. This is just to be safe.
|
|
kDebug() << "Error - d->samples is only " << d->samples;
|
|
updateDataBuffers();
|
|
kDebug() << "d->samples is now " << d->samples;
|
|
if (d->samples < 4) {
|
|
return;
|
|
}
|
|
}
|
|
d->plotData.prepend(sampleBuf);
|
|
Q_ASSERT(sampleBuf.count() == d->plotColors.count());
|
|
if ((uint)d->plotData.size() > d->samples) {
|
|
d->plotData.removeLast(); // we have too many. Remove the last item
|
|
if ((uint)d->plotData.size() > d->samples) {
|
|
// If we still have too many, then we have resized the widget.
|
|
// Remove one more. That way we will slowly resize to the new size
|
|
d->plotData.removeLast();
|
|
}
|
|
}
|
|
|
|
if (d->bezierCurveOffset >= 2) {
|
|
d->bezierCurveOffset = 0;
|
|
} else {
|
|
d->bezierCurveOffset++;
|
|
}
|
|
|
|
Q_ASSERT((uint)d->plotData.size() >= d->bezierCurveOffset);
|
|
|
|
// If the vertical lines are scrolling, increment the offset
|
|
// so they move with the data.
|
|
if (d->verticalLinesScroll) {
|
|
d->verticalLinesOffset =
|
|
(d->verticalLinesOffset + d->horizontalScale) % d->verticalLinesDistance;
|
|
}
|
|
update();
|
|
}
|
|
|
|
void SignalPlotter::reorderPlots(const QList<uint>& newOrder)
|
|
{
|
|
if (newOrder.count() != d->plotColors.count()) {
|
|
kDebug() << "neworder has " << newOrder.count()
|
|
<< " and plot colors is " << d->plotColors.count();
|
|
return;
|
|
}
|
|
foreach (QList<double> data, d->plotData) {
|
|
if (newOrder.count() != data.count()) {
|
|
kDebug() << "Serious problem in move sample. plotdata[i] has "
|
|
<< data.count() << " and neworder has " << newOrder.count();
|
|
} else {
|
|
QList<double> newPlot;
|
|
for (int i = 0; i < newOrder.count(); i++) {
|
|
int newIndex = newOrder[i];
|
|
newPlot.append(data.at(newIndex));
|
|
}
|
|
data = newPlot;
|
|
}
|
|
}
|
|
QList<PlotColor> newPlotColors;
|
|
for (int i = 0; i < newOrder.count(); i++) {
|
|
int newIndex = newOrder[i];
|
|
PlotColor newColor = d->plotColors.at(newIndex);
|
|
newPlotColors.append(newColor);
|
|
}
|
|
d->plotColors = newPlotColors;
|
|
}
|
|
|
|
void SignalPlotter::setVerticalRange(double min, double max)
|
|
{
|
|
d->verticalMin = min;
|
|
d->verticalMax = max;
|
|
calculateNiceRange();
|
|
}
|
|
|
|
QList<PlotColor> &SignalPlotter::plotColors()
|
|
{
|
|
return d->plotColors;
|
|
}
|
|
|
|
void SignalPlotter::removePlot(uint pos)
|
|
{
|
|
if (pos >= (uint)d->plotColors.size()) {
|
|
return;
|
|
}
|
|
d->plotColors.removeAt(pos);
|
|
|
|
foreach (QList<double> data, d->plotData) {
|
|
if ((uint)data.size() >= pos) {
|
|
data.removeAt(pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SignalPlotter::scale(qreal delta)
|
|
{
|
|
if (d->scaledBy == delta) {
|
|
return;
|
|
}
|
|
d->scaledBy = delta;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
calculateNiceRange();
|
|
}
|
|
|
|
qreal SignalPlotter::scaledBy() const
|
|
{
|
|
return d->scaledBy;
|
|
}
|
|
|
|
void SignalPlotter::setTitle(const QString &title)
|
|
{
|
|
if (d->title == title) {
|
|
return;
|
|
}
|
|
d->title = title;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
QString SignalPlotter::title() const
|
|
{
|
|
return d->title;
|
|
}
|
|
|
|
void SignalPlotter::setUseAutoRange(bool value)
|
|
{
|
|
d->useAutoRange = value;
|
|
calculateNiceRange();
|
|
// this change will be detected in paint and the image cache regenerated
|
|
}
|
|
|
|
bool SignalPlotter::useAutoRange() const
|
|
{
|
|
return d->useAutoRange;
|
|
}
|
|
|
|
double SignalPlotter::verticalMinValue() const
|
|
{
|
|
return d->verticalMin;
|
|
}
|
|
|
|
double SignalPlotter::verticalMaxValue() const
|
|
{
|
|
return d->verticalMax;
|
|
}
|
|
|
|
void SignalPlotter::setHorizontalScale(uint scale)
|
|
{
|
|
if (scale == d->horizontalScale) {
|
|
return;
|
|
}
|
|
|
|
d->horizontalScale = scale;
|
|
updateDataBuffers();
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
uint SignalPlotter::horizontalScale() const
|
|
{
|
|
return d->horizontalScale;
|
|
}
|
|
|
|
void SignalPlotter::setShowVerticalLines(bool value)
|
|
{
|
|
if (d->showVerticalLines == value) {
|
|
return;
|
|
}
|
|
d->showVerticalLines = value;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
bool SignalPlotter::showVerticalLines() const
|
|
{
|
|
return d->showVerticalLines;
|
|
}
|
|
|
|
void SignalPlotter::setVerticalLinesColor(const QColor &color)
|
|
{
|
|
if (d->verticalLinesColor == color) {
|
|
return;
|
|
}
|
|
d->verticalLinesColor = color;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
QColor SignalPlotter::verticalLinesColor() const
|
|
{
|
|
return d->verticalLinesColor;
|
|
}
|
|
|
|
void SignalPlotter::setVerticalLinesDistance(uint distance)
|
|
{
|
|
if (distance == d->verticalLinesDistance) {
|
|
return;
|
|
}
|
|
d->verticalLinesDistance = distance;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
uint SignalPlotter::verticalLinesDistance() const
|
|
{
|
|
return d->verticalLinesDistance;
|
|
}
|
|
|
|
void SignalPlotter::setVerticalLinesScroll(bool value)
|
|
{
|
|
if (value == d->verticalLinesScroll) {
|
|
return;
|
|
}
|
|
d->verticalLinesScroll = value;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
bool SignalPlotter::verticalLinesScroll() const
|
|
{
|
|
return d->verticalLinesScroll;
|
|
}
|
|
|
|
void SignalPlotter::setShowHorizontalLines(bool value)
|
|
{
|
|
if (value == d->showHorizontalLines) {
|
|
return;
|
|
}
|
|
d->showHorizontalLines = value;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
bool SignalPlotter::showHorizontalLines() const
|
|
{
|
|
return d->showHorizontalLines;
|
|
}
|
|
|
|
void SignalPlotter::setFontColor(const QColor &color)
|
|
{
|
|
d->fontColor = color;
|
|
}
|
|
|
|
QColor SignalPlotter::fontColor() const
|
|
{
|
|
return d->fontColor;
|
|
}
|
|
|
|
void SignalPlotter::setHorizontalLinesColor(const QColor &color)
|
|
{
|
|
if (color == d->horizontalLinesColor) {
|
|
return;
|
|
}
|
|
d->horizontalLinesColor = color;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
QColor SignalPlotter::horizontalLinesColor() const
|
|
{
|
|
return d->horizontalLinesColor;
|
|
}
|
|
|
|
void SignalPlotter::setHorizontalLinesCount(uint count)
|
|
{
|
|
if (count == d->horizontalLinesCount) {
|
|
return;
|
|
}
|
|
d->horizontalLinesCount = count;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
calculateNiceRange();
|
|
}
|
|
|
|
uint SignalPlotter::horizontalLinesCount() const
|
|
{
|
|
return d->horizontalLinesCount;
|
|
}
|
|
|
|
void SignalPlotter::setShowLabels(bool value)
|
|
{
|
|
if (value == d->showLabels) {
|
|
return;
|
|
}
|
|
d->showLabels = value;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
bool SignalPlotter::showLabels() const
|
|
{
|
|
return d->showLabels;
|
|
}
|
|
|
|
void SignalPlotter::setShowTopBar(bool value)
|
|
{
|
|
if (d->showTopBar == value) {
|
|
return;
|
|
}
|
|
d->showTopBar = value;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
bool SignalPlotter::showTopBar() const
|
|
{
|
|
return d->showTopBar;
|
|
}
|
|
|
|
void SignalPlotter::setFont(const QFont &font)
|
|
{
|
|
d->font = font;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
QFont SignalPlotter::font() const
|
|
{
|
|
return d->font;
|
|
}
|
|
|
|
QString SignalPlotter::svgBackground()
|
|
{
|
|
return d->svgFilename;
|
|
}
|
|
|
|
void SignalPlotter::setSvgBackground(const QString &filename)
|
|
{
|
|
if (d->svgFilename == filename) {
|
|
return;
|
|
}
|
|
|
|
if (!filename.isEmpty() && filename[0] == '/') {
|
|
KStandardDirs *kstd = KGlobal::dirs();
|
|
d->svgFilename = kstd->findResource("data", "ksysguard/" + filename);
|
|
} else {
|
|
d->svgFilename = filename;
|
|
}
|
|
|
|
if (!d->svgFilename.isEmpty()) {
|
|
if (d->svgBackground) {
|
|
delete d->svgBackground;
|
|
}
|
|
d->svgBackground = new Svg(this);
|
|
d->svgBackground->setImagePath(d->svgFilename);
|
|
}
|
|
|
|
}
|
|
|
|
void SignalPlotter::setBackgroundColor(const QColor &color)
|
|
{
|
|
if (color == d->backgroundColor) {
|
|
return;
|
|
}
|
|
d->backgroundColor = color;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
QColor SignalPlotter::backgroundColor() const
|
|
{
|
|
return d->backgroundColor;
|
|
}
|
|
|
|
void SignalPlotter::setThinFrame(bool set)
|
|
{
|
|
if (d->showThinFrame == set) {
|
|
return;
|
|
}
|
|
d->showThinFrame = set;
|
|
d->backgroundPixmap = QPixmap(); // we changed a paint setting, so reset the cache
|
|
}
|
|
|
|
void SignalPlotter::setStackPlots(bool stack)
|
|
{
|
|
d->stackPlots = stack;
|
|
d->fillPlots = stack;
|
|
}
|
|
|
|
bool SignalPlotter::stackPlots() const
|
|
{
|
|
return d->stackPlots;
|
|
}
|
|
|
|
void SignalPlotter::updateDataBuffers()
|
|
{
|
|
// This is called when the widget has resized
|
|
//
|
|
// Determine new number of samples first.
|
|
// +0.5 to ensure rounding up
|
|
// +4 for extra data points so there is
|
|
// 1) no wasted space and
|
|
// 2) no loss of precision when drawing the first data point.
|
|
d->samples = static_cast<uint>(((size().width() - 2) /
|
|
d->horizontalScale) + 4.5);
|
|
}
|
|
|
|
QPixmap SignalPlotter::getSnapshotImage(uint w, uint height)
|
|
{
|
|
uint horizontalStep = (uint)((1.0 * w / size().width()) + 0.5); // get the closest integer horizontal step
|
|
uint newWidth = (uint) (horizontalStep * size().width());
|
|
QPixmap image = QPixmap(newWidth, height);
|
|
QPainter p(&image);
|
|
drawWidget(&p, newWidth, height, newWidth);
|
|
p.end();
|
|
return image;
|
|
}
|
|
|
|
void SignalPlotter::setGeometry(const QRectF &geometry)
|
|
{
|
|
// First update our size, then update the data buffers accordingly.
|
|
QGraphicsWidget::setGeometry(geometry);
|
|
updateDataBuffers();
|
|
}
|
|
|
|
void SignalPlotter::paint(QPainter *painter,
|
|
const QStyleOptionGraphicsItem *option, QWidget *widget)
|
|
{
|
|
Q_UNUSED(option);
|
|
Q_UNUSED(widget);
|
|
|
|
uint w = (uint) size().width();
|
|
uint h = (uint) size().height();
|
|
|
|
// Do not do repaints when the widget is not yet setup properly.
|
|
if (w <= 2) {
|
|
return;
|
|
}
|
|
|
|
drawWidget(painter, w, h, d->horizontalScale);
|
|
}
|
|
|
|
void SignalPlotter::drawWidget(QPainter *p, uint w, uint height, int horizontalScale)
|
|
{
|
|
uint h = height; // h will become the height of just the bit we draw the plots in
|
|
p->setFont(d->font);
|
|
|
|
uint fontheight = p->fontMetrics().height();
|
|
if (d->verticalMin < d->niceVertMin ||
|
|
d->verticalMax > d->niceVertMax ||
|
|
d->verticalMax < (d->niceVertRange * 0.75 + d->niceVertMin) ||
|
|
d->niceVertRange == 0) {
|
|
calculateNiceRange();
|
|
}
|
|
QPen pen;
|
|
pen.setWidth(1);
|
|
pen.setCapStyle(Qt::RoundCap);
|
|
p->setPen(pen);
|
|
|
|
uint top = p->pen().width() / 2; // The y position of the top of the graph. Basically this is one more than the height of the top bar
|
|
h-= top;
|
|
|
|
// Check if there's enough room to actually show a top bar.
|
|
// Must be enough room for a bar at the top, plus horizontal
|
|
// lines each of a size with room for a scale.
|
|
bool showTopBar = d->showTopBar && h > (fontheight/*top bar size*/ +5/*smallest reasonable size for a graph*/);
|
|
if (showTopBar) {
|
|
top += fontheight; // The top bar has the same height as fontheight. Thus the top of the graph is at fontheight
|
|
h -= fontheight;
|
|
}
|
|
if (d->backgroundPixmap.isNull() ||
|
|
(uint)d->backgroundPixmap.size().height() != height ||
|
|
(uint)d->backgroundPixmap.size().width() != w) {
|
|
// recreate on resize etc
|
|
d->backgroundPixmap = QPixmap(w, height);
|
|
d->backgroundPixmap.fill(Qt::transparent);
|
|
QPainter pCache(&d->backgroundPixmap);
|
|
pCache.setRenderHint(QPainter::Antialiasing, false);
|
|
pCache.setFont(d->font);
|
|
|
|
drawBackground(&pCache, w, height);
|
|
|
|
if (d->showThinFrame) {
|
|
drawThinFrame(&pCache, w, height);
|
|
// We have a 'frame' in the bottom and right - so subtract them from the view
|
|
h--;
|
|
w--;
|
|
pCache.setClipRect(0, 0, w, height-1);
|
|
}
|
|
|
|
if (showTopBar) {
|
|
int separatorX = w / 2;
|
|
drawTopBarFrame(&pCache, separatorX, top);
|
|
}
|
|
|
|
// Draw scope-like grid vertical lines if it doesn't move.
|
|
// If it does move, draw it in the dynamic part of the code.
|
|
if (!d->verticalLinesScroll && d->showVerticalLines && w > 60) {
|
|
drawVerticalLines(&pCache, top, w, h);
|
|
}
|
|
|
|
if (d->showHorizontalLines) {
|
|
drawHorizontalLines(&pCache, top, w, h);
|
|
}
|
|
|
|
} else {
|
|
if (d->showThinFrame) {
|
|
// We have a 'frame' in the bottom and right - so subtract them from the view
|
|
h--;
|
|
w--;
|
|
}
|
|
}
|
|
p->drawPixmap(0, 0, d->backgroundPixmap);
|
|
p->setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
if (showTopBar) {
|
|
int separatorX = w / 2;
|
|
int topBarWidth = w - separatorX -2;
|
|
drawTopBarContents(p, separatorX, topBarWidth, top -1);
|
|
}
|
|
|
|
p->setClipRect(0, top, w, h);
|
|
// Draw scope-like grid vertical lines
|
|
if (d->verticalLinesScroll && d->showVerticalLines && w > 60) {
|
|
drawVerticalLines(p, top, w, h);
|
|
}
|
|
|
|
drawPlots(p, top, w, h, horizontalScale);
|
|
|
|
if (d->showLabels && w > 60 && h > (fontheight + 1)) {
|
|
// if there's room to draw the labels, then draw them!
|
|
drawAxisText(p, top, h);
|
|
}
|
|
}
|
|
|
|
void SignalPlotter::drawBackground(QPainter *p, int w, int h)
|
|
{
|
|
if (d->svgBackground) {
|
|
d->svgBackground->resize(w, h);
|
|
d->svgBackground->paint(p, 0, 0);
|
|
} else {
|
|
p->fillRect(0, 0, w, h, d->backgroundColor);
|
|
}
|
|
}
|
|
|
|
void SignalPlotter::drawThinFrame(QPainter *p, int w, int h)
|
|
{
|
|
// Draw white line along the bottom and the right side of the
|
|
// widget to create a 3D like look.
|
|
p->setPen(kapp->palette().color(QPalette::Light));
|
|
p->drawLine(0, h - 1, w - 1, h - 1);
|
|
p->drawLine(w - 1, 0, w - 1, h - 1);
|
|
}
|
|
|
|
void SignalPlotter::calculateNiceRange()
|
|
{
|
|
d->niceVertRange = d->verticalMax - d->verticalMin;
|
|
// If the range is too small we will force it to 1.0 since it
|
|
// looks a lot nicer.
|
|
if (d->niceVertRange < 0.000001) {
|
|
d->niceVertRange = 1.0;
|
|
}
|
|
|
|
d->niceVertMin = d->verticalMin;
|
|
if (d->verticalMin != 0.0) {
|
|
double dim = pow(10, floor(log10(fabs(d->verticalMin)))) / 2;
|
|
if (d->verticalMin < 0.0) {
|
|
d->niceVertMin = dim * floor(d->verticalMin / dim);
|
|
} else {
|
|
d->niceVertMin = dim * ceil(d->verticalMin / dim);
|
|
}
|
|
d->niceVertRange = d->verticalMax - d->niceVertMin;
|
|
if (d->niceVertRange < 0.000001) {
|
|
d->niceVertRange = 1.0;
|
|
}
|
|
}
|
|
// Massage the range so that the grid shows some nice values.
|
|
double step = d->niceVertRange / (d->scaledBy * (d->horizontalLinesCount + 1));
|
|
int logdim = (int)floor(log10(step));
|
|
double dim = pow((double)10.0, logdim) / 2;
|
|
int a = (int)ceil(step / dim);
|
|
if (logdim >= 0) {
|
|
d->precision = 0;
|
|
} else if (a % 2 == 0) {
|
|
d->precision = -logdim;
|
|
} else {
|
|
d->precision = 1 - logdim;
|
|
}
|
|
d->niceVertRange = d->scaledBy * dim * a * (d->horizontalLinesCount + 1);
|
|
d->niceVertMax = d->niceVertMin + d->niceVertRange;
|
|
}
|
|
|
|
void SignalPlotter::drawTopBarFrame(QPainter *p, int separatorX, int height)
|
|
{
|
|
// Draw horizontal bar with current sensor values at top of display.
|
|
// Remember that it has a height of 'height'. Thus the lowest pixel
|
|
// it can draw on is height-1 since we count from 0.
|
|
p->setPen(Qt::NoPen);
|
|
p->setPen(d->fontColor);
|
|
p->drawText(0, 1, separatorX, height, Qt::AlignCenter, d->title);
|
|
p->setPen(d->horizontalLinesColor);
|
|
p->drawLine(separatorX - 1, 1, separatorX - 1, height - 1);
|
|
}
|
|
|
|
void SignalPlotter::drawTopBarContents(QPainter *p, int x, int width, int height)
|
|
{
|
|
// The height is the height of the contents, so this will be
|
|
// one pixel less than the height of the topbar
|
|
double bias = -d->niceVertMin;
|
|
double scaleFac = width / d->niceVertRange;
|
|
// The top bar shows the current values of all the plot data.
|
|
// This iterates through each different plot and plots the newest data for each.
|
|
if (!d->plotData.isEmpty()) {
|
|
QList<double> newestData = d->plotData.first();
|
|
for (int i = newestData.count()-1; i >= 0; --i) {
|
|
double newest_datapoint = newestData.at(i);
|
|
int start = x + (int)(bias * scaleFac);
|
|
int end = x + (int)((bias += newest_datapoint) * scaleFac);
|
|
int start2 = qMin(start, end);
|
|
end = qMax(start, end);
|
|
start = start2;
|
|
|
|
// If the rect is wider than 2 pixels we draw only the last
|
|
// pixels with the bright color. The rest is painted with
|
|
// a 50% darker color.
|
|
|
|
p->setPen(Qt::NoPen);
|
|
QLinearGradient linearGrad(QPointF(start, 1), QPointF(end, 1));
|
|
linearGrad.setColorAt(0, d->plotColors[i].darkColor);
|
|
linearGrad.setColorAt(1, d->plotColors[i].color);
|
|
p->fillRect(start, 1, end - start, height-1, QBrush(linearGrad));
|
|
}
|
|
}
|
|
}
|
|
|
|
void SignalPlotter::drawVerticalLines(QPainter *p, int top, int w, int h)
|
|
{
|
|
p->setPen(d->verticalLinesColor);
|
|
for (int x = d->verticalLinesOffset; x < (w - 2); x += d->verticalLinesDistance) {
|
|
p->drawLine(w - x, top, w - x, h + top -1);
|
|
}
|
|
}
|
|
|
|
void SignalPlotter::drawPlots(QPainter *p, int top, int w, int h, int horizontalScale)
|
|
{
|
|
Q_ASSERT(d->niceVertRange != 0);
|
|
|
|
if (d->niceVertRange == 0) {
|
|
d->niceVertRange = 1;
|
|
}
|
|
double scaleFac = (h - 1) / d->niceVertRange;
|
|
|
|
int xPos = 0;
|
|
QList< QList<double> >::Iterator it = d->plotData.begin();
|
|
|
|
p->setPen(Qt::NoPen);
|
|
// In autoRange mode we determine the range and plot the values in
|
|
// one go. This is more efficiently than running through the
|
|
// buffers twice but we do react on recently discarded samples as
|
|
// well as new samples one plot too late. So the range is not
|
|
// correct if the recently discarded samples are larger or smaller
|
|
// than the current extreme values. But we can probably live with
|
|
// this.
|
|
|
|
// These values aren't used directly anywhere. Instead we call
|
|
// calculateNiceRange() which massages these values into a nicer
|
|
// values. Rounding etc. This means it's safe to change these values
|
|
// without affecting any other drawings.
|
|
if (d->useAutoRange) {
|
|
d->verticalMin = d->verticalMax = 0.0;
|
|
}
|
|
|
|
// d->bezierCurveOffset is how many points we have at the start.
|
|
// All the bezier curves are in groups of 3, with the first of the
|
|
// next group being the last point of the previous group
|
|
|
|
// Example, when d->bezierCurveOffset == 0, and we have data, then just
|
|
// plot a normal bezier curve. (we will have at least 3 points in this case)
|
|
// When d->bezierCurveOffset == 1, then we want a bezier curve that uses
|
|
// the first data point and the second data point. Then the next group
|
|
// starts from the second data point.
|
|
//
|
|
// When d->bezierCurveOffset == 2, then we want a bezier curve that
|
|
// uses the first, second and third data.
|
|
for (uint i = 0; it != d->plotData.end() && i < d->samples; ++i) {
|
|
QPen pen;
|
|
pen.setWidth(1);
|
|
pen.setCapStyle(Qt::FlatCap);
|
|
|
|
// We will plot 1 bezier curve for every 3 points, with the 4th point
|
|
// being the end of one bezier curve and the start of the second.
|
|
// This does means the bezier curves will not join nicely, but it
|
|
// should be better than nothing.
|
|
QList<double> datapoints = *it;
|
|
QList<double> prev_datapoints = datapoints;
|
|
QList<double> prev_prev_datapoints = datapoints;
|
|
QList<double> prev_prev_prev_datapoints = datapoints;
|
|
|
|
if (i == 0 && d->bezierCurveOffset > 0) {
|
|
// We are plotting an incomplete bezier curve - we don't have
|
|
// all the data we want. Try to cope.
|
|
xPos += horizontalScale * d->bezierCurveOffset;
|
|
if (d->bezierCurveOffset == 1) {
|
|
prev_datapoints = *it;
|
|
++it; // Now we are on the first element of the next group, if it exists
|
|
if (it != d->plotData.end()) {
|
|
prev_prev_prev_datapoints = prev_prev_datapoints = *it;
|
|
} else {
|
|
prev_prev_prev_datapoints = prev_prev_datapoints = prev_datapoints;
|
|
}
|
|
} else {
|
|
// d->bezierCurveOffset must be 2 now
|
|
prev_datapoints = *it;
|
|
Q_ASSERT(it != d->plotData.end());
|
|
++it;
|
|
prev_prev_datapoints = *it;
|
|
Q_ASSERT(it != d->plotData.end());
|
|
++it; // Now we are on the first element of the next group, if it exists
|
|
if (it != d->plotData.end()) {
|
|
prev_prev_prev_datapoints = *it;
|
|
} else {
|
|
prev_prev_prev_datapoints = prev_prev_datapoints;
|
|
}
|
|
}
|
|
} else {
|
|
// We have a group of 3 points at least. That's 1 start point and 2 control points.
|
|
xPos += horizontalScale * 3;
|
|
it++;
|
|
if (it != d->plotData.end()) {
|
|
prev_datapoints = *it;
|
|
it++;
|
|
if (it != d->plotData.end()) {
|
|
prev_prev_datapoints = *it;
|
|
it++; // We are now on the next set of data points
|
|
if (it != d->plotData.end()) {
|
|
// We have this datapoint, so use it for our finish point
|
|
prev_prev_prev_datapoints = *it;
|
|
} else {
|
|
// We don't have the next set, so use our last control
|
|
// point as our finish point
|
|
prev_prev_prev_datapoints = prev_prev_datapoints;
|
|
}
|
|
} else {
|
|
prev_prev_prev_datapoints = prev_prev_datapoints = prev_datapoints;
|
|
}
|
|
} else {
|
|
prev_prev_prev_datapoints = prev_prev_datapoints = prev_datapoints = datapoints;
|
|
}
|
|
}
|
|
|
|
float x0 = w - xPos + 3.0 * horizontalScale;
|
|
float x1 = w - xPos + 2.0 * horizontalScale;
|
|
float x2 = w - xPos + 1.0 * horizontalScale;
|
|
float x3 = w - xPos;
|
|
float y0 = h - 1 + top;
|
|
float y1 = y0;
|
|
float y2 = y0;
|
|
float y3 = y0;
|
|
|
|
int offset = 0; // Our line is 2 pixels thick. This means that when we draw the area, we need to offset
|
|
double max_y = 0;
|
|
double min_y = 0;
|
|
for (int j = qMin(datapoints.size(), d->plotColors.size()) - 1; j >=0; --j) {
|
|
if (d->useAutoRange) {
|
|
// If we use autorange, then we need to prepare the min and max values for _next_ time we paint.
|
|
// If we are stacking the plots, then we need to add the maximums together.
|
|
double current_maxvalue =
|
|
qMax(datapoints[j],
|
|
qMax(prev_datapoints[j],
|
|
qMax(prev_prev_datapoints[j],
|
|
prev_prev_prev_datapoints[j])));
|
|
double current_minvalue =
|
|
qMin(datapoints[j],
|
|
qMin(prev_datapoints[j],
|
|
qMin(prev_prev_datapoints[j],
|
|
prev_prev_prev_datapoints[j])));
|
|
d->verticalMax = qMax(d->verticalMax, current_maxvalue);
|
|
d->verticalMin = qMin(d->verticalMin, current_maxvalue);
|
|
if (d->stackPlots) {
|
|
max_y += current_maxvalue;
|
|
min_y += current_minvalue;
|
|
}
|
|
}
|
|
|
|
// Draw polygon only if enough data points are available.
|
|
if (j < prev_prev_prev_datapoints.count() &&
|
|
j < prev_prev_datapoints.count() &&
|
|
j < prev_datapoints.count()) {
|
|
|
|
// The height of the whole widget is h+top-> The height of
|
|
// the area we are plotting in is just h.
|
|
// The y coordinate system starts from the top, so at the
|
|
// bottom the y coordinate is h+top.
|
|
// So to draw a point at value y', we need to put this at h+top-y'
|
|
float delta_y0;
|
|
delta_y0 = (datapoints[j] - d->niceVertMin) * scaleFac;
|
|
|
|
float delta_y1;
|
|
delta_y1 = (prev_datapoints[j] - d->niceVertMin) * scaleFac;
|
|
|
|
float delta_y2;
|
|
delta_y2 = (prev_prev_datapoints[j] - d->niceVertMin) * scaleFac;
|
|
|
|
float delta_y3;
|
|
delta_y3 = (prev_prev_prev_datapoints[j] - d->niceVertMin) * scaleFac;
|
|
|
|
QPainterPath path;
|
|
if (d->stackPlots && offset) {
|
|
// we don't want the lines to overdraw each other.
|
|
// This isn't a great solution though :(
|
|
if (delta_y0 < 3) {
|
|
delta_y0=3;
|
|
}
|
|
if (delta_y1 < 3) {
|
|
delta_y1=3;
|
|
}
|
|
if (delta_y2 < 3) {
|
|
delta_y2=3;
|
|
}
|
|
if (delta_y3 < 3) {
|
|
delta_y3=3;
|
|
}
|
|
}
|
|
path.moveTo(x0, y0 - delta_y0);
|
|
path.cubicTo(x1, y1 - delta_y1, x2, y2 - delta_y2, x3, y3 - delta_y3);
|
|
|
|
if (d->fillPlots) {
|
|
QPainterPath path2(path);
|
|
QLinearGradient myGradient(0,(h - 1 + top), 0, (h - 1 + top) / 5);
|
|
Q_ASSERT(d->plotColors.size() >= j);
|
|
QColor c0(d->plotColors[j].darkColor);
|
|
QColor c1(d->plotColors[j].color);
|
|
c0.setAlpha(150);
|
|
c1.setAlpha(150);
|
|
myGradient.setColorAt(0, c0);
|
|
myGradient.setColorAt(1, c1);
|
|
|
|
path2.lineTo(x3, y3 - offset);
|
|
if (d->stackPlots) {
|
|
// offset is set to 1 after the first plot is drawn,
|
|
// so we don't trample on top of the 2pt thick line
|
|
path2.cubicTo(x2, y2 - offset, x1, y1 - offset, x0, y0 - offset);
|
|
} else {
|
|
path2.lineTo(x0, y0 - 1);
|
|
}
|
|
p->setBrush(myGradient);
|
|
p->setPen(Qt::NoPen);
|
|
p->drawPath(path2);
|
|
}
|
|
p->setBrush(Qt::NoBrush);
|
|
Q_ASSERT(d->plotColors.size() >= j);
|
|
pen.setColor(d->plotColors[j].color);
|
|
p->setPen(pen);
|
|
p->drawPath(path);
|
|
|
|
if (d->stackPlots) {
|
|
// We can draw the plots stacked on top of each other.
|
|
// This means that say plot 0 has the value 2 and plot
|
|
// 1 has the value 3, then we plot plot 0 at 2 and plot 1 at 2+3 = 5.
|
|
y0 -= delta_y0;
|
|
y1 -= delta_y1;
|
|
y2 -= delta_y2;
|
|
y3 -= delta_y3;
|
|
offset = 1; // see the comment further up for int offset;
|
|
}
|
|
}
|
|
if (d->useAutoRange && d->stackPlots) {
|
|
d->verticalMax = qMax(max_y, d->verticalMax);
|
|
d->verticalMin = qMin(min_y, d->verticalMin);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SignalPlotter::drawAxisText(QPainter *p, int top, int h)
|
|
{
|
|
// Draw horizontal lines and values. Lines are always drawn.
|
|
// Values are only draw when width is greater than 60.
|
|
QString val;
|
|
|
|
// top = 0 or font.height depending on whether there's a topbar or not
|
|
// h = graphing area.height - i.e. the actual space we have to draw inside
|
|
// Note we are drawing from 0,0 as the top left corner. So we have to add on top
|
|
// to get to the top of where we are drawing so top+h is the height of the widget.
|
|
p->setPen(d->fontColor);
|
|
double stepsize = d->niceVertRange / (d->scaledBy * (d->horizontalLinesCount + 1));
|
|
int step =
|
|
(int)ceil((d->horizontalLinesCount+1) *
|
|
(p->fontMetrics().height() + p->fontMetrics().leading() / 2.0) / h);
|
|
if (step == 0) {
|
|
step = 1;
|
|
}
|
|
for (int y = d->horizontalLinesCount + 1; y >= 1; y-= step) {
|
|
int y_coord =
|
|
top + (y * (h - 1)) / (d->horizontalLinesCount + 1); // Make sure it's y*h first to avoid rounding bugs
|
|
if (y_coord - p->fontMetrics().ascent() < top) {
|
|
// at most, only allow 4 pixels of the text to be covered up
|
|
// by the top bar. Otherwise just don't bother to draw it
|
|
continue;
|
|
}
|
|
double value;
|
|
if ((uint)y == d->horizontalLinesCount + 1) {
|
|
value = d->niceVertMin; // sometimes using the formulas gives us a value very slightly off
|
|
} else {
|
|
value = d->niceVertMax / d->scaledBy - y * stepsize;
|
|
}
|
|
|
|
QString number = KGlobal::locale()->formatNumber(value, d->precision);
|
|
val = QString("%1 %2").arg(number, d->unit);
|
|
p->drawText(6, y_coord - 3, val);
|
|
}
|
|
}
|
|
|
|
void SignalPlotter::drawHorizontalLines(QPainter *p, int top, int w, int h)
|
|
{
|
|
p->setPen(d->horizontalLinesColor);
|
|
for (uint y = 0; y <= d->horizontalLinesCount + 1; y++) {
|
|
// note that the y_coord starts from 0. so we draw from pixel number 0 to h-1. Thus the -1 in the y_coord
|
|
int y_coord = top + (y * (h - 1)) / (d->horizontalLinesCount + 1); // Make sure it's y*h first to avoid rounding bugs
|
|
p->drawLine(0, y_coord, w - 2, y_coord);
|
|
}
|
|
}
|
|
|
|
double SignalPlotter::lastValue(uint i) const
|
|
{
|
|
if (d->plotData.isEmpty() || d->plotData.first().size() <= (int)i) {
|
|
return 0;
|
|
}
|
|
return d->plotData.first()[i];
|
|
}
|
|
|
|
QString SignalPlotter::lastValueAsString(uint i) const
|
|
{
|
|
if (d->plotData.isEmpty()) {
|
|
return QString();
|
|
}
|
|
double value = d->plotData.first()[i] / d->scaledBy; // retrieve the newest value for this plot then scale it correct
|
|
QString number = KGlobal::locale()->formatNumber(value, (value >= 100)?0:2);
|
|
return QString("%1 %2").arg(number, d->unit);
|
|
}
|
|
|
|
} // Plasma namespace
|
|
|
|
#include "signalplotter.moc"
|
|
|