mirror of
https://bitbucket.org/smil3y/kde-workspace.git
synced 2025-02-24 19:02:51 +00:00

note: plasma already has a signal plotter Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
1076 lines
36 KiB
C++
1076 lines
36 KiB
C++
/*
|
|
This file is part of the KDE project
|
|
|
|
Copyright (c) 2006 - 2009 John Tapsell <tapsell@kde.org>
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public
|
|
License version 2 or at your option version 3 as published by
|
|
the Free Software Foundation.
|
|
|
|
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 "ksignalplotter.h"
|
|
#include "ksignalplotter_p.h"
|
|
|
|
#include <QtGui/QPainter>
|
|
#include <QtGui/QPixmap>
|
|
#include <QtGui/QPainterPath>
|
|
#include <QtGui/QPen>
|
|
#include <QtGui/qevent.h>
|
|
#include <QEvent>
|
|
|
|
#include <kdebug.h>
|
|
#include <kglobal.h>
|
|
#include <klocale.h>
|
|
#include <kiconloader.h>
|
|
|
|
#include <cmath> //For floor, ceil, log10 etc for calculating ranges
|
|
#include <limits>
|
|
|
|
#ifdef SVG_SUPPORT
|
|
#include <plasma/svg.h>
|
|
#endif
|
|
|
|
|
|
#define VERTICAL_LINE_OFFSET 1
|
|
//Never store less 1000 samples if not visible. This is kinda arbituary
|
|
#define NUM_SAMPLES_WHEN_INVISIBLE ((uint)1000)
|
|
|
|
KSignalPlotter::KSignalPlotter( QWidget *parent)
|
|
: QWidget(parent), d(new KSignalPlotterPrivate(this))
|
|
{
|
|
qRegisterMetaType<KLocalizedString>("KLocalizedString");
|
|
// Anything smaller than this does not make sense.
|
|
setMinimumSize( KIconLoader::SizeSmall, KIconLoader::SizeSmall);
|
|
QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
sizePolicy.setHeightForWidth(false);
|
|
setSizePolicy( sizePolicy );
|
|
}
|
|
|
|
KSignalPlotter::~KSignalPlotter()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
KLocalizedString KSignalPlotter::unit() const {
|
|
return d->mUnit;
|
|
}
|
|
void KSignalPlotter::setUnit(const KLocalizedString &unit) {
|
|
d->mUnit = unit;
|
|
}
|
|
|
|
void KSignalPlotter::addBeam( const QColor &color )
|
|
{
|
|
QList< QList<qreal> >::Iterator it;
|
|
//When we add a new beam, go back and set the data for this beam to NaN for all the other times, to pad it out.
|
|
//This is because it makes it easier for moveSensors
|
|
for(it = d->mBeamData.begin(); it != d->mBeamData.end(); ++it) {
|
|
(*it).append( std::numeric_limits<qreal>::quiet_NaN() );
|
|
}
|
|
d->mBeamColors.append(color);
|
|
d->mBeamColorsLight.append(color.lighter());
|
|
}
|
|
|
|
QColor KSignalPlotter::beamColor( int index ) const {
|
|
return d->mBeamColors[ index ];
|
|
}
|
|
|
|
void KSignalPlotter::setBeamColor( int index, const QColor &color ) {
|
|
if(!color.isValid()) {
|
|
kDebug(1215) << "Invalid color";
|
|
return;
|
|
}
|
|
if( index >= d->mBeamColors.count() ) {
|
|
kDebug(1215) << "Invalid index" << index;
|
|
return;
|
|
}
|
|
Q_ASSERT(d->mBeamColors.count() == d->mBeamColorsLight.count());
|
|
|
|
d->mBeamColors[ index ] = color;
|
|
d->mBeamColorsLight[ index ] = color.lighter();
|
|
}
|
|
|
|
int KSignalPlotter::numBeams() const {
|
|
return d->mBeamColors.count();
|
|
}
|
|
|
|
void KSignalPlotter::addSample( const QList<qreal>& sampleBuf )
|
|
{
|
|
d->addSample(sampleBuf);
|
|
d->mGraphWidget->update();
|
|
}
|
|
void KSignalPlotter::reorderBeams( const QList<int>& newOrder )
|
|
{
|
|
d->reorderBeams(newOrder);
|
|
}
|
|
|
|
void KSignalPlotter::changeRange( qreal min, qreal max )
|
|
{
|
|
if( min == d->mUserMinValue && max == d->mUserMaxValue ) return;
|
|
d->mUserMinValue = min;
|
|
d->mUserMaxValue = max;
|
|
d->calculateNiceRange();
|
|
}
|
|
|
|
void KSignalPlotter::removeBeam( int index )
|
|
{
|
|
if(index >= d->mBeamColors.size()) return;
|
|
if(index >= d->mBeamColorsLight.size()) return;
|
|
d->mBeamColors.removeAt( index );
|
|
d->mBeamColorsLight.removeAt(index);
|
|
|
|
QList< QList<qreal> >::Iterator i;
|
|
for(i = d->mBeamData.begin(); i != d->mBeamData.end(); ++i) {
|
|
if( (*i).size() >= index)
|
|
(*i).removeAt(index);
|
|
}
|
|
if(d->mUseAutoRange)
|
|
d->rescale();
|
|
}
|
|
|
|
void KSignalPlotter::setScaleDownBy( qreal value )
|
|
{
|
|
if(d->mScaleDownBy == value) return;
|
|
d->mScaleDownBy = value;
|
|
d->mBackgroundImage = QPixmap(); //we changed a paint setting, so reset the cache
|
|
d->calculateNiceRange();
|
|
update();
|
|
}
|
|
qreal KSignalPlotter::scaleDownBy() const
|
|
{
|
|
return d->mScaleDownBy;
|
|
}
|
|
|
|
void KSignalPlotter::setUseAutoRange( bool value )
|
|
{
|
|
d->mUseAutoRange = value;
|
|
d->calculateNiceRange();
|
|
//this change will be detected in paint and the image cache regenerated
|
|
}
|
|
|
|
bool KSignalPlotter::useAutoRange() const
|
|
{
|
|
return d->mUseAutoRange;
|
|
}
|
|
|
|
void KSignalPlotter::setMinimumValue( qreal min )
|
|
{
|
|
if(min == d->mUserMinValue) return;
|
|
d->mUserMinValue = min;
|
|
d->calculateNiceRange();
|
|
update();
|
|
//this change will be detected in paint and the image cache regenerated
|
|
}
|
|
|
|
qreal KSignalPlotter::minimumValue() const
|
|
{
|
|
return d->mUserMinValue;
|
|
}
|
|
|
|
void KSignalPlotter::setMaximumValue( qreal max )
|
|
{
|
|
if(max == d->mUserMaxValue) return;
|
|
d->mUserMaxValue = max;
|
|
d->calculateNiceRange();
|
|
update();
|
|
//this change will be detected in paint and the image cache regenerated
|
|
}
|
|
|
|
qreal KSignalPlotter::maximumValue() const
|
|
{
|
|
return d->mUserMaxValue;
|
|
}
|
|
|
|
qreal KSignalPlotter::currentMaximumRangeValue() const
|
|
{
|
|
return d->mNiceMaxValue;
|
|
}
|
|
|
|
qreal KSignalPlotter::currentMinimumRangeValue() const
|
|
{
|
|
return d->mNiceMinValue;
|
|
}
|
|
|
|
void KSignalPlotter::setHorizontalScale( uint scale )
|
|
{
|
|
if (scale == d->mHorizontalScale || scale == 0)
|
|
return;
|
|
|
|
d->mHorizontalScale = scale;
|
|
d->updateDataBuffers();
|
|
#ifdef USE_QIMAGE
|
|
d->mScrollableImage = QImage();
|
|
#else
|
|
d->mScrollableImage = QPixmap();
|
|
#endif
|
|
|
|
update();
|
|
}
|
|
|
|
int KSignalPlotter::horizontalScale() const
|
|
{
|
|
return d->mHorizontalScale;
|
|
}
|
|
|
|
void KSignalPlotter::setShowVerticalLines( bool value )
|
|
{
|
|
if(d->mShowVerticalLines == value) return;
|
|
d->mShowVerticalLines = value;
|
|
d->mBackgroundImage = QPixmap(); //we changed a paint setting, so reset the cache
|
|
#ifdef USE_QIMAGE
|
|
d->mScrollableImage = QImage();
|
|
#else
|
|
d->mScrollableImage = QPixmap();
|
|
#endif
|
|
update();
|
|
}
|
|
|
|
bool KSignalPlotter::showVerticalLines() const
|
|
{
|
|
return d->mShowVerticalLines;
|
|
}
|
|
|
|
void KSignalPlotter::setVerticalLinesDistance( uint distance )
|
|
{
|
|
if(distance == d->mVerticalLinesDistance) return;
|
|
d->mVerticalLinesDistance = distance;
|
|
d->mBackgroundImage = QPixmap(); //we changed a paint setting, so reset the cache
|
|
#ifdef USE_QIMAGE
|
|
d->mScrollableImage = QImage();
|
|
#else
|
|
d->mScrollableImage = QPixmap();
|
|
#endif
|
|
update();
|
|
}
|
|
|
|
uint KSignalPlotter::verticalLinesDistance() const
|
|
{
|
|
return d->mVerticalLinesDistance;
|
|
}
|
|
|
|
void KSignalPlotter::setVerticalLinesScroll( bool value )
|
|
{
|
|
if(value == d->mVerticalLinesScroll) return;
|
|
d->mVerticalLinesScroll = value;
|
|
#ifdef USE_QIMAGE
|
|
d->mScrollableImage = QImage();
|
|
#else
|
|
d->mScrollableImage = QPixmap();
|
|
#endif
|
|
update();
|
|
}
|
|
|
|
bool KSignalPlotter::verticalLinesScroll() const
|
|
{
|
|
return d->mVerticalLinesScroll;
|
|
}
|
|
|
|
void KSignalPlotter::setShowHorizontalLines( bool value )
|
|
{
|
|
if(value == d->mShowHorizontalLines) return;
|
|
d->mShowHorizontalLines = value;
|
|
#ifdef USE_QIMAGE
|
|
d->mScrollableImage = QImage();
|
|
#else
|
|
d->mScrollableImage = QPixmap();
|
|
#endif
|
|
|
|
update();
|
|
}
|
|
|
|
bool KSignalPlotter::showHorizontalLines() const
|
|
{
|
|
return d->mShowHorizontalLines;
|
|
}
|
|
|
|
void KSignalPlotter::setShowAxis( bool value )
|
|
{
|
|
if(value == d->mShowAxis) return;
|
|
d->mShowAxis = value;
|
|
d->mBackgroundImage = QPixmap(); //we changed a paint setting, so reset the cache
|
|
#ifdef USE_QIMAGE
|
|
d->mScrollableImage = QImage();
|
|
#else
|
|
d->mScrollableImage = QPixmap();
|
|
#endif
|
|
update();
|
|
}
|
|
|
|
bool KSignalPlotter::showAxis() const
|
|
{
|
|
return d->mShowAxis;
|
|
}
|
|
|
|
QString KSignalPlotter::svgBackground() const {
|
|
return d->mSvgFilename;
|
|
}
|
|
void KSignalPlotter::setSvgBackground( const QString &filename )
|
|
{
|
|
if(d->mSvgFilename == filename) return;
|
|
d->mSvgFilename = filename;
|
|
#ifdef SVG_SUPPORT
|
|
if(filename.isEmpty()) {
|
|
delete d->mSvgRenderer;
|
|
d->mSvgRenderer = NULL;
|
|
} else {
|
|
if(!d->mSvgRenderer)
|
|
d->mSvgRenderer = new Plasma::Svg(this);
|
|
d->mSvgRenderer->setImagePath(d->mSvgFilename);
|
|
}
|
|
#endif
|
|
d->mBackgroundImage = QPixmap();
|
|
update();
|
|
}
|
|
|
|
void KSignalPlotter::setMaxAxisTextWidth(int axisTextWidth)
|
|
{
|
|
if(d->mAxisTextWidth == axisTextWidth) return;
|
|
d->mAxisTextWidth = axisTextWidth;
|
|
d->mBackgroundImage = QPixmap(); //we changed a paint setting, so reset the cache
|
|
update();
|
|
}
|
|
|
|
int KSignalPlotter::maxAxisTextWidth() const
|
|
{
|
|
return d->mAxisTextWidth;
|
|
}
|
|
void KSignalPlotter::changeEvent ( QEvent * event )
|
|
{
|
|
switch (event->type()) {
|
|
case QEvent::ApplicationPaletteChange:
|
|
case QEvent::PaletteChange:
|
|
case QEvent::FontChange:
|
|
case QEvent::LanguageChange:
|
|
case QEvent::LocaleChange:
|
|
case QEvent::LayoutDirectionChange:
|
|
d->mBackgroundImage = QPixmap(); //we changed a paint setting, so reset the cache
|
|
update();
|
|
break;
|
|
default: //Do nothing
|
|
break;
|
|
}
|
|
}
|
|
|
|
void KSignalPlotter::resizeEvent( QResizeEvent* event )
|
|
{
|
|
QRect boundingBox(QPoint(0,0), event->size());
|
|
int fontHeight = fontMetrics().height();
|
|
if( d->mShowAxis && d->mAxisTextWidth != 0 && boundingBox.width() > (d->mAxisTextWidth*1.10+2) && boundingBox.height() > fontHeight ) { //if there's room to draw the labels, then draw them!
|
|
//We want to adjust the size of plotter bit inside so that the axis text aligns nicely at the top and bottom
|
|
//but we don't want to sacrifice too much of the available room, so don't use it if it will take more than 20% of the available space
|
|
qreal offset = (fontHeight+1)/2;
|
|
if(offset < boundingBox.height() * 0.1)
|
|
boundingBox.adjust(0,offset, 0, -offset);
|
|
|
|
int padding = 1;
|
|
if(boundingBox.width() > d->mAxisTextWidth + 50)
|
|
padding = 10; //If there's plenty of room, at 10 pixels for padding the axis text, so that it looks nice
|
|
|
|
if ( layoutDirection() == Qt::RightToLeft )
|
|
boundingBox.setRight(boundingBox.right() - d->mAxisTextWidth - padding);
|
|
else
|
|
boundingBox.setLeft(d->mAxisTextWidth+padding);
|
|
d->mActualAxisTextWidth = d->mAxisTextWidth;
|
|
} else {
|
|
d->mActualAxisTextWidth = 0;
|
|
}
|
|
if(d->mShowThinFrame)
|
|
boundingBox.adjust(0,0,-1,-1);
|
|
|
|
// Remember bounding box to pass to update, so that we only update the plotting area the next time, if that's the only that thing that has changed
|
|
d->mPlottingArea = boundingBox;
|
|
|
|
//Calculate the new number of horizontal lines
|
|
int newHorizontalLinesCount = qBound(0, (int)(boundingBox.height() / fontHeight)-2, 4);
|
|
if(newHorizontalLinesCount != d->mHorizontalLinesCount) {
|
|
d->mHorizontalLinesCount = newHorizontalLinesCount;
|
|
d->calculateNiceRange();
|
|
}
|
|
#ifdef USE_QIMAGE
|
|
d->mScrollableImage = QImage();
|
|
#else
|
|
d->mScrollableImage = QPixmap();
|
|
#endif
|
|
|
|
d->mGraphWidget->setVisible(true);
|
|
d->mGraphWidget->setGeometry(boundingBox);
|
|
|
|
d->updateDataBuffers();
|
|
}
|
|
|
|
KSignalPlotterPrivate::KSignalPlotterPrivate(KSignalPlotter *q_ptr_) : q(q_ptr_)
|
|
{
|
|
mPrecision = 0;
|
|
mMaxSamples = NUM_SAMPLES_WHEN_INVISIBLE;
|
|
mMinValue = mMaxValue = std::numeric_limits<qreal>::quiet_NaN();
|
|
mUserMinValue = mUserMaxValue = 0.0;
|
|
mNiceMinValue = mNiceMaxValue = 0.0;
|
|
mNiceRange = 0;
|
|
mUseAutoRange = true;
|
|
mScaleDownBy = 1;
|
|
mShowThinFrame = true;
|
|
mSmoothGraph = true;
|
|
mShowVerticalLines = false;
|
|
mVerticalLinesDistance = 30;
|
|
mVerticalLinesScroll = true;
|
|
mVerticalLinesOffset = 0;
|
|
mHorizontalScale = 6;
|
|
mShowHorizontalLines = true;
|
|
mHorizontalLinesCount = 4;
|
|
mShowAxis = true;
|
|
mAxisTextWidth = 0;
|
|
mScrollOffset = 0;
|
|
mStackBeams = false;
|
|
mFillOpacity = 20;
|
|
mRescaleTime = 0;
|
|
mUnit = ki18n("%1");
|
|
mAxisTextOverlapsPlotter = false;
|
|
mActualAxisTextWidth = 0;
|
|
mGraphWidget = new GraphWidget(q); ///< This is the widget that draws the actual graph
|
|
mGraphWidget->signalPlotterPrivate = this;
|
|
mGraphWidget->setVisible(false);
|
|
}
|
|
|
|
void KSignalPlotterPrivate::recalculateMaxMinValueForSample(const QList<qreal>&sampleBuf, int time )
|
|
{
|
|
if(mStackBeams) {
|
|
qreal value=0;
|
|
for(int i = sampleBuf.count()-1; i>= 0; i--) {
|
|
qreal newValue = sampleBuf[i];
|
|
if( !std::isinf(newValue) && !std::isnan(newValue) )
|
|
value += newValue;
|
|
}
|
|
if(std::isnan(mMinValue) || mMinValue > value) mMinValue = value;
|
|
if(std::isnan(mMaxValue) || mMaxValue < value) mMaxValue = value;
|
|
if(value > 0.7*mMaxValue)
|
|
mRescaleTime = time;
|
|
} else {
|
|
qreal value;
|
|
for(int i = sampleBuf.count()-1; i>= 0; i--) {
|
|
value = sampleBuf[i];
|
|
if( !std::isinf(value) && !std::isnan(value) ) {
|
|
if(std::isnan(mMinValue) || mMinValue > value) mMinValue = value;
|
|
if(std::isnan(mMaxValue) || mMaxValue < value) mMaxValue = value;
|
|
if(value > 0.7*mMaxValue)
|
|
mRescaleTime = time;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void KSignalPlotterPrivate::rescale() {
|
|
mMaxValue = mMinValue = std::numeric_limits<qreal>::quiet_NaN();
|
|
for(int i = mBeamData.count()-1; i >= 0; i--) {
|
|
recalculateMaxMinValueForSample(mBeamData[i], i);
|
|
}
|
|
calculateNiceRange();
|
|
}
|
|
|
|
void KSignalPlotterPrivate::addSample( const QList<qreal>& sampleBuf )
|
|
{
|
|
if(sampleBuf.count() != mBeamColors.count()) {
|
|
kDebug(1215) << "Sample data discarded - contains wrong number of beams";
|
|
return;
|
|
}
|
|
mBeamData.prepend(sampleBuf);
|
|
if((unsigned int)mBeamData.size() > mMaxSamples) {
|
|
mBeamData.removeLast(); // we have too many. Remove the last item
|
|
if((unsigned int)mBeamData.size() > mMaxSamples)
|
|
mBeamData.removeLast(); // 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
|
|
}
|
|
|
|
if(mUseAutoRange) {
|
|
recalculateMaxMinValueForSample(sampleBuf, 0);
|
|
|
|
if(mRescaleTime++ > mMaxSamples)
|
|
rescale();
|
|
else if(mMinValue < mNiceMinValue || mMaxValue > mNiceMaxValue || (mMaxValue > mUserMaxValue && mNiceRange != 1 && mMaxValue < (mNiceRange*0.75 + mNiceMinValue)) || mNiceRange == 0)
|
|
calculateNiceRange();
|
|
} else {
|
|
if(mMinValue < mNiceMinValue || mMaxValue > mNiceMaxValue || (mMaxValue > mUserMaxValue && mNiceRange != 1 && mMaxValue < (mNiceRange*0.75 + mNiceMinValue)) || mNiceRange == 0)
|
|
calculateNiceRange();
|
|
}
|
|
|
|
if(mScrollableImage.isNull())
|
|
return;
|
|
QPainter pCache(&mScrollableImage);
|
|
drawBeamToScrollableImage(&pCache, 0);
|
|
}
|
|
|
|
void KSignalPlotterPrivate::reorderBeams( const QList<int>& newOrder )
|
|
{
|
|
if(newOrder.count() != mBeamColors.count()) {
|
|
return;
|
|
}
|
|
QList< QList<qreal> >::Iterator it;
|
|
for(it = mBeamData.begin(); it != mBeamData.end(); ++it) {
|
|
if(newOrder.count() != (*it).count()) {
|
|
kWarning(1215) << "Serious problem in move sample. beamdata[i] has " << (*it).count() << " and neworder has " << newOrder.count();
|
|
} else {
|
|
QList<qreal> newBeam;
|
|
for(int i = 0; i < newOrder.count(); i++) {
|
|
int newIndex = newOrder[i];
|
|
newBeam.append((*it).at(newIndex));
|
|
}
|
|
(*it) = newBeam;
|
|
}
|
|
}
|
|
QList< QColor > newBeamColors;
|
|
QList< QColor > newBeamColorsDark;
|
|
for(int i = 0; i < newOrder.count(); i++) {
|
|
int newIndex = newOrder[i];
|
|
newBeamColors.append(mBeamColors.at(newIndex));
|
|
newBeamColorsDark.append(mBeamColorsLight.at(newIndex));
|
|
}
|
|
mBeamColors = newBeamColors;
|
|
mBeamColorsLight = newBeamColorsDark;
|
|
}
|
|
|
|
void KSignalPlotterPrivate::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.
|
|
*/
|
|
if(q->isVisible())
|
|
mMaxSamples = uint(q->size().width() / mHorizontalScale + 4);
|
|
else //If it's not visible, we can't rely on sensible values for width. Store some minimum number of data points
|
|
mMaxSamples = qMin((uint)(q->size().width() / mHorizontalScale + 4), NUM_SAMPLES_WHEN_INVISIBLE);
|
|
}
|
|
|
|
void KSignalPlotter::paintEvent( QPaintEvent* event)
|
|
{
|
|
if (testAttribute(Qt::WA_PendingResizeEvent))
|
|
return; // lets not do this more than necessary, shall we?
|
|
QPainter p(this);
|
|
QPainter *painter = &p;
|
|
|
|
uint w = size().width();
|
|
uint h = size().height();
|
|
/* Do not do repaints when the widget is not yet setup properly. */
|
|
if ( w <= 2 || h <= 2 )
|
|
return;
|
|
|
|
bool onlyDrawPlotter = event && d->mPlottingArea.contains(event->rect());
|
|
if(onlyDrawPlotter)
|
|
return; //The painting will be handled entirely by GraphWidget::paintEvent
|
|
|
|
if(!onlyDrawPlotter && d->mShowThinFrame)
|
|
d->drawThinFrame(painter, d->mPlottingArea.adjusted(0,0,1,1)); //We have a 'frame' in the bottom and right - so subtract them from the view
|
|
|
|
if(d->mShowAxis) {
|
|
int fontheight = fontMetrics().height();
|
|
if( d->mPlottingArea.height() > fontheight ) { //if there's room to draw the labels, then draw them!
|
|
d->drawAxisText(painter, QRect(0,0,w,h));
|
|
}
|
|
}
|
|
}
|
|
|
|
void GraphWidget::paintEvent( QPaintEvent*)
|
|
{
|
|
if (testAttribute(Qt::WA_PendingResizeEvent))
|
|
return; // lets not do this more than necessary, shall we?
|
|
|
|
uint w = width();
|
|
uint h = height();
|
|
/* Do not do repaints when the widget is not yet setup properly. */
|
|
if ( w <= 2 || h <= 2 )
|
|
return;
|
|
QPainter p(this);
|
|
|
|
signalPlotterPrivate->drawWidget(&p, QRect(0,0,w,h));
|
|
|
|
if(signalPlotterPrivate->mAxisTextOverlapsPlotter && signalPlotterPrivate->mShowAxis) {
|
|
uint fontheight = signalPlotterPrivate->q->fontMetrics().height();
|
|
if( h > fontheight ) { //if there's room to draw the labels, then draw them!
|
|
QSize originalSize = signalPlotterPrivate->q->size();
|
|
signalPlotterPrivate->drawAxisText(&p, QRect(-signalPlotterPrivate->mPlottingArea.topLeft(), originalSize));
|
|
}
|
|
}
|
|
}
|
|
|
|
void KSignalPlotterPrivate::drawWidget(QPainter *p, const QRect &boundingBox)
|
|
{
|
|
#ifdef SVG_SUPPORT
|
|
if(!mSvgFilename.isEmpty()) {
|
|
if(mBackgroundImage.isNull() || mBackgroundImage.height() != boundingBox.height() || mBackgroundImage.width() != boundingBox.width()) { //recreate on resize etc
|
|
updateSvgBackground(boundingBox);
|
|
}
|
|
p->drawPixmap(boundingBox, mBackgroundImage);
|
|
}
|
|
#endif
|
|
|
|
if( mScrollableImage.isNull() )
|
|
redrawScrollableImage();
|
|
|
|
//We draw the pixmap in two halves, wrapping around the window
|
|
if(mScrollOffset > 1) {
|
|
#ifdef USE_QIMAGE
|
|
p->drawImage(boundingBox.right() - mScrollOffset+2, boundingBox.top(), mScrollableImage, 0, 0, mScrollOffset-1, boundingBox.height());
|
|
#else
|
|
p->drawPixmap(boundingBox.right() - mScrollOffset+2, boundingBox.top(), mScrollableImage, 0, 0, mScrollOffset-1, boundingBox.height());
|
|
#endif
|
|
}
|
|
int widthOfSecondHalf = boundingBox.width() - mScrollOffset + 1;
|
|
if(widthOfSecondHalf > 0) {
|
|
#ifdef USE_QIMAGE
|
|
p->drawImage(boundingBox.left(), boundingBox.top(), mScrollableImage, mScrollableImage.width() - widthOfSecondHalf-1, 0, widthOfSecondHalf, boundingBox.height());
|
|
#else
|
|
p->drawPixmap(boundingBox.left(), boundingBox.top(), mScrollableImage, mScrollableImage.width() - widthOfSecondHalf-1, 0, widthOfSecondHalf, boundingBox.height());
|
|
#endif
|
|
}
|
|
|
|
/* 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(mShowVerticalLines && !mVerticalLinesScroll)
|
|
drawVerticalLines(p, boundingBox);
|
|
}
|
|
void KSignalPlotterPrivate::drawBackground(QPainter *p, const QRect &boundingBox) const
|
|
{
|
|
p->setRenderHint(QPainter::Antialiasing, false);
|
|
#ifdef SVG_SUPPORT
|
|
if(!mSvgFilename.isEmpty()) //our background is an svg, so don't paint over the top of it
|
|
p->fillRect(boundingBox, Qt::transparent);
|
|
else
|
|
#endif
|
|
p->fillRect(boundingBox, q->palette().brush(QPalette::Base));
|
|
|
|
if ( mShowHorizontalLines )
|
|
drawHorizontalLines(p, boundingBox.adjusted(0,0,1,0));
|
|
|
|
if ( mShowVerticalLines && mVerticalLinesScroll )
|
|
drawVerticalLines(p, boundingBox, mVerticalLinesOffset);
|
|
|
|
p->setRenderHint(QPainter::Antialiasing, true);
|
|
}
|
|
|
|
bool KSignalPlotter::thinFrame() const
|
|
{
|
|
return d->mShowThinFrame;
|
|
}
|
|
void KSignalPlotter::setThinFrame(bool thinFrame)
|
|
{
|
|
if(thinFrame == d->mShowThinFrame)
|
|
return;
|
|
|
|
d->mShowThinFrame = thinFrame;
|
|
update(); //Trigger a repaint
|
|
}
|
|
|
|
#ifdef SVG_SUPPORT
|
|
void KSignalPlotterPrivate::updateSvgBackground(const QRect &boundingBox)
|
|
{
|
|
Q_ASSERT(!mSvgFilename.isEmpty());
|
|
Q_ASSERT(boundingBox.isNull());
|
|
mBackgroundImage = QPixmap(boundingBox.width(), boundingBox.height());
|
|
Q_ASSERT(!mBackgroundImage.isNull());
|
|
QPainter pCache(&mBackgroundImage);
|
|
pCache.fill( q->palette().color(QPalette::Base) );
|
|
|
|
svgRenderer->resize(boundingBox.size());
|
|
svgRenderer->paint(&pCache, 0, 0);
|
|
|
|
}
|
|
#endif
|
|
void KSignalPlotterPrivate::redrawScrollableImage()
|
|
{
|
|
//Align width of bounding box to the size of the horizontal scale
|
|
int alignedWidth = ((mPlottingArea.width() + 1) / mHorizontalScale + 1) * mHorizontalScale;
|
|
//Redraw the whole thing
|
|
#ifdef USE_QIMAGE
|
|
mScrollableImage = QImage(alignedWidth, mPlottingArea.height(),QImage::Format_ARGB32_Premultiplied);
|
|
#else
|
|
mScrollableImage = QPixmap(alignedWidth, mPlottingArea.height());
|
|
#endif
|
|
Q_ASSERT(!mScrollableImage.isNull());
|
|
|
|
mScrollOffset = 0;
|
|
mVerticalLinesOffset = mVerticalLinesDistance - mHorizontalScale+1; // mVerticalLinesDistance - alignedWidth % mVerticalLinesDistance;
|
|
//We need to draw the background for areas without a beam
|
|
int withoutBeamWidth = qMax(mBeamData.size()-1, 0) * mHorizontalScale;
|
|
QPainter pCache(&mScrollableImage);
|
|
if(withoutBeamWidth < mScrollableImage.width())
|
|
drawBackground(&pCache, QRect(withoutBeamWidth, 0, alignedWidth - withoutBeamWidth, mScrollableImage.height()));
|
|
|
|
/* Draw scope-like grid vertical lines */
|
|
mVerticalLinesOffset = 0;
|
|
if(mBeamData.size() > 2) {
|
|
for(int i = mBeamData.size()-2; i >= 0; i--)
|
|
drawBeamToScrollableImage(&pCache, i);
|
|
}
|
|
}
|
|
|
|
void KSignalPlotterPrivate::drawThinFrame(QPainter *p, const QRect &boundingBox)
|
|
{
|
|
/* Draw a line along the bottom and the right side of the
|
|
* widget to create a 3D like look. */
|
|
p->setRenderHint(QPainter::Antialiasing, false);
|
|
p->setPen( QPen(q->palette().color( QPalette::Light ), 0) );
|
|
p->drawLine( boundingBox.bottomLeft(), boundingBox.bottomRight());
|
|
p->drawLine( boundingBox.bottomRight(), boundingBox.topRight());
|
|
p->setRenderHint(QPainter::Antialiasing, true);
|
|
}
|
|
|
|
void KSignalPlotterPrivate::calculateNiceRange()
|
|
{
|
|
qreal max = mUserMaxValue;
|
|
qreal min = mUserMinValue;
|
|
if( mUseAutoRange ) {
|
|
if(!std::isnan(mMaxValue) && mMaxValue * 0.99 > max) //Allow max value to go very slightly over the given max, for rounding reasons
|
|
max = mMaxValue;
|
|
if(!std::isnan(mMinValue) && mMinValue * 0.99 < min) {
|
|
min = mMinValue;
|
|
}
|
|
}
|
|
|
|
/* If the range is too small we will force it to 1.0 since it
|
|
* looks a lot nicer. */
|
|
if(max - min < 0.000001 )
|
|
max = min +1;
|
|
|
|
qreal range = max - min;
|
|
|
|
// Massage the range so that the grid shows some nice values.
|
|
qreal step;
|
|
int number_lines_above_zero = 0;
|
|
int number_lines_below_zero = 0;
|
|
//If y=0 is visible and have at least 1 horizontal lines make sure that we have a line crossing through 0
|
|
bool alignToXAxis = min < 0 && max > 0 && mHorizontalLinesCount >= 1;
|
|
if( alignToXAxis ) {
|
|
number_lines_above_zero = int( mHorizontalLinesCount * max / range);
|
|
number_lines_below_zero = mHorizontalLinesCount - number_lines_above_zero -1; //subtract 1 line for the actual 0 line
|
|
step = qMax( max / (mScaleDownBy*(number_lines_above_zero+1)), -min/(mScaleDownBy*(number_lines_below_zero+1)));
|
|
} else
|
|
step = range / (mScaleDownBy*(mHorizontalLinesCount+1));
|
|
|
|
const int sigFigs = 2; //Number of significant figures of the step to use. Update the 0.05 below if this changes
|
|
int logdim = (int)floor( log10( step ) ) - (sigFigs-1); //find the order of the number, reduced by 1. E.g. if step=1234 then logdim is 3-1=2
|
|
qreal dim = pow( (qreal)10.0, logdim ); //e.g. if step=1234, logdim=2, so dim = 100
|
|
int a = (int)ceil( step / dim - 0.000005); //so a = ceil(1234/100) = ceil(12.34) = 13 (we subtract an epsilon)
|
|
|
|
if(logdim >= 0)
|
|
mPrecision = 0; //Work out the number of decimal places. e.g. If step=1.5, then logdim = -1 so precision is 1
|
|
else if(a % 10 == 0) //We just happened to round to a nice number, requiring 1 less precision
|
|
mPrecision = -logdim - 1;
|
|
else
|
|
mPrecision = -logdim;
|
|
step = dim*a; //e.g. if step=1234, dim=100, a=13 so step= 1300. So 1234 was rounded up to 1300
|
|
|
|
range = mScaleDownBy * step * (mHorizontalLinesCount+1);
|
|
|
|
if( alignToXAxis ) {
|
|
max = mScaleDownBy * step * (number_lines_above_zero+1);
|
|
min = -mScaleDownBy * step * (number_lines_below_zero+1);
|
|
} else
|
|
max = min + range;
|
|
|
|
if( mNiceMinValue == min && mNiceRange == range)
|
|
return; //nothing changed
|
|
|
|
mNiceMaxValue = max;
|
|
mNiceMinValue = min;
|
|
mNiceRange = range;
|
|
|
|
#ifdef USE_QIMAGE
|
|
mScrollableImage = QImage();
|
|
#else
|
|
mScrollableImage = QPixmap();
|
|
#endif
|
|
emit q->axisScaleChanged();
|
|
q->update();
|
|
}
|
|
|
|
void KSignalPlotterPrivate::drawVerticalLines(QPainter *p, const QRect &boundingBox, int offset) const
|
|
{
|
|
QColor color = q->palette().color(QPalette::Window);
|
|
if(!mVerticalLinesScroll)
|
|
color.setAlpha(127);
|
|
p->setPen( QPen(color, 0) );
|
|
|
|
p->setRenderHint(QPainter::Antialiasing, false);
|
|
for ( int x = boundingBox.right() - ( offset % mVerticalLinesDistance); x >= boundingBox.left(); x -= mVerticalLinesDistance )
|
|
p->drawLine( x, boundingBox.top(), x, boundingBox.bottom() );
|
|
p->setRenderHint(QPainter::Antialiasing, true);
|
|
}
|
|
|
|
void KSignalPlotterPrivate::drawBeamToScrollableImage(QPainter *p, int index)
|
|
{
|
|
QRect cacheBoundingBox = QRect(mScrollOffset, 0, mHorizontalScale, mScrollableImage.height());
|
|
|
|
drawBackground(p, cacheBoundingBox);
|
|
drawBeam(p, cacheBoundingBox, mHorizontalScale, index);
|
|
|
|
mScrollOffset += mHorizontalScale;
|
|
mVerticalLinesOffset = (mVerticalLinesOffset + mHorizontalScale) % mVerticalLinesDistance;
|
|
if(mScrollOffset >= mScrollableImage.width()-1) {
|
|
mScrollOffset = 0;
|
|
mVerticalLinesOffset--; //We skip over the last pixel drawn
|
|
}
|
|
}
|
|
|
|
void KSignalPlotterPrivate::drawBeam(QPainter *p, const QRect &boundingBox, int horizontalScale, int index)
|
|
{
|
|
if(mNiceRange == 0) return;
|
|
QPen pen;
|
|
if(mHorizontalScale == 1) //Don't use a pen width of 2 if there's only 1 pixel between points
|
|
pen.setWidth(1);
|
|
else
|
|
pen.setWidth(2);
|
|
|
|
pen.setCapStyle(Qt::FlatCap);
|
|
|
|
qreal scaleFac = (boundingBox.height()-2) / mNiceRange;
|
|
if(mBeamData.size() - 1 <= index )
|
|
return; // Something went wrong?
|
|
|
|
const QList<qreal> &datapoints = mBeamData[index];
|
|
const QList<qreal> &prev_datapoints = mBeamData[index+1];
|
|
bool hasPrevPrevDatapoints = (index +2 < mBeamData.size()); //used for bezier curve gradient calculation
|
|
const QList<qreal> &prev_prev_datapoints = hasPrevPrevDatapoints?mBeamData[index+2]:prev_datapoints;
|
|
|
|
qreal x0 = boundingBox.right();
|
|
qreal x1 = qMax(boundingBox.right() - horizontalScale, 0);
|
|
|
|
qreal xaxis = boundingBox.bottom();
|
|
if( mNiceMinValue < 0)
|
|
xaxis = qMax(qreal(xaxis + mNiceMinValue*scaleFac), qreal(boundingBox.top()));
|
|
|
|
const int count = qMin(datapoints.size(), mBeamColors.size());
|
|
QVector<QPainterPath> paths(count);
|
|
QPointF previous_c0;
|
|
QPointF previous_c1;
|
|
QPointF previous_c2;
|
|
QPointF previous_c3;
|
|
qreal previous_point0 = 0;
|
|
qreal previous_point1 = 0;
|
|
qreal previous_point2 = 0;
|
|
bool firstLine = true;
|
|
for (int j = 0; j < count; ++j) {
|
|
qreal point0 = datapoints[j];
|
|
if( std::isnan(point0) )
|
|
continue; //Just do not draw points with nans. skip them
|
|
|
|
qreal point1 = prev_datapoints[j];
|
|
qreal point2 = prev_prev_datapoints[j];
|
|
|
|
if(std::isnan(point1))
|
|
point1 = point0;
|
|
else if(mSmoothGraph && !std::isinf(point1)) {
|
|
// Apply a weighted average just to smooth the graph out a bit
|
|
// Do not try to smooth infinities or nans
|
|
point0 = (2*point0 + point1)/3;
|
|
if(!std::isnan(point2) && !std::isinf(point2))
|
|
point1 = (2*point1 + point2)/3;
|
|
// We don't bother to average out y2. This will introduce slight inaccuracies in the gradients, but they aren't really noticeable.
|
|
}
|
|
if(std::isnan(point2))
|
|
point2 = point1;
|
|
|
|
if (mStackBeams) {
|
|
point0 = previous_point0 = previous_point0 + point0;
|
|
point1 = previous_point1 = previous_point1 + point1;
|
|
point2 = previous_point2 = previous_point2 + point2;
|
|
}
|
|
|
|
qreal y0 = qBound((qreal)boundingBox.top(), boundingBox.bottom() - (point0 - mNiceMinValue)*scaleFac, (qreal)boundingBox.bottom());
|
|
qreal y1 = qBound((qreal)boundingBox.top(), boundingBox.bottom() - (point1 - mNiceMinValue)*scaleFac, (qreal)boundingBox.bottom());
|
|
qreal y2 = qBound((qreal)boundingBox.top(), boundingBox.bottom() - (point2 - mNiceMinValue)*scaleFac, (qreal)boundingBox.bottom());
|
|
|
|
QPainterPath &path = paths[j];
|
|
path.moveTo( x1, y1);
|
|
QPointF c1( x1 + horizontalScale/3.0, (4* y1 - y2)/3.0 );//Control point 1 - same gradient as prev_prev_datapoint to prev_datapoint
|
|
QPointF c2( x1 + 2*horizontalScale/3.0, (2* y0 + y1)/3.0);//Control point 2 - same gradient as prev_datapoint to datapoint
|
|
QPointF c3(x0,y0);
|
|
path.cubicTo( c1, c2, c3 );
|
|
if(mFillOpacity) {
|
|
QPainterPath fillPath = path; //Make a copy to do our fill
|
|
if (!mStackBeams || firstLine) {
|
|
fillPath.lineTo(x0, xaxis);
|
|
fillPath.lineTo(x1, xaxis);
|
|
fillPath.lineTo(x1,y1);
|
|
} else {
|
|
fillPath.lineTo(x0, previous_c3.y());
|
|
fillPath.cubicTo(previous_c2, previous_c1, previous_c0);
|
|
fillPath.lineTo(x1, y1);
|
|
}
|
|
if (mStackBeams) {
|
|
previous_c0 = QPointF(x1,y1);
|
|
previous_c1 = c1;
|
|
previous_c2 = c2;
|
|
previous_c3 = c3;
|
|
}
|
|
QColor fillColor = mBeamColors[j];
|
|
fillColor.setAlpha(mFillOpacity);
|
|
p->fillPath(fillPath, fillColor);
|
|
}
|
|
firstLine = false;
|
|
}
|
|
for(int j = count-1; j >= 0 ; --j) {
|
|
if(mFillOpacity)
|
|
pen.setColor(mBeamColorsLight.at(j));
|
|
else
|
|
pen.setColor(mBeamColors.at(j));
|
|
p->strokePath(paths.at(j),pen);
|
|
}
|
|
}
|
|
void KSignalPlotterPrivate::drawAxisText(QPainter *p, const QRect &boundingBox)
|
|
{
|
|
if(mHorizontalLinesCount < 0) return;
|
|
p->setFont( q->font() );
|
|
qreal stepsize = mNiceRange/(mScaleDownBy*(mHorizontalLinesCount+1));
|
|
if(mActualAxisTextWidth == 0) //If we are drawing completely inside the plotter area, using the Text color
|
|
p->setPen( QPen( q->palette().brush(QPalette::Text), 0) );
|
|
else
|
|
p->setPen( QPen( q->palette().brush(QPalette::WindowText), 0) );
|
|
int axisTitleIndex=1;
|
|
QString val;
|
|
int numItems = mHorizontalLinesCount +2;
|
|
int fontHeight = p->fontMetrics().height();
|
|
if(numItems == 2 && boundingBox.height() < fontHeight*2 )
|
|
numItems = 1;
|
|
mAxisTextOverlapsPlotter = false;
|
|
for ( int y = 0; y < numItems; y++, axisTitleIndex++) {
|
|
int y_coord = boundingBox.top() + (y * (boundingBox.height()-fontHeight)) /(mHorizontalLinesCount+1); //Make sure it's y*h first to avoid rounding bugs
|
|
qreal value;
|
|
if(y == mHorizontalLinesCount+1)
|
|
value = mNiceMinValue/mScaleDownBy; //sometimes using the formulas gives us a value very slightly off
|
|
else
|
|
value = mNiceMaxValue/mScaleDownBy - y * stepsize;
|
|
|
|
val = scaledValueAsString(value, mPrecision);
|
|
QRect textBoundingRect = p->fontMetrics().boundingRect(val);
|
|
|
|
if(textBoundingRect.width() > mActualAxisTextWidth)
|
|
mAxisTextOverlapsPlotter = true;
|
|
int offset = qMax(mActualAxisTextWidth - textBoundingRect.right(), -textBoundingRect.left());
|
|
if ( q->layoutDirection() == Qt::RightToLeft )
|
|
p->drawText( boundingBox.left(), y_coord, boundingBox.width() - offset , fontHeight+1, Qt::AlignLeft | Qt::AlignTop, val);
|
|
else
|
|
p->drawText( boundingBox.left() + offset, y_coord, boundingBox.width() - offset, fontHeight+1, Qt::AlignLeft | Qt::AlignTop, val);
|
|
}
|
|
}
|
|
|
|
void KSignalPlotterPrivate::drawHorizontalLines(QPainter *p, const QRect &boundingBox) const
|
|
{
|
|
if(mHorizontalLinesCount <= 0) return;
|
|
p->setPen( QPen(q->palette().color(QPalette::Window) ));
|
|
for ( int y = 0; y <= mHorizontalLinesCount+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 = boundingBox.top() + (y * (boundingBox.height()-1)) / (mHorizontalLinesCount+1); //Make sure it's y*h first to avoid rounding bugs
|
|
p->drawLine( boundingBox.left(), y_coord, boundingBox.right() - 1, y_coord);
|
|
}
|
|
}
|
|
|
|
int KSignalPlotter::currentAxisPrecision() const
|
|
{
|
|
return d->mPrecision;
|
|
}
|
|
|
|
qreal KSignalPlotter::lastValue( int i) const
|
|
{
|
|
if(d->mBeamData.isEmpty() || d->mBeamData.first().size() <= i) return std::numeric_limits<qreal>::quiet_NaN();
|
|
return d->mBeamData.first().at(i);
|
|
}
|
|
QString KSignalPlotter::lastValueAsString( int i, int precision) const
|
|
{
|
|
if(d->mBeamData.isEmpty() || d->mBeamData.first().size() <= i || std::isnan(d->mBeamData.first().at(i))) return QString();
|
|
return valueAsString(d->mBeamData.first().at(i), precision); //retrieve the newest value for this beam
|
|
}
|
|
QString KSignalPlotter::valueAsString( qreal value, int precision) const
|
|
{
|
|
if(std::isnan(value))
|
|
return QString();
|
|
value = value / d->mScaleDownBy; // scale the value. E.g. from Bytes to KiB
|
|
return d->scaledValueAsString(value, precision);
|
|
}
|
|
QString KSignalPlotterPrivate::scaledValueAsString( qreal value, int precision) const
|
|
{
|
|
qreal absvalue = qAbs(value);
|
|
if(precision == -1) {
|
|
if(absvalue >= 99.5)
|
|
precision = 0;
|
|
else if(absvalue>=0.995 || (mScaleDownBy == 1 && mNiceMaxValue > 20))
|
|
precision = 1;
|
|
else
|
|
precision = 2;
|
|
}
|
|
|
|
if( absvalue < 1E6 ) {
|
|
if(precision == 0)
|
|
return mUnit.subs((long)value).toString();
|
|
else
|
|
return mUnit.subs(value, 0, 'f', precision).toString();
|
|
}
|
|
else
|
|
return mUnit.subs(value, 0, 'g', precision).toString();
|
|
}
|
|
|
|
bool KSignalPlotter::smoothGraph() const
|
|
{
|
|
return d->mSmoothGraph;
|
|
}
|
|
|
|
void KSignalPlotter::setSmoothGraph(bool smooth)
|
|
{
|
|
d->mSmoothGraph = smooth;
|
|
#ifdef USE_QIMAGE
|
|
d->mScrollableImage = QImage();
|
|
#else
|
|
d->mScrollableImage = QPixmap();
|
|
#endif
|
|
}
|
|
|
|
bool KSignalPlotter::stackGraph() const
|
|
{
|
|
return d->mStackBeams;
|
|
}
|
|
void KSignalPlotter::setStackGraph(bool stack)
|
|
{
|
|
d->mStackBeams = stack;
|
|
#ifdef USE_QIMAGE
|
|
d->mScrollableImage = QImage();
|
|
#else
|
|
d->mScrollableImage = QPixmap();
|
|
#endif
|
|
|
|
}
|
|
|
|
int KSignalPlotter::fillOpacity() const
|
|
{
|
|
return d->mFillOpacity;
|
|
}
|
|
void KSignalPlotter::setFillOpacity(int fill)
|
|
{
|
|
d->mFillOpacity = fill;
|
|
#ifdef USE_QIMAGE
|
|
d->mScrollableImage = QImage();
|
|
#else
|
|
d->mScrollableImage = QPixmap();
|
|
#endif
|
|
|
|
}
|
|
|
|
QSize KSignalPlotter::sizeHint() const
|
|
{
|
|
return QSize(200,200); //Just a random size which would usually look okay
|
|
}
|
|
|
|
GraphWidget::GraphWidget(QWidget *parent) : QWidget(parent)
|
|
{
|
|
setAttribute(Qt::WA_NoSystemBackground);
|
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
|
}
|