From 55f95e594c527148ef40f64fbe2ce8ed2184c0d7 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Mon, 16 Jun 2025 14:55:25 +0100 Subject: [PATCH] Overview Metric Tile - support for metric overrides (#4649) Use italics for overrides --- src/Charts/OverviewItems.cpp | 76 ++++++++-- src/Charts/OverviewItems.h | 15 ++ src/Gui/MetricOverrideDialog.cpp | 236 +++++++++++++++++++++++++++++++ src/Gui/MetricOverrideDialog.h | 62 ++++++++ src/Metrics/SpecialFields.cpp | 2 +- src/Metrics/SpecialFields.h | 2 +- src/src.pro | 6 +- 7 files changed, 385 insertions(+), 14 deletions(-) create mode 100644 src/Gui/MetricOverrideDialog.cpp create mode 100644 src/Gui/MetricOverrideDialog.h diff --git a/src/Charts/OverviewItems.cpp b/src/Charts/OverviewItems.cpp index cc0543eaf..f8a72242d 100644 --- a/src/Charts/OverviewItems.cpp +++ b/src/Charts/OverviewItems.cpp @@ -823,10 +823,6 @@ MetricOverviewItem::MetricOverviewItem(ChartSpace *parent, QString name, QString this->type = OverviewItemType::METRIC; this->symbol = symbol; - RideMetricFactory &factory = RideMetricFactory::instance(); - this->metric = const_cast(factory.rideMetric(symbol)); - if (metric) units = metric->units(GlobalContext::context()->useMetricUnits); - // prepare the gold, silver and bronze medal gold = colouredPixmapFromPNG(":/images/medal.png", QColor(249,166,2)).scaledToWidth(ROWHEIGHT*2); silver = colouredPixmapFromPNG(":/images/medal.png", QColor(192,192,192)).scaledToWidth(ROWHEIGHT*2); @@ -838,6 +834,8 @@ MetricOverviewItem::MetricOverviewItem(ChartSpace *parent, QString name, QString configwidget = new OverviewItemConfig(this); configwidget->hide(); + + configChanged(0); } MetricOverviewItem::~MetricOverviewItem() @@ -845,6 +843,56 @@ MetricOverviewItem::~MetricOverviewItem() delete sparkline; } +void +MetricOverviewItem::configChanged(qint32) { + + RideMetricFactory& factory = RideMetricFactory::instance(); + metric = factory.rideMetric(symbol); + + // Only display the override option for metrics that exist. + setShowEdit(metric != nullptr); + units = (metric) ? metric->units(GlobalContext::context()->useMetricUnits) : ""; + + // Update the value and override status + if (rideItem) { + value = rideItem->getStringForSymbol(symbol, GlobalContext::context()->useMetricUnits); + overridden = rideItem->ride() ? (rideItem->ride()->metricOverrides.contains(symbol)) : false; + } +} + +void MetricOverviewItem::displayTileEditMenu(const QPoint& pos) { + + RideMetricFactory& factory = RideMetricFactory::instance(); + metric = factory.rideMetric(symbol); + + // Only display the Metric Override Dialog for metrics that exist. + if (metric && rideItem) { + + double editValue = rideItem->getForSymbol(symbol, GlobalContext::context()->useMetricUnits); + MetricOverrideDialog* metricOverrideDialog = new MetricOverrideDialog(parent->context, metric->internalName(), editValue, pos); + connect(metricOverrideDialog, SIGNAL(finished(int)), this, SLOT(updateTile(int))); + metricOverrideDialog->show(); // configured for delete on close + } +} + +void MetricOverviewItem::updateTile(int ret) { + + // Ensure tile contents are updated + if (rideItem && (ret == QDialog::Accepted)) { + setData(rideItem); + update(); + } +} + +void MetricOverviewItem::metadataChanged() { + + // Ensure when metadata is edited in the details tab it is updated on the tile. + if (rideItem) { + setData(rideItem); + update(); + } +} + TopNOverviewItem::TopNOverviewItem(ChartSpace *parent, QString name, QString symbol) : ChartSpaceItem(parent, name) { // metric @@ -921,7 +969,7 @@ MetaOverviewItem::configChanged(qint32) } // Update the value - if (rideItem) value = rideItem->getText(symbol, ""); + value = rideItem ? rideItem->getText(symbol, "") : ""; // sparkline if are we numeric? if (fieldtype == FIELD_INTEGER || fieldtype == FIELD_DOUBLE) { @@ -945,13 +993,13 @@ void MetaOverviewItem::updateTile(int ret) { // Ensure tile contents are updated if (ret == QDialog::Accepted) { - if (rideItem) value = rideItem->getText(symbol, ""); + value = rideItem ? rideItem->getText(symbol, "") : ""; update(); } } -void MetaOverviewItem::metadataChanged() { - +void MetaOverviewItem::metadataChanged() +{ // Ensure when metadata is edited in the details tab it // is updated on the tile. if (rideItem) { @@ -1439,8 +1487,15 @@ RPEOverviewItem::setData(RideItem *item) void MetricOverviewItem::setData(RideItem *item) { + if (rideItem) disconnect(rideItem, SIGNAL(rideMetadataChanged()), this, SLOT(metadataChanged())); + if (item) connect(item, SIGNAL(rideMetadataChanged()), this, SLOT(metadataChanged())); + + rideItem = item; + if (item == NULL || item->ride() == NULL) return; + overridden = (rideItem->ride()->metricOverrides.contains(symbol)); + // get last 30 days, if they exist QList points; @@ -3294,6 +3349,8 @@ MetricOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem if (geometry().height() > (ROWHEIGHT*6)) mid=((ROWHEIGHT*1.5f) + (ROWHEIGHT*3) / 2.0f) - (addy/2); // we align centre and mid + bool prevItalic = parent->bigfont.italic(); + parent->bigfont.setItalic(overridden); QFontMetrics fm(parent->bigfont); QRectF rect = QFontMetrics(parent->bigfont, parent->device()).boundingRect(value); @@ -3301,8 +3358,7 @@ MetricOverviewItem::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem painter->setFont(parent->bigfont); painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, mid + (fm.ascent() / 3.0f)), value); // divided by 3 to account for "gap" at top of font - painter->drawText(QPointF((geometry().width() - rect.width()) / 2.0f, - mid + (fm.ascent() / 3.0f)), value); // divided by 3 to account for "gap" at top of font + parent->bigfont.setItalic(prevItalic); // now units if (units != "" && addy > 0) { diff --git a/src/Charts/OverviewItems.h b/src/Charts/OverviewItems.h index 9d4501c6b..93ae7c963 100644 --- a/src/Charts/OverviewItems.h +++ b/src/Charts/OverviewItems.h @@ -25,6 +25,7 @@ #include "DataFilter.h" #include #include "MetadataDialog.h" +#include "MetricOverrideDialog.h" // qt charts for zone chart #include @@ -256,17 +257,22 @@ class MetricOverviewItem : public ChartSpaceItem void setData(RideItem *item); void setDateRange(DateRange); + virtual void displayTileEditMenu(const QPoint& pos) override; + QWidget *config() { return configwidget; } // create and config static ChartSpaceItem *create(ChartSpace *parent) { return new MetricOverviewItem(parent, "PowerIndex", "power_index"); } + void configChanged(qint32) override; + QString symbol; const RideMetric *metric; QString units; bool up; bool showrange = false; + bool overridden = false; QString value, upper, lower, mean; Sparkline *sparkline; @@ -276,6 +282,15 @@ class MetricOverviewItem : public ChartSpaceItem QPixmap gold, silver, bronze; // medals OverviewItemConfig *configwidget; + + protected slots: + + void updateTile(int ret); + void metadataChanged(); + + protected: + + RideItem* rideItem = nullptr; }; // top N uses this to hold details for date range diff --git a/src/Gui/MetricOverrideDialog.cpp b/src/Gui/MetricOverrideDialog.cpp new file mode 100644 index 000000000..2d9bc69ce --- /dev/null +++ b/src/Gui/MetricOverrideDialog.cpp @@ -0,0 +1,236 @@ + +/* + * Metric Override Dialog Copyright (c) 2025 Paul Johnson (paulj49457@gmail.com) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, 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 "MetricOverrideDialog.h" +#include "Context.h" +#include "RideCache.h" +#include "RideMetric.h" + +#include +#include + +MetricOverrideDialog::MetricOverrideDialog(Context* context, const QString& fieldName, const double value, QPoint pos) : + QDialog(context->mainWindow), context_(context), fieldName_(fieldName), pos_(pos) +{ + setAttribute(Qt::WA_DeleteOnClose); + + const RideMetric* metric = GlobalContext::context()->specialFields.rideMetric(fieldName_); + + // Metric Override Dialog should only be created for metrics that exist, if not exit. + if (metric == nullptr) { done(QDialog::Rejected); return; } + + setWindowTitle(tr("Metric Override Editor")); + + bool useMetricUnits = GlobalContext::context()->useMetricUnits; + QString units = metric->units(useMetricUnits); + + if (metric->isDate()) { + dlgMetricType_ = DialogMetricType::DATE; + } else if (units == "seconds" || units == tr("seconds")) { + dlgMetricType_ = DialogMetricType::SECS_TIME; + } else if (units == "minutes" || units == tr("minutes")) { + dlgMetricType_ = DialogMetricType::MINS_TIME; + } else { + dlgMetricType_ = DialogMetricType::DOUBLE; + } + + // if metric is a time or date remove any units, since the field will be a QTimeEdit or QDateEdit field + units = (dlgMetricType_ == DialogMetricType::DOUBLE) ? QString(" (%1)").arg(units) : ""; + + // we need to show what units we use for weight... + if (fieldName_ == "Weight") { units = useMetricUnits ? tr(" (kg)") : tr(" (lbs)"); } + + metricLabel_ = new QLabel(QString("%1%2").arg(GlobalContext::context()->specialFields.displayName(fieldName_)).arg(units)); + + switch (dlgMetricType_) { + case DialogMetricType::DATE: { + metricEdit_ = new QDateEdit(this); + dynamic_cast(metricEdit_)->setDisplayFormat("dd MMM yyyy"); // same format as metric tile + dynamic_cast(metricEdit_)->setDate(QDate(1900,1,1).addDays(value)); + } break; + + case DialogMetricType::SECS_TIME: { + metricEdit_ = new QTimeEdit(this); + dynamic_cast(metricEdit_)->setDisplayFormat("h:mm:ss"); // same format as metric tile + dynamic_cast(metricEdit_)->setTime(QTime(0,0,0,0).addSecs(value)); + } break; + + case DialogMetricType::MINS_TIME: { + + metricEdit_ = new QTimeEdit(this); + dynamic_cast(metricEdit_)->setDisplayFormat("mm:ss"); // same format as metric tile + dynamic_cast(metricEdit_)->setTime(QTime(0,0,0,0).addSecs(value*60)); + } break; + + case DialogMetricType::DOUBLE: { + + metricEdit_ = new QDoubleSpinBox(this); + dynamic_cast(metricEdit_)->setButtonSymbols(QAbstractSpinBox::NoButtons); + dynamic_cast(metricEdit_)->setMinimum(-9999999.99); + dynamic_cast(metricEdit_)->setMaximum(9999999.99); + dynamic_cast(metricEdit_)->setMinimumWidth(150); + dynamic_cast(metricEdit_)->setValue(value); + } break; + default: { + qDebug() << "Unsupported type in metric override dialog"; + } break; + } + + QHBoxLayout* metricDataLayout = new QHBoxLayout(); + metricDataLayout->setContentsMargins(0, 0, 0, 0); + metricDataLayout->addSpacing(5); + metricDataLayout->addWidget(metricLabel_); + metricDataLayout->addSpacing(5); + metricDataLayout->addWidget(metricEdit_,10); + metricDataLayout->addStretch(); + + // buttons + QHBoxLayout* buttons = new QHBoxLayout; + QPushButton* cancel = new QPushButton(tr("Cancel"), this); + QPushButton* clear = new QPushButton(tr("Clear"), this); + QPushButton* set = new QPushButton(tr("Set"), this); + buttons->addStretch(75); + buttons->addWidget(cancel); + buttons->addWidget(clear); + buttons->addWidget(set); + + // Layout + QFormLayout* layout = new QFormLayout(this); + layout->addRow(metricDataLayout); + layout->addRow(buttons); + + adjustSize(); // Window to contents + + connect(set, SIGNAL(clicked()), this, SLOT(setClicked())); + connect(clear, SIGNAL(clicked()), this, SLOT(clearClicked())); + connect(cancel, SIGNAL(clicked()), this, SLOT(cancelClicked())); +} + +MetricOverrideDialog::~MetricOverrideDialog() +{ + delete metricLabel_; + delete metricEdit_; // QWidget destructor is virtual +} + +void +MetricOverrideDialog::showEvent(QShowEvent*) +{ + + QSize gcWindowSize = context_->mainWindow->size(); + QPoint gcWindowPosn = context_->mainWindow->pos(); + + int xLimit = gcWindowPosn.x() + gcWindowSize.width() - geometry().width() - 10; + int yLimit = gcWindowPosn.y() + gcWindowSize.height() - geometry().height() - 10; + + int xDialog = (pos_.x() > xLimit) ? xLimit : pos_.x(); + int yDialog = (pos_.y() > yLimit) ? yLimit : pos_.y(); + + move(xDialog, yDialog); +} + +void +MetricOverrideDialog::setClicked() +{ + // get the current value from the metricEdit_ into a string + QString text; + switch (dlgMetricType_) { + case DialogMetricType::DATE: { + text = QString("%1").arg(QDate(1900, 01, 01).daysTo(dynamic_cast(metricEdit_)->date())); + } break; + case DialogMetricType::SECS_TIME: { + text = QString("%1").arg(QTime(0, 0, 0, 0).secsTo(dynamic_cast(metricEdit_)->time())); + } break; + case DialogMetricType::MINS_TIME: { + text = QString("%1").arg((QTime(0, 0, 0, 0).secsTo(dynamic_cast(metricEdit_)->time())) / 60.0); + } break; + case DialogMetricType::DOUBLE: { + text = QString("%1").arg(dynamic_cast(metricEdit_)->value()); + + // convert from imperial to metric if needed + if (!GlobalContext::context()->useMetricUnits) { + double value = text.toDouble() * (1 / GlobalContext::context()->specialFields.rideMetric(fieldName_)->conversion()); + value -= GlobalContext::context()->specialFields.rideMetric(fieldName_)->conversionSum(); + text = QString("%1").arg(value); + } + } break; + default: { + qDebug() << "Unsupported type in metric override dialog"; + } break; + } + + // update metric override QMap! + QMap override; + override.insert("value", text); + + // check for compatability metrics + QString symbol = GlobalContext::context()->specialFields.metricSymbol(fieldName_); + if (fieldName_ == "TSS") symbol = "coggan_tss"; + if (fieldName_ == "NP") symbol = "coggan_np"; + if (fieldName_ == "IF") symbol = "coggan_if"; + + RideItem* rideI = context_->rideItem(); + if (!rideI) { qDebug() << "rideI error in metric override dialog"; done(QDialog::Rejected); return; } + + // Update the metadata value in the ride item. + rideI->ride()->metricOverrides.insert(symbol, override); + + // rideItem is now dirty! + rideI->setDirty(true); + + // refresh as state has changed + rideI->notifyRideMetadataChanged(); + + done(QDialog::Accepted); // our work is done! +} + +void +MetricOverrideDialog::clearClicked() { + + QString symbol = GlobalContext::context()->specialFields.metricSymbol(fieldName_); + if (fieldName_ == "TSS") symbol = "coggan_tss"; + if (fieldName_ == "NP") symbol = "coggan_np"; + if (fieldName_ == "IF") symbol = "coggan_if"; + + RideItem* rideI = context_->rideItem(); + if (!rideI) { qDebug() << "rideI error in metric override dialog"; done(QDialog::Rejected); return; } + + if (rideI->ride()->metricOverrides.contains(symbol)) { + + // remove existing override for this metric + rideI->ride()->metricOverrides.remove(symbol); + + // rideItem is now dirty! + rideI->setDirty(true); + + // get refresh done, coz overrides state has changed + rideI->notifyRideMetadataChanged(); + + done(QDialog::Accepted); // our work is done! + + } else { + + done(QDialog::Rejected); // our work is done! + } +} + +void +MetricOverrideDialog::cancelClicked() +{ + done(QDialog::Rejected); // no work to do +} diff --git a/src/Gui/MetricOverrideDialog.h b/src/Gui/MetricOverrideDialog.h new file mode 100644 index 000000000..e370c90b2 --- /dev/null +++ b/src/Gui/MetricOverrideDialog.h @@ -0,0 +1,62 @@ + +/* + * Metric Override Dialog Copyright (c) 2025 Paul Johnson (paulj49457@gmail.com) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, 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 + */ + +#ifndef _MetricOverrideDialog_h +#define _MetricOverrideDialog_h + +#include "GoldenCheetah.h" +#include "Context.h" + +#include +#include + +// Dialog class to allow overriding of metric data items +class MetricOverrideDialog : public QDialog +{ + Q_OBJECT + G_OBJECT + + public: + MetricOverrideDialog(Context *context, const QString& fieldName, const double value, QPoint pos); + virtual ~MetricOverrideDialog(); + + protected: + + void showEvent(QShowEvent*) override; + + private slots: + + void cancelClicked(); + void clearClicked(); + void setClicked(); + + private: + + enum class DialogMetricType { DATE, SECS_TIME, MINS_TIME, DOUBLE }; + + QPoint pos_; + QString fieldName_; + DialogMetricType dlgMetricType_ = DialogMetricType::DOUBLE; + Context* context_ = nullptr; + + QLabel* metricLabel_ = nullptr; + QWidget* metricEdit_ = nullptr; +}; + +#endif // _MetricOverrideDialog_h diff --git a/src/Metrics/SpecialFields.cpp b/src/Metrics/SpecialFields.cpp index a83eaf74d..8a8025b6a 100644 --- a/src/Metrics/SpecialFields.cpp +++ b/src/Metrics/SpecialFields.cpp @@ -138,7 +138,7 @@ SpecialFields::metricSymbol(QString name) const } const RideMetric * -SpecialFields::rideMetric(QString&name) const +SpecialFields::rideMetric(const QString &name) const { return metricmap.value(name, NULL); } diff --git a/src/Metrics/SpecialFields.h b/src/Metrics/SpecialFields.h index 386695988..b7d11fa99 100644 --- a/src/Metrics/SpecialFields.h +++ b/src/Metrics/SpecialFields.h @@ -38,7 +38,7 @@ class SpecialFields QString makeTechName(QString) const; // return a SQL friendly name QString metricSymbol(QString) const; // return symbol for user friendly name - const RideMetric *rideMetric(QString&) const; // retuen metric ptr for user friendly name + const RideMetric *rideMetric(const QString&) const; // return metric ptr for user friendly name QString displayName(QString &) const; // return display (localized) name for name QString internalName(QString) const; // return internal (english) Name for display diff --git a/src/src.pro b/src/src.pro index 098ed8d6a..d5fc099b1 100644 --- a/src/src.pro +++ b/src/src.pro @@ -649,7 +649,8 @@ HEADERS += Gui/AboutDialog.h Gui/AddIntervalDialog.h Gui/AnalysisSidebar.h Gui/C Gui/Views.h Gui/BatchProcessingDialog.h Gui/DownloadRideDialog.h Gui/ManualRideDialog.h Gui/NewSideBar.h \ Gui/MergeActivityWizard.h Gui/RideImportWizard.h Gui/SplitActivityWizard.h Gui/SolverDisplay.h Gui/MetricSelect.h \ Gui/AddTileWizard.h Gui/NavigationModel.h Gui/AthleteView.h Gui/AthleteConfigDialog.h Gui/AthletePages.h Gui/Perspective.h \ - Gui/PerspectiveDialog.h Gui/SplashScreen.h Gui/StyledItemDelegates.h Gui/MetadataDialog.h Gui/ActionButtonBox.h + Gui/PerspectiveDialog.h Gui/SplashScreen.h Gui/StyledItemDelegates.h Gui/MetadataDialog.h Gui/ActionButtonBox.h \ + Gui/MetricOverrideDialog.h # metrics and models HEADERS += Metrics/Banister.h Metrics/CPSolver.h Metrics/Estimator.h Metrics/ExtendedCriticalPower.h Metrics/HrZones.h Metrics/PaceZones.h \ @@ -759,7 +760,8 @@ SOURCES += Gui/AboutDialog.cpp Gui/AddIntervalDialog.cpp Gui/AnalysisSidebar.cpp Gui/BatchProcessingDialog.cpp Gui/DownloadRideDialog.cpp Gui/ManualRideDialog.cpp Gui/EditUserMetricDialog.cpp Gui/NewSideBar.cpp \ Gui/MergeActivityWizard.cpp Gui/RideImportWizard.cpp Gui/SplitActivityWizard.cpp Gui/SolverDisplay.cpp Gui/MetricSelect.cpp \ Gui/AddTileWizard.cpp Gui/NavigationModel.cpp Gui/AthleteView.cpp Gui/AthleteConfigDialog.cpp Gui/AthletePages.cpp Gui/Perspective.cpp \ - Gui/PerspectiveDialog.cpp Gui/SplashScreen.cpp Gui/StyledItemDelegates.cpp Gui/MetadataDialog.cpp Gui/ActionButtonBox.cpp + Gui/PerspectiveDialog.cpp Gui/SplashScreen.cpp Gui/StyledItemDelegates.cpp Gui/MetadataDialog.cpp Gui/ActionButtonBox.cpp \ + Gui/MetricOverrideDialog.cpp ## Models and Metrics SOURCES += Metrics/aBikeScore.cpp Metrics/aCoggan.cpp Metrics/AerobicDecoupling.cpp Metrics/Banister.cpp Metrics/BasicRideMetrics.cpp \