1 /*
2     KSysGuard, the KDE System Guard
3 
4     Copyright (c) 1999 - 2002 Chris Schlaeger <cs@kde.org>
5 
6  This program is free software; you can redistribute it and/or
7  modify it under the terms of the GNU General Public License as
8  published by the Free Software Foundation; either version 2 of
9  the License or (at your option) version 3 or any later version
10  accepted by the membership of KDE e.V. (or its successor approved
11  by the membership of KDE e.V.), which shall act as a proxy
12  defined in Section 14 of version 3 of the license.
13 
14  This program is distributed in the hope that it will be useful,
15  but WITHOUT ANY WARRANTY; without even the implied warranty of
16  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  GNU General Public License for more details.
18 
19  You should have received a copy of the GNU General Public License
20  along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 */
22 
23 #include <QApplication>
24 #include <QDebug>
25 #include <QDomElement>
26 #include <QToolTip>
27 #include <QVBoxLayout>
28 #include <QHBoxLayout>
29 #include <QLabel>
30 #include <QLocale>
31 #include <QResizeEvent>
32 #include <QStandardPaths>
33 
34 #include <KMessageBox>
35 #include <ksignalplotter.h>
36 
37 #include <ksgrd/SensorManager.h>
38 #include "StyleEngine.h"
39 
40 #include "FancyPlotterSettings.h"
41 
42 #include "FancyPlotter.h"
43 
44 // The unicode character 0x25CF is a big filled in circle.  We would prefer
45 // to use this in the tooltip (or other messages). However it's possible
46 // that the font used to draw the tooltip won't have it.  So we fall back to a
47 // "#" instead.
48 static constexpr const int BLACK_CIRCLE = 0x25CF;
circleCharacter(const QFontMetrics & fm)49 static inline QChar circleCharacter(const QFontMetrics& fm)
50 {
51     return fm.inFont(QChar(BLACK_CIRCLE)) ? QChar(BLACK_CIRCLE) : QLatin1Char('#');
52 }
53 
54 class SensorToAdd {
55   public:
56     QRegExp name;
57     QString hostname;
58     QString type;
59     QList<QColor> colors;
60     QString summationName;
61 };
62 
63 class FancyPlotterLabel : public QLabel {
64   public:
FancyPlotterLabel(QWidget * parent)65     FancyPlotterLabel(QWidget *parent) : QLabel(parent) {
66         setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
67         longHeadingWidth = 0;
68         shortHeadingWidth = 0;
69         textMargin = 0;
70         setLayoutDirection(Qt::LeftToRight); //We do this because we organise the strings ourselves.. is this going to muck it up though for RTL languages?
71     }
~FancyPlotterLabel()72     ~FancyPlotterLabel() override {
73     }
setLabel(const QString & name,const QColor & color)74     void setLabel(const QString &name, const QColor &color) {
75         labelName = name;
76 
77         if(indicatorSymbol.isNull()) {
78             indicatorSymbol = circleCharacter(fontMetrics());
79         }
80         changeLabel(color);
81 
82     }
setValueText(const QString & value)83     void setValueText(const QString &value) {
84         //value can have multiple strings, separated with the 0x9c character
85         valueText = value.split(QChar(0x9c));
86         resizeEvent(nullptr);
87         update();
88     }
resizeEvent(QResizeEvent *)89     void resizeEvent( QResizeEvent * ) override {
90         QFontMetrics fm = fontMetrics();
91 
92         if(valueText.isEmpty()) {
93             if(longHeadingWidth < width())
94                 setText(longHeadingText);
95             else
96                 setText(shortHeadingText);
97             return;
98         }
99         QString value = valueText.first();
100 
101         int textWidth = fm.boundingRect(value).width();
102         if(textWidth + longHeadingWidth < width())
103             setBothText(longHeadingText, value);
104         else if(textWidth + shortHeadingWidth < width())
105             setBothText(shortHeadingText, value);
106         else {
107             int valueTextCount = valueText.count();
108             int i;
109             for(i = 1; i < valueTextCount; ++i) {
110                 textWidth = fm.boundingRect(valueText.at(i)).width();
111                 if(textWidth + shortHeadingWidth <= width()) {
112                     break;
113                 }
114             }
115             if(i < valueTextCount)
116                 setBothText(shortHeadingText, valueText.at(i));
117             else
118                 setText(noHeadingText + valueText.last()); //This just sets the color of the text
119         }
120     }
changeLabel(const QColor & _color)121     void changeLabel(const QColor &_color) {
122         color = _color;
123 
124         if ( QApplication::layoutDirection() == Qt::RightToLeft )
125             longHeadingText = QLatin1String(": ") + labelName + QLatin1String(" <font color=\"") + color.name() + QLatin1String("\">") + indicatorSymbol + QLatin1String("</font>");
126         else
127             longHeadingText = QLatin1String("<qt><font color=\"") + color.name() + QLatin1String("\">") + indicatorSymbol + QLatin1String("</font> ") + labelName + QLatin1String(" :");
128         shortHeadingText = QLatin1String("<qt><font color=\"") + color.name() + QLatin1String("\">") + indicatorSymbol + QLatin1String("</font>");
129         noHeadingText = QLatin1String("<qt><font color=\"") + color.name() + QLatin1String("\">");
130 
131         textMargin = fontMetrics().boundingRect(QLatin1Char('x')).width() + margin()*2 + frameWidth()*2;
132         longHeadingWidth = fontMetrics().boundingRect(labelName + QLatin1String(" :") + indicatorSymbol + QLatin1String(" x")).width() + textMargin;
133         shortHeadingWidth = fontMetrics().boundingRect(indicatorSymbol).width() + textMargin;
134         setMinimumWidth(shortHeadingWidth);
135         update();
136     }
137   private:
setBothText(const QString & heading,const QString & value)138     void setBothText(const QString &heading, const QString & value) {
139         if(QApplication::layoutDirection() == Qt::LeftToRight)
140             setText(heading + QLatin1Char(' ') + value);
141         else
142             setText(QStringLiteral("<qt>") + value + QLatin1Char(' ') + heading);
143     }
144     int textMargin;
145     QString longHeadingText;
146     QString shortHeadingText;
147     QString noHeadingText;
148     int longHeadingWidth;
149     int shortHeadingWidth;
150     QList<QString> valueText;
151     QString labelName;
152     QColor color;
153     static QChar indicatorSymbol;
154 };
155 
156 QChar FancyPlotterLabel::indicatorSymbol;
157 
FancyPlotter(QWidget * parent,const QString & title,SharedSettings * workSheetSettings)158 FancyPlotter::FancyPlotter( QWidget* parent,
159                             const QString &title,
160                             SharedSettings *workSheetSettings)
161   : KSGRD::SensorDisplay( parent, title, workSheetSettings )
162 {
163     mBeams = 0;
164     mSettingsDialog = 0;
165     mSensorReportedMax = mSensorReportedMin = 0;
166     mSensorManualMax = mSensorManualMin = 0;
167     mUseManualRange = false;
168     mNumAnswers = 0;
169     mLabelsWidget = nullptr;
170 
171     mIndicatorSymbol = circleCharacter(QFontMetrics(QToolTip::font()));
172 
173     QBoxLayout *layout = new QVBoxLayout(this);
174     layout->setSpacing(0);
175     mPlotter = new KSignalPlotter( this );
176     int axisTextWidth = fontMetrics().boundingRect(i18nc("Largest axis title", "99999 XXXX")).width();
177     mPlotter->setMaxAxisTextWidth( axisTextWidth );
178     mPlotter->setUseAutoRange( true );
179     mHeading = new QLabel(translatedTitle(), this);
180     QFont headingFont;
181     headingFont.setWeight(QFont::Bold);
182     headingFont.setPointSizeF(headingFont.pointSizeF() * 1.19);
183     mHeading->setFont(headingFont);
184     layout->addWidget(mHeading);
185     layout->addWidget(mPlotter);
186 
187     /* Create a set of labels underneath the graph. */
188     mLabelsWidget = new QWidget;
189     layout->addWidget(mLabelsWidget);
190     QBoxLayout *outerLabelLayout = new QHBoxLayout(mLabelsWidget);
191     outerLabelLayout->setSpacing(0);
192     outerLabelLayout->setContentsMargins(0,0,0,0);
193 
194     /* create a spacer to fill up the space up to the start of the graph */
195     outerLabelLayout->addItem(new QSpacerItem(axisTextWidth + 10, 0, QSizePolicy::Preferred));
196 
197     mLabelLayout = new QHBoxLayout;
198     outerLabelLayout->addLayout(mLabelLayout);
199     mLabelLayout->setContentsMargins(0,0,0,0);
200     QFont font;
201     font.setPointSize( KSGRD::Style->fontSize() );
202     mPlotter->setFont( font );
203 
204     /* All RMB clicks to the mPlotter widget will be handled by
205      * SensorDisplay::eventFilter. */
206     mPlotter->installEventFilter( this );
207 
208     setPlotterWidget( mPlotter );
209     connect(mPlotter, &KSignalPlotter::axisScaleChanged, this, &FancyPlotter::plotterAxisScaleChanged);
210     QDomElement emptyElement;
211     restoreSettings(emptyElement);
212 }
213 
~FancyPlotter()214 FancyPlotter::~FancyPlotter()
215 {
216 }
217 
setTitle(const QString & title)218 void FancyPlotter::setTitle( const QString &title ) { //virtual
219     KSGRD::SensorDisplay::setTitle( title );
220     if(mHeading)
221         mHeading->setText(translatedTitle());
222 }
223 
eventFilter(QObject * object,QEvent * event)224 bool FancyPlotter::eventFilter( QObject* object, QEvent* event ) {	//virtual
225     if(event->type() == QEvent::ToolTip)
226         setTooltip();
227     return SensorDisplay::eventFilter(object, event);
228 }
229 
configureSettings()230 void FancyPlotter::configureSettings()
231 {
232     if(mSettingsDialog)
233         return;
234     mSettingsDialog = new FancyPlotterSettings( this, mSharedSettings->locked );
235 
236     mSettingsDialog->setTitle( title() );
237     mSettingsDialog->setUseManualRange( mUseManualRange );
238     if(mUseManualRange) {
239         mSettingsDialog->setMinValue( mSensorManualMin );
240         mSettingsDialog->setMaxValue( mSensorManualMax );
241     } else {
242         mSettingsDialog->setMinValue( mSensorReportedMin );
243         mSettingsDialog->setMaxValue( mSensorReportedMax );
244     }
245 
246     mSettingsDialog->setHorizontalScale( mPlotter->horizontalScale() );
247 
248     mSettingsDialog->setShowVerticalLines( mPlotter->showVerticalLines() );
249     mSettingsDialog->setVerticalLinesDistance( mPlotter->verticalLinesDistance() );
250     mSettingsDialog->setVerticalLinesScroll( mPlotter->verticalLinesScroll() );
251 
252     mSettingsDialog->setShowHorizontalLines( mPlotter->showHorizontalLines() );
253 
254     mSettingsDialog->setShowAxis( mPlotter->showAxis() );
255 
256     mSettingsDialog->setFontSize( mPlotter->font().pointSize()  );
257 
258     mSettingsDialog->setRangeUnits( mUnit );
259     mSettingsDialog->setRangeUnits( mUnit );
260 
261     mSettingsDialog->setStackBeams( mPlotter->stackGraph() );
262 
263     bool hasIntegerRange = true;
264     SensorModelEntry::List list;
265     for ( int i = 0; i < (int)mBeams; ++i ) {
266         FPSensorProperties *sensor = nullptr;
267         //find the first sensor for this beam, since one beam can have many sensors
268         for ( int j = 0; j < sensors().count(); ++j ) {
269             FPSensorProperties *sensor2 = static_cast<FPSensorProperties *>(sensors().at(j));
270             if(sensor2->beamId == i)
271                 sensor = sensor2;
272         }
273         if(!sensor)
274             return;
275         SensorModelEntry entry;
276         entry.setId( i );
277         entry.setHostName( sensor->hostName() );
278         entry.setSensorName( sensor->regExpName().isEmpty()?sensor->name():sensor->regExpName() );
279         entry.setUnit( sensor->unit() );
280         entry.setStatus( sensor->isOk() ? i18n( "OK" ) : i18n( "Error" ) );
281         entry.setColor( mPlotter->beamColor( i ) );
282         if(!sensor->isInteger)
283             hasIntegerRange = false;
284         list.append( entry );
285     }
286     mSettingsDialog->setSensors( list );
287     mSettingsDialog->setHasIntegerRange( hasIntegerRange );
288 
289     connect(mSettingsDialog, &FancyPlotterSettings::applyClicked, this, &FancyPlotter::applySettings);
290     connect(mSettingsDialog, &FancyPlotterSettings::okClicked, this, &FancyPlotter::applySettings);
291     connect(mSettingsDialog, &FancyPlotterSettings::finished, this, &FancyPlotter::settingsFinished);
292 
293     mSettingsDialog->open();	// open() opens the dialog modally (ie. blocks the parent window)
294 }
295 
settingsFinished()296 void FancyPlotter::settingsFinished()
297 {
298     if(mSettingsDialog->result() == QDialog::Accepted) {
299         // Apply and OK also connect to applySettings, but it's
300         // racy which slot is called first when OK is clicked:
301         // so do it here, too, and worst case we'll apply twice while OKing.
302         applySettings();
303     }
304     mSettingsDialog->hide();
305     mSettingsDialog->deleteLater();
306     mSettingsDialog = nullptr;
307 }
308 
applySettings()309 void FancyPlotter::applySettings() {
310     if (!mSettingsDialog) {
311         return;
312     }
313     setTitle( mSettingsDialog->title() );
314 
315     mUseManualRange = mSettingsDialog->useManualRange();
316     if(mUseManualRange) {
317         mSensorManualMin = mSettingsDialog->minValue();
318         mSensorManualMax = mSettingsDialog->maxValue();
319         mPlotter->changeRange( mSettingsDialog->minValue(), mSettingsDialog->maxValue() );
320     }
321     else {
322         mPlotter->changeRange( mSensorReportedMin, mSensorReportedMax );
323     }
324 
325     if ( mPlotter->horizontalScale() != mSettingsDialog->horizontalScale() ) {
326         mPlotter->setHorizontalScale( mSettingsDialog->horizontalScale() );
327     }
328 
329     mPlotter->setShowVerticalLines( mSettingsDialog->showVerticalLines() );
330     mPlotter->setVerticalLinesDistance( mSettingsDialog->verticalLinesDistance() );
331     mPlotter->setVerticalLinesScroll( mSettingsDialog->verticalLinesScroll() );
332 
333     mPlotter->setShowHorizontalLines( mSettingsDialog->showHorizontalLines() );
334 
335     mPlotter->setShowAxis( mSettingsDialog->showAxis() );
336     mPlotter->setStackGraph( mSettingsDialog->stackBeams() );
337 
338     QFont font;
339     font.setPointSize( mSettingsDialog->fontSize() );
340     mPlotter->setFont( font );
341 
342     QList<int> deletedBeams = mSettingsDialog->deleted();
343     for ( int i =0; i < deletedBeams.count(); ++i) {
344         removeBeam(deletedBeams[i]);
345     }
346     mSettingsDialog->clearDeleted(); //We have deleted them, so clear the deleted
347 
348     reorderBeams(mSettingsDialog->order());
349     mSettingsDialog->resetOrder(); //We have now reordered the sensors, so reset the order
350 
351     SensorModelEntry::List list = mSettingsDialog->sensors();
352 
353     for( int i = 0; i < list.count(); i++)
354         setBeamColor(i, list[i].color());
355     mPlotter->update();
356 }
357 
resizeEvent(QResizeEvent *)358 void FancyPlotter::resizeEvent( QResizeEvent* )
359 {
360     bool showHeading = true;
361     bool showLabels = true;
362 
363     if( height() < mLabelsWidget->sizeHint().height() + mHeading->sizeHint().height() + mPlotter->minimumHeight() )
364         showHeading = false;
365     if( height() < mLabelsWidget->sizeHint().height() + mPlotter->minimumHeight() )
366         showLabels = false;
367     mHeading->setVisible(showHeading);
368     mLabelsWidget->setVisible(showLabels);
369 
370 }
371 
reorderBeams(const QList<int> & orderOfBeams)372 void FancyPlotter::reorderBeams(const QList<int> & orderOfBeams)
373 {
374     //Q_ASSERT(orderOfBeams.size() == mLabelLayout.size());  Commented out because it cause compile problems in some cases??
375     //Reorder the graph
376     mPlotter->reorderBeams(orderOfBeams);
377     //Reorder the labels underneath the graph
378     QList<QLayoutItem *> labelsInOldOrder;
379     while(!mLabelLayout->isEmpty())
380         labelsInOldOrder.append(mLabelLayout->takeAt(0));
381 
382     for(int newIndex = 0; newIndex < orderOfBeams.count(); newIndex++) {
383         int oldIndex = orderOfBeams.at(newIndex);
384         mLabelLayout->addItem(labelsInOldOrder.at(oldIndex));
385     }
386 
387     for ( int i = 0; i < sensors().count(); ++i ) {
388         FPSensorProperties *sensor = static_cast<FPSensorProperties *>(sensors().at(i));
389         for(int newIndex = 0; newIndex < orderOfBeams.count(); newIndex++) {
390             int oldIndex = orderOfBeams.at(newIndex);
391             if(oldIndex == sensor->beamId) {
392                 sensor->beamId = newIndex;
393                 break;
394             }
395         }
396     }
397 }
applyStyle()398 void FancyPlotter::applyStyle()
399 {
400     QFont font = mPlotter->font();
401     font.setPointSize(KSGRD::Style->fontSize() );
402     mPlotter->setFont( font );
403     for ( int i = 0; i < mPlotter->numBeams() &&
404             i < KSGRD::Style->numSensorColors(); ++i ) {
405         setBeamColor(i, KSGRD::Style->sensorColor(i));
406     }
407 
408     mPlotter->update();
409 }
setBeamColor(int i,const QColor & color)410 void FancyPlotter::setBeamColor(int i, const QColor &color)
411 {
412         mPlotter->setBeamColor( i, color );
413         static_cast<FancyPlotterLabel *>((static_cast<QWidgetItem *>(mLabelLayout->itemAt(i)))->widget())->changeLabel(color);
414 }
addSensor(const QString & hostName,const QString & name,const QString & type,const QString & title)415 bool FancyPlotter::addSensor( const QString &hostName, const QString &name,
416         const QString &type, const QString &title )
417 {
418     return addSensor( hostName, name, type, title,
419             KSGRD::Style->sensorColor( mBeams ), QString(), mBeams );
420 }
421 
addSensor(const QString & hostName,const QString & name,const QString & type,const QString & title,const QColor & color,const QString & regexpName,int beamId,const QString & summationName)422 bool FancyPlotter::addSensor( const QString &hostName, const QString &name,
423         const QString &type, const QString &title,
424         const QColor &color, const QString &regexpName,
425         int beamId, const QString & summationName)
426 {
427     if ( type != QLatin1String("integer") && type != QLatin1String("float") )
428         return false;
429 
430 
431     registerSensor( new FPSensorProperties( hostName, name, type, title, color, regexpName, beamId, summationName ) );
432 
433     /* To differentiate between answers from value requests and info
434      * requests we add 100 to the beam index for info requests. */
435     sendRequest( hostName, name + QLatin1Char('?'), sensors().size() - 1 + 100 );
436 
437     if((int)mBeams == beamId) {
438         mPlotter->addBeam( color );
439         /* Add a label for this beam */
440         FancyPlotterLabel *label = new FancyPlotterLabel(this);
441         mLabelLayout->addWidget(label);
442         if(!summationName.isEmpty()) {
443             label->setLabel(summationName, mPlotter->beamColor(mBeams));
444         }
445         ++mBeams;
446     }
447 
448     return true;
449 }
450 
removeBeam(uint beamId)451 bool FancyPlotter::removeBeam( uint beamId )
452 {
453     if ( beamId >= mBeams ) {
454         qDebug() << "FancyPlotter::removeBeam: beamId out of range ("
455             << beamId << ")";
456         return false;
457     }
458 
459     mPlotter->removeBeam( beamId );
460     --mBeams;
461     QWidget *label = (static_cast<QWidgetItem *>(mLabelLayout->takeAt( beamId )))->widget();
462     mLabelLayout->removeWidget(label);
463     delete label;
464 
465     mSensorReportedMax = 0;
466     mSensorReportedMin = 0;
467     for ( int i = sensors().count()-1; i >= 0; --i ) {
468         FPSensorProperties *sensor = static_cast<FPSensorProperties *>(sensors().at(i));
469 
470         if(sensor->beamId == (int)beamId)
471             removeSensor( i );
472         else {
473             if(sensor->beamId > (int)beamId)
474                 sensor->beamId--;  //sensor pointer is no longer valid after removing the sensor
475             mSensorReportedMax = qMax(mSensorReportedMax, sensor->maxValue);
476             mSensorReportedMin = qMin(mSensorReportedMin, sensor->minValue);
477         }
478     }
479     //change the plotter's range to the new maximum
480     if ( !mUseManualRange )
481         mPlotter->changeRange( mSensorReportedMin, mSensorReportedMax );
482     else
483         mPlotter->changeRange( mSensorManualMin, mSensorManualMax );
484 
485     //loop through the new sensors to find the new unit
486     for ( int i = 0; i < sensors().count(); i++ ) {
487         FPSensorProperties *sensor = static_cast<FPSensorProperties *>(sensors().at(i));
488         if(i == 0)
489             mUnit = sensor->unit();
490         else if(mUnit != sensor->unit()) {
491             mUnit = QLatin1String("");
492             break;
493         }
494     }
495     //adjust the scale to take into account the removed sensor
496     plotterAxisScaleChanged();
497 
498     return true;
499 }
500 
isPercentage(FPSensorProperties * sensor)501 static inline bool isPercentage(FPSensorProperties* sensor)
502 {
503     return sensor->unit() == QLatin1String("%");
504 }
505 
506 // The "useful precision" of a value; integers don't need
507 // any decimal point, percentages can do with 0.1% or 99.9%,
508 // and the rest should be fine with 2 digits.
usefulPrecision(FPSensorProperties * sensor)509 static inline int usefulPrecision(FPSensorProperties* sensor)
510 {
511     if (sensor->isInteger)
512         return 0;
513     if (isPercentage(sensor))
514         return 1;
515     return 2;
516 }
517 
setTooltip()518 void FancyPlotter::setTooltip()
519 {
520     QString tooltip = QStringLiteral("<qt><p style='white-space:pre'>");
521 
522     QString description;
523     QString lastValue;
524     bool neednewline = false;
525     bool showingSummationGroup = false;
526     int beamId = -1;
527     //Note that the number of beams can be less than the number of sensors, since some sensors
528     //get added together for a beam.
529     //For the tooltip, we show all the sensors
530     for ( int i = 0; i < sensors().count(); ++i ) {
531         FPSensorProperties *sensor = static_cast<FPSensorProperties *>(sensors().at(i));
532         description = sensor->description();
533         if(description.isEmpty())
534             description = sensor->name();
535 
536         if(sensor->isOk()) {
537             static_assert(std::is_same<double, decltype(sensor->lastValue)>::value, "Sensor values should be double");
538             lastValue = QLocale().toString( sensor->lastValue, 'f', usefulPrecision(sensor) );
539             if (isPercentage(sensor))
540                 lastValue = i18nc("units", "%1%", lastValue);
541             else if( !sensor->unit().isEmpty() )
542                 lastValue = i18nc("units", QString(QLatin1String("%1 ") + sensor->unit()).toUtf8().constData(), lastValue);
543         } else {
544             lastValue = i18n("Error");
545         }
546         if (beamId != sensor->beamId) {
547             if (!sensor->summationName.isEmpty()) {
548                 tooltip += i18nc("%1 is what is being shown statistics for, like 'Memory', 'Swap', etc.", "<p><b>%1:</b><br>", i18n(sensor->summationName.toUtf8().constData()));
549                 showingSummationGroup = true;
550                 neednewline = false;
551             } else if (showingSummationGroup) {
552                 //When a summation group has finished, separate the next sensor with a newline
553                 showingSummationGroup = false;
554                 tooltip += QLatin1String("<br>");
555             }
556 
557         }
558         beamId = sensor->beamId;
559 
560         if(sensor->isLocalhost()) {
561             tooltip += QStringLiteral( "%1%2 %3 (%4)" ).arg( neednewline  ? QStringLiteral("<br>") : QString())
562                 .arg(QLatin1String("<font color=\"") + mPlotter->beamColor( beamId ).name() + QLatin1String("\">") + mIndicatorSymbol+ QLatin1String("</font>"))
563                 .arg( i18n(description.toUtf8().constData()) )
564                 .arg( lastValue );
565 
566         } else {
567             tooltip += QStringLiteral( "%1%2 %3:%4 (%5)" ).arg( neednewline ? QStringLiteral("<br>") : QString() )
568                 .arg(QLatin1String("<font color=\"") + mPlotter->beamColor( beamId ).name() + QLatin1String("\">") + mIndicatorSymbol+ QLatin1String("</font>"))
569                 .arg( sensor->hostName() )
570                 .arg( i18n(description.toUtf8().constData()) )
571                 .arg( lastValue );
572         }
573         neednewline = true;
574     }
575     //  tooltip += "</td></tr></table>";
576     mPlotter->setToolTip( tooltip );
577 }
578 
sendDataToPlotter()579 void FancyPlotter::sendDataToPlotter( )
580 {
581     if(!mSampleBuf.isEmpty() && mBeams != 0) {
582         if((uint)mSampleBuf.count() > mBeams) {
583             mSampleBuf.clear();
584             return; //ignore invalid results - can happen if a sensor is deleted
585         }
586         while((uint)mSampleBuf.count() < mBeams)
587             mSampleBuf.append(mPlotter->lastValue(mSampleBuf.count())); //we might have sensors missing so set their values to the previously known value
588         mPlotter->addSample( mSampleBuf );
589         if(isVisible()) {
590             if(QToolTip::isVisible() && (qApp->topLevelAt(QCursor::pos()) == window()) && mPlotter->geometry().contains(mPlotter->mapFromGlobal( QCursor::pos() ))) {
591                 setTooltip();
592                 QToolTip::showText(QCursor::pos(), mPlotter->toolTip(), mPlotter);
593             }
594             QString lastValue;
595             int beamId = -1;
596             for ( int i = 0; i < sensors().size(); ++i ) {
597                 FPSensorProperties *sensor = static_cast<FPSensorProperties *>(sensors().at(i));
598                 if(sensor->beamId == beamId)
599                     continue;
600                 beamId = sensor->beamId;
601                 if(sensor->isOk() && mPlotter->numBeams() > beamId) {
602 
603                     int precision;
604                     if(sensor->unit() == mUnit) {
605                         precision = (sensor->isInteger && mPlotter->scaleDownBy() == 1)?0:-1;
606                         lastValue = mPlotter->lastValueAsString(beamId, precision);
607                     } else {
608                         static_assert(std::is_same<double, decltype(mPlotter->lastValue(beamId))>::value, "Beam values should be double");
609                         precision = usefulPrecision(sensor);
610                         lastValue = QLocale().toString( mPlotter->lastValue(beamId), 'f', precision );
611                         if (isPercentage(sensor))
612                             lastValue = i18nc("units", "%1%", lastValue);
613                         else if( !sensor->unit().isEmpty() )  {
614                             lastValue = i18nc("units", QString(QLatin1String("%1 ") + sensor->unit()).toUtf8().constData(), lastValue);
615                         }
616                     }
617 
618                     if(sensor->maxValue != 0 && !isPercentage(sensor)) {
619                         //Use a multi length string incase we do not have enough room
620                         lastValue = i18n("%1 of %2", lastValue, mPlotter->valueAsString(sensor->maxValue, precision) ) + "\xc2\x9c" + lastValue;
621                     }
622                 } else {
623                     lastValue = i18n("Error");
624                 }
625                 static_cast<FancyPlotterLabel *>((static_cast<QWidgetItem *>(mLabelLayout->itemAt(beamId)))->widget())->setValueText(lastValue);
626             }
627         }
628 
629     }
630     mSampleBuf.clear();
631 }
timerTick()632 void FancyPlotter::timerTick() //virtual
633 {
634     if(mNumAnswers < sensors().count())
635         sendDataToPlotter(); //we haven't received enough answers yet, but plot what we do have
636     mNumAnswers = 0;
637     SensorDisplay::timerTick();
638 }
plotterAxisScaleChanged()639 void FancyPlotter::plotterAxisScaleChanged()
640 {
641     //Prevent this being called recursively
642     disconnect(mPlotter, &KSignalPlotter::axisScaleChanged, this, &FancyPlotter::plotterAxisScaleChanged);
643     KLocalizedString unit;
644     double value = mPlotter->currentMaximumRangeValue();
645     if(mUnit  == QLatin1String("KiB")) {
646         if(value >= 1024*1024*1024*0.7) {  //If it's over 0.7TiB, then set the scale to terabytes
647             mPlotter->setScaleDownBy(1024*1024*1024);
648             unit = ki18nc("units", "%1 TiB"); // the unit - terabytes
649         } else if(value >= 1024*1024*0.7) {  //If it's over 0.7GiB, then set the scale to gigabytes
650             mPlotter->setScaleDownBy(1024*1024);
651             unit = ki18nc("units", "%1 GiB"); // the unit - gigabytes
652         } else if(value > 1024) {
653             mPlotter->setScaleDownBy(1024);
654             unit = ki18nc("units", "%1 MiB"); // the unit - megabytes
655         } else {
656             mPlotter->setScaleDownBy(1);
657             unit = ki18nc("units", "%1 KiB"); // the unit - kilobytes
658         }
659     } else if(mUnit == QLatin1String("KiB/s")) {
660         if(value >= 1024*1024*1024*0.7) {  //If it's over 0.7TiB, then set the scale to terabytes
661             mPlotter->setScaleDownBy(1024*1024*1024);
662             unit = ki18nc("units", "%1 TiB/s"); // the unit - terabytes per second
663         } else if(value >= 1024*1024*0.7) {  //If it's over 0.7GiB, then set the scale to gigabytes
664             mPlotter->setScaleDownBy(1024*1024);
665             unit = ki18nc("units", "%1 GiB/s"); // the unit - gigabytes per second
666         } else if(value > 1024) {
667             mPlotter->setScaleDownBy(1024);
668             unit = ki18nc("units", "%1 MiB/s"); // the unit - megabytes per second
669         } else {
670             mPlotter->setScaleDownBy(1);
671             unit = ki18nc("units", "%1 KiB/s"); // the unit - kilobytes per second
672         }
673     } else if(mUnit == QLatin1String("%")) {
674         mPlotter->setScaleDownBy(1);
675         unit = ki18nc("units", "%1%"); //the unit - percentage
676     } else if(mUnit.isEmpty()) {
677         unit = ki18nc("unitless - just a number", "%1");
678     } else {
679 #if 0  // the strings are here purely for translation
680         NOOP_I18NC("units", "%1 1/s"); // the unit - 1 per second
681         NOOP_I18NC("units", "%1 s");  // the unit - seconds
682         NOOP_I18NC("units", "%1 MHz");  // the unit - frequency megahertz
683 #endif
684         mPlotter->setScaleDownBy(1);
685         //translate any others
686         unit = ki18nc("units", QString(QLatin1String("%1 ") + mUnit).toUtf8().constData());
687     }
688     mPlotter->setUnit(unit);
689     //reconnect
690     connect(mPlotter, &KSignalPlotter::axisScaleChanged, this, &FancyPlotter::plotterAxisScaleChanged);
691 }
answerReceived(int id,const QList<QByteArray> & answerlist)692 void FancyPlotter::answerReceived( int id, const QList<QByteArray> &answerlist )
693 {
694     QByteArray answer;
695 
696     if(!answerlist.isEmpty()) answer = answerlist[0];
697     if ( (uint)id < 100 ) {
698         //Make sure that we put the answer in the correct place.  Its index in the list should be equal to the sensor index.  This in turn will contain the beamId
699         if(id >= sensors().count())
700             return;  //just ignore if we get a result for an invalid sensor
701         FPSensorProperties *sensor = static_cast<FPSensorProperties *>(sensors().at(id));
702         int beamId = sensor->beamId;
703         double value = answer.toDouble();
704         while(beamId > mSampleBuf.count())
705             mSampleBuf.append(0); //we might have sensors missing so set their values to zero
706 
707         if(beamId == mSampleBuf.count()) {
708             mSampleBuf.append( value );
709         } else {
710             mSampleBuf[beamId] += value; //If we get two answers for the same beamid, we should add them together.  That's how the summation works
711         }
712         sensor->lastValue = value;
713         /* We received something, so the sensor is probably ok. */
714         sensorError( id, false );
715 
716         if(++mNumAnswers == sensors().count())
717             sendDataToPlotter(); //we have received all the answers so start plotting
718     } else if ( id >= 100 && id < 200 ) {
719         if( (id - 100) >= sensors().count())
720             return;  //just ignore if we get a result for an invalid sensor
721         KSGRD::SensorFloatInfo info( answer );
722         QString unit = info.unit();
723         if(unit.toUpper() == QLatin1String("KB") || unit.toUpper() == QLatin1String("KIB"))
724             unit = QStringLiteral("KiB");
725         if(unit.toUpper() == QLatin1String("KB/S") || unit.toUpper() == QLatin1String("KIB/S"))
726             unit = QStringLiteral("KiB/s");
727 
728         if(id == 100) //if we are the first sensor, just use that sensors units as the global unit
729             mUnit = unit;
730         else if(unit != mUnit)
731             mUnit = QLatin1String(""); //if the units don't match, then set the units on the scale to empty, to avoid any confusion
732 
733         mSensorReportedMax = qMax(mSensorReportedMax, info.max());
734         mSensorReportedMin = qMin(mSensorReportedMin, info.min());
735 
736         if ( !mUseManualRange )
737             mPlotter->changeRange( mSensorReportedMin, mSensorReportedMax );
738         plotterAxisScaleChanged();
739 
740         FPSensorProperties *sensor = static_cast<FPSensorProperties *>(sensors().at(id - 100));
741         sensor->maxValue = info.max();
742         sensor->minValue = info.min();
743         sensor->setUnit( unit );
744         sensor->setDescription( info.name() );
745 
746         QString summationName = sensor->summationName;
747         int beamId = sensor->beamId;
748 
749         Q_ASSERT(beamId < mPlotter->numBeams());
750         Q_ASSERT(beamId < mLabelLayout->count());
751 
752         if(summationName.isEmpty())
753             static_cast<FancyPlotterLabel *>((static_cast<QWidgetItem *>(mLabelLayout->itemAt(beamId)))->widget())->setLabel(info.name(), mPlotter->beamColor(beamId));
754 
755     } else if( id == 200) {
756         /* FIXME This doesn't check the host!  */
757         if(!mSensorsToAdd.isEmpty())  {
758             foreach(SensorToAdd *sensor, mSensorsToAdd) {
759                 int beamId = mBeams;  //Assign the next sensor to the next available beamId
760                 for ( int i = 0; i < answerlist.count(); ++i ) {
761                     if ( answerlist[ i ].isEmpty() )
762                         continue;
763                     QString sensorName = QString::fromUtf8(answerlist[ i ].split('\t')[0]);
764                     if(sensor->name.exactMatch(sensorName)) {
765                         if(sensor->summationName.isEmpty())
766                             beamId = mBeams; //If summationName is not empty then reuse the previous beamId.  In this way we can have multiple sensors with the same beamId, which can then be summed together
767                         QColor color;
768                         if(!sensor->colors.isEmpty() )
769                             color = sensor->colors.takeFirst();
770                         else if(KSGRD::Style->numSensorColors() != 0)
771                             color = KSGRD::Style->sensorColor( beamId % KSGRD::Style->numSensorColors());
772                         addSensor( sensor->hostname, sensorName,
773                                 (sensor->type.isEmpty()) ? QStringLiteral("float") : sensor->type
774                                 , QLatin1String(""), color, sensor->name.pattern(), beamId, sensor->summationName);
775                     }
776                 }
777             }
778             qDeleteAll(mSensorsToAdd);
779             mSensorsToAdd.clear();
780         }
781     }
782 }
783 
restoreSettings(QDomElement & element)784 bool FancyPlotter::restoreSettings( QDomElement &element )
785 {
786     mUseManualRange = element.attribute( QStringLiteral("manualRange"), QStringLiteral("0") ).toInt();
787 
788     if(mUseManualRange) {
789         mSensorManualMax = element.attribute( QStringLiteral("max") ).toDouble();
790         mSensorManualMin = element.attribute( QStringLiteral("min") ).toDouble();
791         mPlotter->changeRange( mSensorManualMin, mSensorManualMax );
792     } else {
793         mPlotter->changeRange( mSensorReportedMin, mSensorReportedMax );
794     }
795 
796     mPlotter->setUseAutoRange(element.attribute( QStringLiteral("autoRange"), QStringLiteral("1") ).toInt());
797 
798     // Do not restore the color settings from a previous version
799     int version = element.attribute(QStringLiteral("version"), QStringLiteral("0")).toInt();
800 
801     mPlotter->setShowVerticalLines( element.attribute( QStringLiteral("vLines"), QStringLiteral("0") ).toUInt() );
802     mPlotter->setVerticalLinesDistance( element.attribute( QStringLiteral("vDistance"), QStringLiteral("30") ).toUInt() );
803     mPlotter->setVerticalLinesScroll( element.attribute( QStringLiteral("vScroll"), QStringLiteral("0") ).toUInt() );
804     mPlotter->setHorizontalScale( element.attribute( QStringLiteral("hScale"), QStringLiteral("6") ).toUInt() );
805 
806     mPlotter->setShowHorizontalLines( element.attribute( QStringLiteral("hLines"), QStringLiteral("1") ).toUInt() );
807     mPlotter->setStackGraph( element.attribute(QStringLiteral("stacked"), QStringLiteral("0")).toInt());
808 
809     QString filename = element.attribute( QStringLiteral("svgBackground"));
810     if (!filename.isEmpty() && filename[0] == QLatin1Char('/')) {
811         filename = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("ksysguard/") + filename);
812     }
813     mPlotter->setSvgBackground( filename );
814     if(version >= 1) {
815         mPlotter->setShowAxis( element.attribute( QStringLiteral("labels"), QStringLiteral("1") ).toUInt() );
816         uint fontsize = element.attribute( QStringLiteral("fontSize"), QStringLiteral("0")).toUInt();
817         if(fontsize == 0) fontsize =  KSGRD::Style->fontSize();
818         QFont font;
819         font.setPointSize( fontsize );
820         mPlotter->setFont( font );
821     }
822     QDomNodeList dnList = element.elementsByTagName( QStringLiteral("beam") );
823     for ( int i = 0; i < dnList.count(); ++i ) {
824         QDomElement el = dnList.item( i ).toElement();
825         if(el.hasAttribute(QStringLiteral("regexpSensorName"))) {
826             SensorToAdd *sensor = new SensorToAdd();
827             sensor->name = QRegExp(el.attribute(QStringLiteral("regexpSensorName")));
828             sensor->hostname = el.attribute( QStringLiteral("hostName") );
829             sensor->type = el.attribute( QStringLiteral("sensorType") );
830             sensor->summationName = el.attribute(QStringLiteral("summationName"));
831             QStringList colors = el.attribute(QStringLiteral("color")).split(QLatin1Char(','));
832             bool ok;
833             foreach(const QString &color, colors) {
834                 int c = color.toUInt( &ok, 0 );
835                 if(ok) {
836                     QColor col( (c & 0xff0000) >> 16, (c & 0xff00) >> 8, (c & 0xff), (c & 0xff000000) >> 24);
837                     if(col.isValid()) {
838                         if(col.alpha() == 0) col.setAlpha(255);
839                         sensor->colors << col;
840                     }
841                     else
842                         sensor->colors << KSGRD::Style->sensorColor( i );
843                 }
844                 else
845                     sensor->colors << KSGRD::Style->sensorColor( i );
846             }
847             mSensorsToAdd.append(sensor);
848             sendRequest( sensor->hostname, QStringLiteral("monitors"), 200 );
849         } else
850             addSensor( el.attribute( QStringLiteral("hostName") ), el.attribute( QStringLiteral("sensorName") ),
851                     ( el.attribute( QStringLiteral("sensorType") ).isEmpty() ? QStringLiteral("float") :
852                       el.attribute( QStringLiteral("sensorType") ) ), QLatin1String(""), restoreColor( el, QStringLiteral("color"),
853                       KSGRD::Style->sensorColor( i ) ), QString(), mBeams, el.attribute(QStringLiteral("summationName")) );
854     }
855 
856     SensorDisplay::restoreSettings( element );
857 
858     return true;
859 }
860 
saveSettings(QDomDocument & doc,QDomElement & element)861 bool FancyPlotter::saveSettings( QDomDocument &doc, QDomElement &element)
862 {
863     element.setAttribute( QStringLiteral("autoRange"), mPlotter->useAutoRange() );
864 
865     element.setAttribute( QStringLiteral("manualRange"), mUseManualRange );
866     if(mUseManualRange) {
867         element.setAttribute( QStringLiteral("min"), mSensorManualMin );
868         element.setAttribute( QStringLiteral("max"), mSensorManualMax );
869     }
870 
871 
872     element.setAttribute( QStringLiteral("vLines"), mPlotter->showVerticalLines() );
873     element.setAttribute( QStringLiteral("vDistance"), mPlotter->verticalLinesDistance() );
874     element.setAttribute( QStringLiteral("vScroll"), mPlotter->verticalLinesScroll() );
875 
876     element.setAttribute( QStringLiteral("hScale"), mPlotter->horizontalScale() );
877 
878     element.setAttribute( QStringLiteral("hLines"), mPlotter->showHorizontalLines() );
879 
880     element.setAttribute( QStringLiteral("svgBackground"), mPlotter->svgBackground() );
881     element.setAttribute( QStringLiteral("stacked"), mPlotter->stackGraph() );
882 
883     element.setAttribute( QStringLiteral("version"), 1 );
884     element.setAttribute( QStringLiteral("labels"), mPlotter->showAxis() );
885     element.setAttribute( QStringLiteral("fontSize"), mPlotter->font().pointSize() );
886 
887     QHash<QString,QDomElement> hash;
888     int beamId = -1;
889     for ( int i = 0; i < sensors().size(); ++i ) {
890         FPSensorProperties *sensor = static_cast<FPSensorProperties *>(sensors().at(i));
891         if(sensor->beamId == beamId)
892             continue;
893         beamId = sensor->beamId;
894 
895         QString regExpName = sensor->regExpName();
896         if(!regExpName.isEmpty() && hash.contains( regExpName )) {
897             QDomElement oldBeam = hash.value(regExpName);
898             saveColorAppend( oldBeam, QStringLiteral("color"), mPlotter->beamColor( beamId ) );
899         } else {
900             QDomElement beam = doc.createElement( QStringLiteral("beam") );
901             element.appendChild( beam );
902             beam.setAttribute( QStringLiteral("hostName"), sensor->hostName() );
903             if(regExpName.isEmpty())
904                 beam.setAttribute( QStringLiteral("sensorName"), sensor->name() );
905             else {
906                 beam.setAttribute( QStringLiteral("regexpSensorName"), sensor->regExpName() );
907                 hash[regExpName] = beam;
908             }
909             if(!sensor->summationName.isEmpty())
910                 beam.setAttribute( QStringLiteral("summationName"), sensor->summationName);
911             beam.setAttribute( QStringLiteral("sensorType"), sensor->type() );
912             saveColor( beam, QStringLiteral("color"), mPlotter->beamColor( beamId ) );
913         }
914     }
915     SensorDisplay::saveSettings( doc, element );
916 
917     return true;
918 }
919 
hasSettingsDialog() const920 bool FancyPlotter::hasSettingsDialog() const
921 {
922     return true;
923 }
924 
FPSensorProperties()925 FPSensorProperties::FPSensorProperties()
926 {
927 }
928 
FPSensorProperties(const QString & hostName,const QString & name,const QString & type,const QString & description,const QColor & color,const QString & regexpName,int beamId_,const QString & summationName_)929 FPSensorProperties::FPSensorProperties( const QString &hostName,
930         const QString &name,
931         const QString &type,
932         const QString &description,
933         const QColor &color,
934         const QString &regexpName,
935         int beamId_,
936         const QString &summationName_ )
937 : KSGRD::SensorProperties( hostName, name, type, description),
938     mColor( color )
939 {
940     setRegExpName(regexpName);
941     beamId = beamId_;
942     summationName = summationName_;
943     maxValue = 0;
944     minValue = 0;
945     lastValue = 0;
946     isInteger = (type == QLatin1String("integer"));
947 }
948 
~FPSensorProperties()949 FPSensorProperties::~FPSensorProperties()
950 {
951 }
952 
setColor(const QColor & color)953 void FPSensorProperties::setColor( const QColor &color )
954 {
955     mColor = color;
956 }
957 
color() const958 QColor FPSensorProperties::color() const
959 {
960     return mColor;
961 }
962 
963 
964 
965