diff --git a/src/Core/Athlete.cpp b/src/Core/Athlete.cpp index e4e7ad814..f972289ab 100644 --- a/src/Core/Athlete.cpp +++ b/src/Core/Athlete.cpp @@ -471,7 +471,7 @@ AthleteDirectoryStructure::upgradedDirectoriesHaveData() { } QList -Athlete::getPDEstimates() +Athlete::getPDEstimates() const { // returns whatever estimator has, if not running QList returning; @@ -592,13 +592,37 @@ Athlete::getPMCFor(Leaf *expr, DataFilterRuntime *df, int stsdays, int ltsdays) } PDEstimate -Athlete::getPDEstimateFor(QDate date, QString model, bool wpk, QString sport) +Athlete::getPDEstimateFor(QDate date, QString model, bool wpk, QString sport) const { // whats the estimate for this date foreach(PDEstimate est, getPDEstimates()) { - if (est.model == model && est.wpk == wpk && est.sport == sport && est.from <= date && est.to >= date) + if (est.model == model && est.wpk == wpk && est.sport == sport && est.from <= date && est.to >= date) { return est; + } } return PDEstimate(); } + +PDEstimate +Athlete::getPDEstimateClosestFor +(QDate date, QString model, bool wpk, QString sport) const +{ + qint64 dist = INT64_MAX; + PDEstimate ret; + foreach (PDEstimate est, getPDEstimates()) { + if (est.model != model || est.wpk != wpk || est.sport != sport) { + continue; + } + if (est.from <= date && date <= est.to) { + return est; + } else if (date < est.from && dist > date.daysTo(est.from)) { + dist = date.daysTo(est.from); + ret = est; + } else if (est.to < date && dist > est.to.daysTo(date)) { + dist = est.to.daysTo(date); + ret = est; + } + } + return ret; +} diff --git a/src/Core/Athlete.h b/src/Core/Athlete.h index b138c5271..6517d6146 100644 --- a/src/Core/Athlete.h +++ b/src/Core/Athlete.h @@ -100,8 +100,9 @@ class Athlete : public QObject CloudServiceAutoDownload *cloudAutoDownload; // Estimates - PDEstimate getPDEstimateFor(QDate, QString model, bool wpk, QString sport); - QList getPDEstimates(); + PDEstimate getPDEstimateFor(QDate, QString model, bool wpk, QString sport) const; + PDEstimate getPDEstimateClosestFor(QDate date, QString model, bool wpk, QString sport) const; + QList getPDEstimates() const; // PMC Data PMCData *getPMCFor(QString metricName, int stsDays = -1, int ltsDays = -1); // no Specification used! diff --git a/src/Core/Settings.h b/src/Core/Settings.h index 918e6aafa..00ae947f9 100644 --- a/src/Core/Settings.h +++ b/src/Core/Settings.h @@ -289,6 +289,7 @@ #define GC_CRANKLENGTH "crankLength" #define GC_WHEELSIZE "wheelsize" #define GC_USE_CP_FOR_FTP "cp/useforftp" // use CP for FTP +#define GC_USE_CP_MODEL "cp/useModel" #define GC_NETWORKFILESTORE_FOLDER "networkfilestore/folder" // folder to sync with #define GC_AUTOBACKUP_FOLDER "autobackup/folder" #define GC_AUTOBACKUP_PERIOD "autobackup/period" // how often is the Athlete Folder backuped up / 0 == never diff --git a/src/Core/Units.cpp b/src/Core/Units.cpp index 64443d872..e2bed283c 100644 --- a/src/Core/Units.cpp +++ b/src/Core/Units.cpp @@ -19,18 +19,18 @@ #include "Units.h" #include -QString kphToPace(double kph, bool metric, bool isSwim) +QTime kphToPaceTime(double kph, bool metric, bool isSwim) { // return a min/mile or min/kph string if (!metric && !isSwim) kph /= KM_PER_MILE; // stop silly stuff if (kph < 0.1) { - return "00:00"; + return QTime(0, 0, 0); } if (kph > 99) { - return "xx:xx"; + return QTime(); } // Swim pace is min/100m or min/100y @@ -47,9 +47,16 @@ QString kphToPace(double kph, bool metric, bool isSwim) minutes++; } - return QString("%1:%2") - .arg(minutes, 2, 10, QLatin1Char('0')) - .arg(seconds, 2, 10, QLatin1Char('0')); + return QTime(0, minutes, seconds); +} + +QString kphToPace(double kph, bool metric, bool isSwim) +{ + QTime time = kphToPaceTime(kph, metric, isSwim); + if (! time.isValid()) { + return "xx:xx"; + } + return time.toString("mm:ss"); } QString mphToPace(double mph, bool metric, bool isSwim) diff --git a/src/Core/Units.h b/src/Core/Units.h index 433860df4..a0728811b 100644 --- a/src/Core/Units.h +++ b/src/Core/Units.h @@ -18,7 +18,6 @@ #ifndef _GC_Units_h #define _GC_Units_h 1 -#include "GoldenCheetah.h" #define KM_PER_MILE 1.609344f #define MILES_PER_KM 0.62137119f @@ -41,8 +40,10 @@ #define MS_IN_WKO_HOURS 360000 // yes this number of ms is required to match for WKO #include +#include + +extern QTime kphToPaceTime(double kph, bool metric, bool isSwim=false); extern QString kphToPace(double kph, bool metric, bool isSwim=false); extern QString mphToPace(double mph, bool metric, bool isSwim=false); #endif // _GC_Units_h - diff --git a/src/Gui/AthleteConfigDialog.cpp b/src/Gui/AthleteConfigDialog.cpp index 594cc9e25..66ff92ec7 100644 --- a/src/Gui/AthleteConfigDialog.cpp +++ b/src/Gui/AthleteConfigDialog.cpp @@ -163,12 +163,20 @@ AthleteConfig::AthleteConfig(QDir home, Context *context) : QWidget *zc = new QWidget(this); QVBoxLayout *zl = new QVBoxLayout(zc); - QTabWidget *zonesTab = new QTabWidget(this); - zl->addWidget(zonesTab); - zonesTab->setContentsMargins(10*dpiXFactor,10*dpiXFactor,10*dpiXFactor,10*dpiXFactor); - zonesTab->addTab(zonePage, tr("Power")); - zonesTab->addTab(hrZonePage, tr("Heartrate")); - zonesTab->addTab(paceZonePage, tr("Pace")); + QTabWidget *zonesTab = nullptr; + if (context->athlete->rideCache->isRunning()) { + QLabel *warn = new QLabel(QString("

%1

%2
") + .arg(tr("Refresh in Progress")) + .arg(tr("A metric refresh is currently running, please try again once that has completed."))); + zl->addWidget(warn, 0, Qt::AlignCenter); + } else { + zonesTab = new QTabWidget(this); + zl->addWidget(zonesTab); + zonesTab->setContentsMargins(10*dpiXFactor,10*dpiXFactor,10*dpiXFactor,10*dpiXFactor); + zonesTab->addTab(zonePage, tr("Power")); + zonesTab->addTab(hrZonePage, tr("Heartrate")); + zonesTab->addTab(paceZonePage, tr("Pace")); + } // if the plot background and window background // are the same color, lets use the accent color since @@ -196,7 +204,9 @@ AthleteConfig::AthleteConfig(QDir home, Context *context) : .arg(2*dpiXFactor) // 4 padding .arg(75*dpiXFactor) // 5 tab minimum width .arg(std.color(QPalette::Text).name()); // 6 tab text color - zonesTab->setStyleSheet(styling); + if (zonesTab != nullptr) { + zonesTab->setStyleSheet(styling); + } measuresTab->setStyleSheet(styling); #endif diff --git a/src/Gui/AthletePages.cpp b/src/Gui/AthletePages.cpp index ef86d80e4..ea6ea2e6a 100644 --- a/src/Gui/AthletePages.cpp +++ b/src/Gui/AthletePages.cpp @@ -54,6 +54,7 @@ #endif #include "MainWindow.h" + // // Passwords page // @@ -950,10 +951,12 @@ ZonePage::saveClicked() if (zonesFile.exists()) context->athlete->zones_[i]->read(zonesFile); } + // which model to use? + appsettings->setCValue(context->athlete->cyclist, zones[i]->useCPModelSetting(), cpPage[i]->useModel->currentIndex()); + // use CP for FTP? appsettings->setCValue(context->athlete->cyclist, zones[i]->useCPforFTPSetting(), cpPage[i]->useCPForFTPCombo->currentIndex()); - // did cp for ftp change ? cppageChanged |= cpPage[i]->saveClicked(); @@ -983,39 +986,31 @@ SchemePage::SchemePage(Zones* zones) : zones(zones) actionButtons->addWidget(addButton); actionButtons->addWidget(deleteButton); + zoneFromDelegate.setRange(0, 1000); + zoneFromDelegate.setSingleStep(1); + zoneFromDelegate.setSuffix(tr("%")); + zoneFromDelegate.setShowSuffixOnEdit(true); + zoneFromDelegate.setShowSuffixOnDisplay(true); + scheme = new QTreeWidget; + scheme->setAlternatingRowColors(true); scheme->headerItem()->setText(0, tr("Short")); scheme->headerItem()->setText(1, tr("Long")); scheme->headerItem()->setText(2, tr("Percent of CP")); scheme->setColumnCount(3); - scheme->setSelectionMode(QAbstractItemView::SingleSelection); - scheme->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit - scheme->setUniformRowHeights(true); - scheme->setIndentation(0); - //scheme->header()->resizeSection(0,90); - //scheme->header()->resizeSection(1,200); - //scheme->header()->resizeSection(2,80); + scheme->setItemDelegateForColumn(2, &zoneFromDelegate); + basicTreeWidgetStyle(scheme); // setup list - for (int i=0; i< zones->getScheme().nzones_default; i++) { - + for (int i = 0; i < zones->getScheme().nzones_default; i++) { QTreeWidgetItem *add = new QTreeWidgetItem(scheme->invisibleRootItem()); add->setFlags(add->flags() | Qt::ItemIsEditable); - // tab name - add->setText(0, zones->getScheme().zone_default_name[i]); - // field name - add->setText(1, zones->getScheme().zone_default_desc[i]); - - // low - QDoubleSpinBox *loedit = new QDoubleSpinBox(this); - loedit->setMinimum(0); - loedit->setMaximum(1000); - loedit->setSingleStep(1.0); - loedit->setDecimals(0); - loedit->setValue(zones->getScheme().zone_default[i]); - scheme->setItemWidget(add, 2, loedit); + add->setData(0, Qt::DisplayRole, zones->getScheme().zone_default_name[i]); + add->setData(1, Qt::DisplayRole, zones->getScheme().zone_default_desc[i]); + add->setData(2, Qt::DisplayRole, zones->getScheme().zone_default[i]); } + updateButtons(); mainLayout->addWidget(scheme); mainLayout->addLayout(actionButtons); @@ -1023,56 +1018,37 @@ SchemePage::SchemePage(Zones* zones) : zones(zones) // button connect connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); + connect(scheme, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), this, SLOT(updateButtons())); } + void SchemePage::addClicked() { - // are we at maximum already? - if (scheme->invisibleRootItem()->childCount() == 10) { - QMessageBox err; - err.setText(tr("Maximum of 10 zones reached.")); - err.setIcon(QMessageBox::Warning); - err.exec(); - return; - } - int index = scheme->invisibleRootItem()->childCount(); // new item QTreeWidgetItem *add = new QTreeWidgetItem; add->setFlags(add->flags() | Qt::ItemIsEditable); - QDoubleSpinBox *loedit = new QDoubleSpinBox(this); - loedit->setMinimum(0); - loedit->setMaximum(1000); - loedit->setSingleStep(1.0); - loedit->setDecimals(0); - loedit->setValue(100); - scheme->invisibleRootItem()->insertChild(index, add); - scheme->setItemWidget(add, 2, loedit); // Short QString text = tr("New"); for (int i=0; scheme->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(0, text); + add->setData(0, Qt::DisplayRole, text); // long text = tr("New"); for (int i=0; scheme->findItems(text, Qt::MatchExactly, 1).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(1, text); -} + add->setData(1, Qt::DisplayRole, text); + add->setData(2, Qt::DisplayRole, 100); -void -SchemePage::renameClicked() -{ - // which one is selected? - if (scheme->currentItem()) scheme->editItem(scheme->currentItem(), 0); + updateButtons(); } void @@ -1081,9 +1057,19 @@ SchemePage::deleteClicked() if (scheme->currentItem()) { int index = scheme->invisibleRootItem()->indexOfChild(scheme->currentItem()); delete scheme->invisibleRootItem()->takeChild(index); + updateButtons(); } } + +void +SchemePage::updateButtons +() +{ + addButton->setEnabled(scheme->invisibleRootItem()->childCount() < 10); + deleteButton->setEnabled(scheme->invisibleRootItem()->childCount() > 0 && scheme->currentItem() != nullptr); +} + // just for sorting struct schemeitem { QString name, desc; @@ -1101,11 +1087,10 @@ SchemePage::getScheme() // read back the details from the table for (int i=0; iinvisibleRootItem()->childCount(); i++) { - schemeitem add; - add.name = scheme->invisibleRootItem()->child(i)->text(0); - add.desc = scheme->invisibleRootItem()->child(i)->text(1); - add.lo = ((QDoubleSpinBox *)(scheme->itemWidget(scheme->invisibleRootItem()->child(i), 2)))->value(); + add.name = scheme->invisibleRootItem()->child(i)->data(0, Qt::DisplayRole).toString(); + add.desc = scheme->invisibleRootItem()->child(i)->data(1, Qt::DisplayRole).toString(); + add.lo = scheme->invisibleRootItem()->child(i)->data(2, Qt::DisplayRole).toInt(); table.append(add); } @@ -1126,24 +1111,73 @@ SchemePage::getScheme() } + +#define CPPAGE_DEFAULT_CP 250 +#define CPPAGE_DEFAULT_FACTOR_AETP 0.85 +#define CPPAGE_DEFAULT_WPRIME 20000 +#define CPPAGE_DEFAULT_PMAX 1000 + +#define CPPAGE_RANGES_COL_STARTDATE 0 +#define CPPAGE_RANGES_COL_CP 1 +#define CPPAGE_RANGES_COL_AETP 2 +#define CPPAGE_RANGES_COL_FTP 3 +#define CPPAGE_RANGES_COL_WPRIME 4 +#define CPPAGE_RANGES_COL_PMAX 5 +#define CPPAGE_RANGES_COL_MODELFIT 6 +#define CPPAGE_RANGES_COL_EST_DEVIATION 7 +#define CPPAGE_RANGES_COL_EST_OFFSET 8 +#define CPPAGE_RANGES_COL_EST_CP 9 +#define CPPAGE_RANGES_COL_EST_FTP 10 +#define CPPAGE_RANGES_COL_EST_WPRIME 11 +#define CPPAGE_RANGES_COL_EST_PMAX 12 +#define CPPAGE_RANGES_COUNT_COL 13 + +#define CPPAGE_RANGES_EST_MATCH 0 +#define CPPAGE_RANGES_EST_DEVIATE 1 +#define CPPAGE_RANGES_EST_NA 2 + +#define CPPAGE_EST_MODEL_NONE 0 +#define CPPAGE_EST_MODEL_CP2 1 +#define CPPAGE_EST_MODEL_CP3 2 +#define CPPAGE_EST_MODEL_EXT 3 + +#define CPPAGE_EST_TOLERANCE_CP 5 +#define CPPAGE_EST_TOLERANCE_WPRIME 1000 +#define CPPAGE_EST_TOLERANCE_PMAX 10 + +#define CPPAGE_INFO_AETP 0 +#define CPPAGE_INFO_FTP_CP 1 +#define CPPAGE_INFO_MODEL_FTP 2 +#define CPPAGE_INFO_MODEL_PMAX 3 +#define CPPAGE_INFO_MODEL_CP2 4 +#define CPPAGE_INFO_MODEL_CP3 5 +#define CPPAGE_LABEL_STARTDATE 6 +#define CPPAGE_LABEL_CP 7 +#define CPPAGE_LABEL_AETP 8 +#define CPPAGE_LABEL_FTP 9 +#define CPPAGE_LABEL_WPRIME 10 +#define CPPAGE_LABEL_PMAX 11 + CPPage::CPPage(Context *context, Zones *zones_, SchemePage *schemePage) : context(context), zones_(zones_), schemePage(schemePage) { active = false; - QGridLayout *mainLayout = new QGridLayout(this); + QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setSpacing(10 *dpiXFactor); - updateButton = new QPushButton(tr("Update")); - updateButton->hide(); addButton = new QPushButton(tr("+")); deleteButton = new QPushButton(tr("-")); + adoptButton = new QPushButton(tr("Adopt")); + adoptButton->setVisible(false); + newZoneRequired = new QPushButton(tr("Changed power estimates are available")); + newZoneRequired->setFlat(true); + newZoneRequired->setVisible(false); #ifndef Q_OS_MAC addButton->setFixedSize(20*dpiXFactor,20*dpiYFactor); deleteButton->setFixedSize(20*dpiXFactor,20*dpiYFactor); #else addButton->setText(tr("Add")); - updateButton->setText(tr("Update")); deleteButton->setText(tr("Delete")); #endif defaultButton = new QPushButton(tr("Def")); @@ -1160,304 +1194,684 @@ CPPage::CPPage(Context *context, Zones *zones_, SchemePage *schemePage) : #endif QHBoxLayout *zoneButtons = new QHBoxLayout; + zoneButtons->setSpacing(2 * dpiXFactor); zoneButtons->addStretch(); - zoneButtons->setSpacing(0); - zoneButtons->addWidget(addZoneButton); - zoneButtons->addWidget(deleteZoneButton); zoneButtons->addWidget(defaultButton); + zoneButtons->addItem(new QSpacerItem(5 * dpiXFactor, defaultButton->sizeHint().height())); + zoneButtons->addWidget(addZoneButton, 0, Qt::AlignHCenter | Qt::AlignTop); + zoneButtons->addWidget(deleteZoneButton, 0, Qt::AlignHCenter | Qt::AlignTop); QHBoxLayout *addLayout = new QHBoxLayout; - QLabel *dateLabel = new QLabel(tr("From Date")); - QLabel *cpLabel = new QLabel(tr("Critical Power")); - QLabel *aetLabel = new QLabel(tr("AeTP")); - QLabel *ftpLabel = new QLabel(tr("FTP")); - QLabel *wLabel = new QLabel(tr("W'")); - QLabel *pmaxLabel = new QLabel(tr("Pmax")); - dateEdit = new QDateEdit; - dateEdit->setDate(QDate::currentDate()); - dateEdit->setCalendarPopup(true); + + useModel = new QComboBox(this); + useModel->addItem(tr("Manual"), "manual"); + useModel->addItem(tr("Semi-Automatic (CP2)"), "cp2"); + useModel->addItem(tr("Semi-Automatic (CP3)"), "cp3"); + useModel->addItem(tr("Semi-Automatic (Extended)"), "ext"); // Use CP for FTP useCPForFTPCombo = new QComboBox(this); useCPForFTPCombo->addItem(tr("Use CP for all metrics")); useCPForFTPCombo->addItem(tr("Use FTP for Coggan metrics")); + b4.modelIdx = appsettings->cvalue(context->athlete->cyclist, zones_->useCPModelSetting(), CPPAGE_EST_MODEL_NONE).toInt(); + useModel->setCurrentIndex(b4.modelIdx); + b4.cpforftp = appsettings->cvalue(context->athlete->cyclist, zones_->useCPforFTPSetting(), 0).toInt() ? 1 : 0; useCPForFTPCombo->setCurrentIndex(b4.cpforftp); - cpEdit = new QDoubleSpinBox; - cpEdit->setMinimum(0); - cpEdit->setMaximum(1000); - cpEdit->setSingleStep(1.0); - cpEdit->setDecimals(0); - - aetEdit = new QDoubleSpinBox; - aetEdit->setMinimum(0); - aetEdit->setMaximum(1000); - aetEdit->setSingleStep(1.0); - aetEdit->setDecimals(0); - - ftpEdit = new QDoubleSpinBox; - ftpEdit->setMinimum(0); - ftpEdit->setMaximum(100000); - ftpEdit->setSingleStep(100); - ftpEdit->setDecimals(0); - - wEdit = new QDoubleSpinBox; - wEdit->setMinimum(0); - wEdit->setMaximum(100000); - wEdit->setSingleStep(100); - wEdit->setDecimals(0); - - pmaxEdit = new QDoubleSpinBox; - pmaxEdit->setMinimum(0); - pmaxEdit->setMaximum(3000); - pmaxEdit->setSingleStep(1.0); - pmaxEdit->setDecimals(0); - QHBoxLayout *actionButtons = new QHBoxLayout; actionButtons->setSpacing(2 *dpiXFactor); - actionButtons->addWidget(cpLabel); - actionButtons->addWidget(cpEdit); - - - actionButtons->addWidget(aetLabel); - actionButtons->addWidget(aetEdit); - actionButtons->addWidget(ftpLabel); - actionButtons->addWidget(ftpEdit); - - actionButtons->addWidget(wLabel); - actionButtons->addWidget(wEdit); - actionButtons->addWidget(pmaxLabel); - actionButtons->addWidget(pmaxEdit); + actionButtons->addWidget(newZoneRequired); actionButtons->addStretch(); - actionButtons->addWidget(updateButton); - actionButtons->addWidget(addButton); - actionButtons->addWidget(deleteButton); + actionButtons->addWidget(adoptButton); + actionButtons->addItem(new QSpacerItem(5 * dpiXFactor, adoptButton->sizeHint().height())); + actionButtons->addWidget(addButton, 0, Qt::AlignHCenter | Qt::AlignTop); + actionButtons->addWidget(deleteButton, 0, Qt::AlignHCenter | Qt::AlignTop); - addLayout->addWidget(dateLabel); - addLayout->addWidget(dateEdit); addLayout->addStretch(); + addLayout->addWidget(useModel); addLayout->addWidget(useCPForFTPCombo); - ranges = new QTreeWidget; - initializeRanges(); + dateDelegate.setCalendarPopup(true); + + cpDelegate.setRange(50, 500); + cpDelegate.setSingleStep(5); + cpDelegate.setSuffix(tr("W")); + cpDelegate.setShowSuffixOnEdit(true); + cpDelegate.setShowSuffixOnDisplay(true); + + aetDelegate.setRange(50, 500); + aetDelegate.setSingleStep(5); + aetDelegate.setSuffix(tr("W")); + aetDelegate.setShowSuffixOnEdit(true); + aetDelegate.setShowSuffixOnDisplay(true); + + ftpDelegate.setRange(50, 500); + ftpDelegate.setSingleStep(5); + ftpDelegate.setSuffix(tr("W")); + ftpDelegate.setShowSuffixOnEdit(true); + ftpDelegate.setShowSuffixOnDisplay(true); + + wDelegate.setRange(1, 100000); + wDelegate.setSingleStep(100); + wDelegate.setSuffix(tr("J")); + wDelegate.setShowSuffixOnEdit(true); + wDelegate.setShowSuffixOnDisplay(true); + + pmaxDelegate.setRange(100, 3000); + pmaxDelegate.setSingleStep(10); + pmaxDelegate.setSuffix(tr("W")); + pmaxDelegate.setShowSuffixOnEdit(true); + pmaxDelegate.setShowSuffixOnDisplay(true); + + ranges = new TreeWidget6(); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_STARTDATE, tr("Start Date")); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_CP, tr("Critical Power")); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_AETP, tr("AeTP")); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_FTP, tr("FTP")); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_WPRIME, tr("W'")); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_PMAX, tr("Pmax")); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_MODELFIT, tr("Model Fit")); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_EST_DEVIATION, "_deviation"); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_EST_OFFSET, "_offset"); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_EST_CP, "_cp"); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_EST_FTP, "_ftp"); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_EST_WPRIME, "_wprime"); + ranges->headerItem()->setText(CPPAGE_RANGES_COL_EST_PMAX, "_pmax"); + ranges->setColumnCount(CPPAGE_RANGES_COUNT_COL); + ranges->setColumnHidden(CPPAGE_RANGES_COL_EST_DEVIATION, true); + ranges->setColumnHidden(CPPAGE_RANGES_COL_EST_OFFSET, true); + ranges->setColumnHidden(CPPAGE_RANGES_COL_EST_CP, true); + ranges->setColumnHidden(CPPAGE_RANGES_COL_EST_FTP, true); + ranges->setColumnHidden(CPPAGE_RANGES_COL_EST_WPRIME, true); + ranges->setColumnHidden(CPPAGE_RANGES_COL_EST_PMAX, true); + ranges->setItemDelegateForColumn(CPPAGE_RANGES_COL_STARTDATE, &dateDelegate); + ranges->setItemDelegateForColumn(CPPAGE_RANGES_COL_CP, &cpDelegate); + ranges->setItemDelegateForColumn(CPPAGE_RANGES_COL_AETP, &aetDelegate); + ranges->setItemDelegateForColumn(CPPAGE_RANGES_COL_FTP, &ftpDelegate); + ranges->setItemDelegateForColumn(CPPAGE_RANGES_COL_WPRIME, &wDelegate); + ranges->setItemDelegateForColumn(CPPAGE_RANGES_COL_PMAX, &pmaxDelegate); + ranges->setItemDelegateForColumn(CPPAGE_RANGES_COL_MODELFIT, &statusDelegate); + ranges->header()->setSectionResizeMode(CPPAGE_RANGES_COL_STARTDATE, QHeaderView::ResizeToContents); + ranges->header()->setSectionResizeMode(CPPAGE_RANGES_COL_CP, QHeaderView::ResizeToContents); + ranges->header()->setSectionResizeMode(CPPAGE_RANGES_COL_AETP, QHeaderView::ResizeToContents); + ranges->header()->setSectionResizeMode(CPPAGE_RANGES_COL_FTP, QHeaderView::ResizeToContents); + ranges->header()->setSectionResizeMode(CPPAGE_RANGES_COL_WPRIME, QHeaderView::ResizeToContents); + ranges->header()->setSectionResizeMode(CPPAGE_RANGES_COL_PMAX, QHeaderView::ResizeToContents); + ranges->header()->setSectionResizeMode(CPPAGE_RANGES_COL_MODELFIT, QHeaderView::ResizeToContents); + basicTreeWidgetStyle(ranges); + + zoneFromDelegate.setRange(0, 1000); + zoneFromDelegate.setSingleStep(1); + zoneFromDelegate.setSuffix(tr("W")); + zoneFromDelegate.setShowSuffixOnEdit(true); + zoneFromDelegate.setShowSuffixOnDisplay(true); zones = new QTreeWidget; zones->headerItem()->setText(0, tr("Short")); zones->headerItem()->setText(1, tr("Long")); zones->headerItem()->setText(2, tr("From Watts")); zones->setColumnCount(3); - zones->setSelectionMode(QAbstractItemView::SingleSelection); - zones->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit - zones->setUniformRowHeights(true); - zones->setIndentation(0); + zones->setItemDelegateForColumn(2, &zoneFromDelegate); + basicTreeWidgetStyle(zones); - mainLayout->addLayout(addLayout, 0,0); - mainLayout->addLayout(actionButtons, 1,0); - mainLayout->addWidget(ranges, 2,0); - mainLayout->addLayout(zoneButtons, 3,0); - mainLayout->addWidget(zones, 4,0); + mainLayout->addLayout(addLayout); + mainLayout->addWidget(ranges); + mainLayout->addLayout(actionButtons); + mainLayout->addWidget(zones); + mainLayout->addLayout(zoneButtons); + + initializeRanges(); - // edit connect - connect(dateEdit, SIGNAL(dateChanged(QDate)), this, SLOT(rangeEdited())); - connect(cpEdit, SIGNAL(valueChanged(double)), this, SLOT(rangeEdited())); - connect(aetEdit, SIGNAL(valueChanged(double)), this, SLOT(rangeEdited())); - connect(ftpEdit, SIGNAL(valueChanged(double)), this, SLOT(rangeEdited())); - connect(wEdit, SIGNAL(valueChanged(double)), this, SLOT(rangeEdited())); - connect(pmaxEdit, SIGNAL(valueChanged(double)), this, SLOT(rangeEdited())); // button connect + connect(newZoneRequired, SIGNAL(clicked()), this, SLOT(addClicked())); connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); - connect(updateButton, SIGNAL(clicked()), this, SLOT(editClicked())); connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); + connect(adoptButton, SIGNAL(clicked()), this, SLOT(adopt())); connect(defaultButton, SIGNAL(clicked()), this, SLOT(defaultClicked())); connect(addZoneButton, SIGNAL(clicked()), this, SLOT(addZoneClicked())); connect(deleteZoneButton, SIGNAL(clicked()), this, SLOT(deleteZoneClicked())); connect(useCPForFTPCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(initializeRanges())); + connect(useModel, SIGNAL(currentIndexChanged(int)), this, SLOT(initializeRanges())); connect(ranges, SIGNAL(itemSelectionChanged()), this, SLOT(rangeSelectionChanged())); connect(zones, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(zonesChanged())); + connect(zones, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), this, SLOT(updateButtons())); + connect(zones->model(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(zonesChanged())); } + qint32 CPPage::saveClicked() { - if (b4.cpforftp != useCPForFTPCombo->currentIndex()) + if ( b4.cpforftp != useCPForFTPCombo->currentIndex() + || b4.modelIdx != useModel->currentIndex()) { return CONFIG_ZONES; - else + } else { return 0; + } } + void CPPage::initializeRanges() { +#if QT_VERSION < 0x060000 + disconnect(ranges->model(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector&)), + this, SLOT(rangeChanged(const QModelIndex&, const QModelIndex&, const QVector&))); +#else + disconnect(ranges->model(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QList&)), + this, SLOT(rangeChanged(const QModelIndex&, const QModelIndex&, const QList&))); +#endif - while( int nb = ranges->topLevelItemCount () ) - { - delete ranges->takeTopLevelItem( nb - 1 ); + while (int nb = ranges->topLevelItemCount()) { + delete ranges->takeTopLevelItem(nb - 1); } - int column = 0; - ranges->headerItem()->setText(column++, tr("From Date")); - ranges->headerItem()->setText(column++, tr("Critical Power")); - ranges->headerItem()->setText(column++, tr("AeTP")); - ranges->headerItem()->setText(column++, tr("FTP")); - ranges->headerItem()->setText(column++, tr("W'")); - ranges->headerItem()->setText(column++, tr("Pmax")); - - ranges->setColumnCount(column); - bool useCPForFTP = (useCPForFTPCombo->currentIndex() == 0 ? true : false); ranges->setColumnHidden(3, useCPForFTP); - ranges->setSelectionMode(QAbstractItemView::SingleSelection); - ranges->setUniformRowHeights(true); - ranges->setIndentation(0); // setup list of ranges - for (int i=0; i< zones_->getRangeSize(); i++) { - + for (int i = 0; i < zones_->getRangeSize(); i++) { QTreeWidgetItem *add = new QTreeWidgetItem(ranges->invisibleRootItem()); - add->setFlags(add->flags() & ~Qt::ItemIsEditable); + add->setFlags(add->flags() | Qt::ItemIsEditable); // Embolden ranges with manually configured zones QFont font; - font.setWeight(zones_->getZoneRange(i).zonesSetFromCP ? - QFont::Normal : QFont::Black); + font.setWeight(zones_->getZoneRange(i).zonesSetFromCP ? QFont::Normal : QFont::Black); - int column = 0; - // date - add->setText(column, zones_->getStartDate(i).toString(tr("MMM d, yyyy"))); - add->setFont(column++, font); + add->setData(CPPAGE_RANGES_COL_STARTDATE, Qt::DisplayRole, zones_->getStartDate(i)); + add->setFont(CPPAGE_RANGES_COL_STARTDATE, font); + add->setData(CPPAGE_RANGES_COL_CP, Qt::DisplayRole, zones_->getCP(i)); + add->setFont(CPPAGE_RANGES_COL_CP, font); + add->setData(CPPAGE_RANGES_COL_AETP, Qt::DisplayRole, zones_->getAeT(i)); + add->setFont(CPPAGE_RANGES_COL_AETP, font); + add->setData(CPPAGE_RANGES_COL_FTP, Qt::DisplayRole, zones_->getFTP(i)); + add->setFont(CPPAGE_RANGES_COL_FTP, font); + add->setData(CPPAGE_RANGES_COL_WPRIME, Qt::DisplayRole, zones_->getWprime(i)); + add->setFont(CPPAGE_RANGES_COL_WPRIME, font); + add->setData(CPPAGE_RANGES_COL_PMAX, Qt::DisplayRole, zones_->getPmax(i)); + add->setFont(CPPAGE_RANGES_COL_PMAX, font); + add->setFont(CPPAGE_RANGES_COL_MODELFIT, font); - // CP - add->setText(column, QString("%1").arg(zones_->getCP(i))); - add->setFont(column++, font); - - // AeT - add->setText(column, QString("%1").arg(zones_->getAeT(i))); - add->setFont(column++, font); - - // FTP - add->setText(column, QString("%1").arg(zones_->getFTP(i))); - add->setFont(column++, font); - - // W' - add->setText(column, QString("%1").arg(zones_->getWprime(i))); - add->setFont(column++, font); - - // Pmax - add->setText(column, QString("%1").arg(zones_->getPmax(i))); - add->setFont(column++, font); + if (useModel->currentIndex() != CPPAGE_EST_MODEL_NONE) { + ranges->setColumnHidden(CPPAGE_RANGES_COL_MODELFIT, false); + int cp = 0; + int aetp = 0; + int ftp = 0; + int wprime = 0; + int pmax = 0; + int estOffset = 0; + bool defaults = false; + if (getValuesFor(zones_->getStartDate(i), false, cp, aetp, ftp, wprime, pmax, estOffset, defaults)) { + add->setData(CPPAGE_RANGES_COL_EST_OFFSET, Qt::DisplayRole, estOffset); + add->setData(CPPAGE_RANGES_COL_EST_CP, Qt::DisplayRole, cp); + add->setData(CPPAGE_RANGES_COL_EST_FTP, Qt::DisplayRole, ftp); + add->setData(CPPAGE_RANGES_COL_EST_WPRIME, Qt::DisplayRole, wprime); + add->setData(CPPAGE_RANGES_COL_EST_PMAX, Qt::DisplayRole, pmax); + } else { + add->setData(CPPAGE_RANGES_COL_EST_CP, Qt::DisplayRole, 0); + } + } else { + ranges->setColumnHidden(CPPAGE_RANGES_COL_MODELFIT, true); + } + setEstimateStatus(add); } - for(int i = 0; i < ranges->columnCount(); i++) - ranges->resizeColumnToContents(i); + + newZoneRequired->setVisible(needsNewRange()); + updateButtons(); + +#if QT_VERSION < 0x060000 + connect(ranges->model(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector&)), + this, SLOT(rangeChanged(const QModelIndex&, const QModelIndex&, const QVector&))); +#else + connect(ranges->model(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QList&)), + this, SLOT(rangeChanged(const QModelIndex&, const QModelIndex&, const QList&))); +#endif } +void +CPPage::rangeChanged +#if QT_VERSION < 0x060000 +(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) +#else +(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles) +#endif +{ + Q_UNUSED(bottomRight) + + if ( roles.length() > 0 + && ! roles.contains(Qt::DisplayRole) + && ! roles.contains(Qt::EditRole)) { + return; + } + zones_->setScheme(schemePage->getScheme()); + int index = topLeft.row(); + switch (topLeft.column()) { + case CPPAGE_RANGES_COL_STARTDATE: { + QDate date = topLeft.data().toDate(); + zones_->setStartDate(index, date); + if (useModel->currentIndex() != CPPAGE_EST_MODEL_NONE) { + adopt(); + setEstimateStatus(ranges->itemFromIndex(topLeft)); + } + break; + } + case CPPAGE_RANGES_COL_CP: { + int cp = topLeft.data().toInt(); + zones_->setCP(index, cp); + if (useCPForFTPCombo->currentIndex() == 0) { + zones_->setFTP(index, cp); + } + setEstimateStatus(ranges->itemFromIndex(topLeft)); + break; + } + case CPPAGE_RANGES_COL_AETP: + zones_->setAeT(index, topLeft.data().toInt()); + setEstimateStatus(ranges->itemFromIndex(topLeft)); + break; + case CPPAGE_RANGES_COL_FTP: + zones_->setFTP(index, topLeft.data().toInt()); + setEstimateStatus(ranges->itemFromIndex(topLeft)); + break; + case CPPAGE_RANGES_COL_WPRIME: { + int wp = topLeft.data().toInt(); + wp = wp ? wp : 20000; + if (wp < 1000) { + wp *= 1000; // entered in kJ we want joules + } + zones_->setWprime(index, wp); + setEstimateStatus(ranges->itemFromIndex(topLeft)); + break; + } + case CPPAGE_RANGES_COL_PMAX: { + int pmax = topLeft.data().toInt(); + pmax = pmax ? pmax : 1000; + zones_->setPmax(index, pmax); + setEstimateStatus(ranges->itemFromIndex(topLeft)); + break; + } + default: + break; + } + if (useModel->currentIndex() == CPPAGE_EST_MODEL_NONE) { + adoptButton->setVisible(false); + } else if (ranges->currentItem() != nullptr && ranges->indexFromItem(ranges->currentItem()).row() == topLeft.row()) { + adoptButton->setVisible(ranges->currentItem()->data(CPPAGE_RANGES_COL_EST_DEVIATION, Qt::DisplayRole).toInt() == CPPAGE_RANGES_EST_DEVIATE); + newZoneRequired->setVisible(needsNewRange()); + } +} + + +void +CPPage::adopt +() +{ + bool preActive = active; + active = false; + QTreeWidgetItem *item = ranges->currentItem(); + if (item != nullptr) { + QDate date = item->data(CPPAGE_RANGES_COL_STARTDATE, Qt::DisplayRole).toDate(); + int curCp = item->data(CPPAGE_RANGES_COL_CP, Qt::DisplayRole).toInt(); + int curAetp = item->data(CPPAGE_RANGES_COL_AETP, Qt::DisplayRole).toInt(); + int curFtp = item->data(CPPAGE_RANGES_COL_FTP, Qt::DisplayRole).toInt(); + int curWprime = item->data(CPPAGE_RANGES_COL_WPRIME, Qt::DisplayRole).toInt(); + int curPmax = item->data(CPPAGE_RANGES_COL_PMAX, Qt::DisplayRole).toInt(); + QModelIndex modelIndex = ranges->indexFromItem(item); + int estCp = 0; + int estAetp = 0; + int estFtp = 0; + int estWprime = 0; + int estPmax = 0; + int estOffset = 0; + bool defaults = false; + if ( getValuesFor(date, false, estCp, estAetp, estFtp, estWprime, estPmax, estOffset, defaults) + && ( estCp != curCp + || estFtp != curFtp + || estWprime != curWprime + || estPmax != curPmax)) { + QLocale locale; + QDialog dialog; + dialog.setWindowTitle(tr("Adopt estimates for range starting on %1").arg(date.toString(locale.dateFormat(QLocale::ShortFormat)))); + + QCheckBox *cpAccept = nullptr; + QCheckBox *aetpAccept = nullptr; + QCheckBox *ftpAccept = nullptr; + QCheckBox *wprimeAccept = nullptr; + QCheckBox *pmaxAccept = nullptr; + + QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Apply + | QDialogButtonBox::Discard); + connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), &dialog, SLOT(accept())); + connect(buttonBox->button(QDialogButtonBox::Discard), SIGNAL(clicked()), &dialog, SLOT(reject())); + + int row = 0; + QGridLayout *grid = new QGridLayout(); + grid->setSpacing(10 * dpiXFactor); + grid->addWidget(new QLabel(tr("Current")), row, 2, Qt::AlignCenter); + grid->addWidget(new QLabel(tr("Estimate")), row, 4, Qt::AlignCenter); + grid->addWidget(new QLabel(tr("Accept")), row, 5, Qt::AlignCenter); + ++row; + mkAdoptionRow(grid, row++, CPPAGE_LABEL_CP, tr("W"), curCp, estCp, cpAccept); + QString infoTextAetp = ""; + if (curAetp != estAetp) { + infoTextAetp = getText(CPPAGE_INFO_AETP, CPPAGE_DEFAULT_FACTOR_AETP * 100); + } + mkAdoptionRow(grid, row++, CPPAGE_LABEL_AETP, tr("W"), curAetp, estAetp, aetpAccept, infoTextAetp); + if (estFtp != curFtp || useCPForFTPCombo->currentIndex() == 1) { + QString infoTextFtp = ""; + if (useCPForFTPCombo->currentIndex() == 0) { + infoTextFtp += getText(CPPAGE_INFO_FTP_CP); + } else if (! modelHasFtp()) { + infoTextFtp += getText(CPPAGE_INFO_MODEL_FTP); + } + mkAdoptionRow(grid, row++, CPPAGE_LABEL_FTP, tr("W"), curFtp, estFtp, ftpAccept, infoTextFtp); + } + mkAdoptionRow(grid, row++, CPPAGE_LABEL_WPRIME, tr("J"), curWprime, estWprime, wprimeAccept); + QString infoTextPmax = ""; + if (! modelHasPmax()) { + infoTextPmax += getText(CPPAGE_INFO_MODEL_PMAX, estPmax); + } + mkAdoptionRow(grid, row++, CPPAGE_LABEL_PMAX, tr("W"), curPmax, estPmax, pmaxAccept, infoTextPmax); + + connectAdoptionDialogApplyButton(QVector({cpAccept, aetpAccept, ftpAccept, wprimeAccept, pmaxAccept}), buttonBox->button(QDialogButtonBox::Apply)); + aetpAccept->setChecked(Qt::Unchecked); + + QVBoxLayout *mainLayout = new QVBoxLayout(&dialog); + mainLayout->addLayout(grid); + mainLayout->addStretch(); + mainLayout->addSpacing(30 * dpiYFactor); + mainLayout->addWidget(buttonBox); + + if (dialog.exec() == QDialog::Accepted) { + if (cpAccept->checkState() == Qt::Checked) { + setRangeData(modelIndex, CPPAGE_RANGES_COL_CP, estCp); + if (useCPForFTPCombo->currentIndex() == 0) { + setRangeData(modelIndex, CPPAGE_RANGES_COL_FTP, estFtp); + } + } + if (aetpAccept->checkState() == Qt::Checked) { + setRangeData(modelIndex, CPPAGE_RANGES_COL_AETP, estAetp); + } + if (ftpAccept->checkState() == Qt::Checked) { + setRangeData(modelIndex, CPPAGE_RANGES_COL_FTP, estFtp); + } + if (wprimeAccept->checkState() == Qt::Checked) { + setRangeData(modelIndex, CPPAGE_RANGES_COL_WPRIME, estWprime); + } + if (pmaxAccept->checkState() == Qt::Checked) { + setRangeData(modelIndex, CPPAGE_RANGES_COL_PMAX, estPmax); + } + } + setRangeData(modelIndex, CPPAGE_RANGES_COL_EST_CP, estCp); + setRangeData(modelIndex, CPPAGE_RANGES_COL_EST_FTP, estFtp); + setRangeData(modelIndex, CPPAGE_RANGES_COL_EST_WPRIME, estWprime); + setRangeData(modelIndex, CPPAGE_RANGES_COL_EST_PMAX, estPmax); + setRangeData(modelIndex, CPPAGE_RANGES_COL_EST_OFFSET, estOffset); + } + } + active = preActive; +} + + +void +CPPage::updateButtons +() +{ + addButton->setEnabled(true); + deleteButton->setEnabled(ranges->currentItem() != nullptr); + addZoneButton->setEnabled(zones->invisibleRootItem()->childCount() < 10 && ranges->currentItem() != nullptr); + deleteZoneButton->setEnabled(zones->invisibleRootItem()->childCount() > 0 && zones->currentItem() != nullptr); +} + + +bool +CPPage::getValuesFor +(const QDate &date, bool allowDefaults, int &cp, int &aetp, int &ftp, int &wprime, int &pmax, int &estOffset, bool &defaults, QDate *startDate) const +{ + estOffset = 0; + defaults = false; + if (useModel->currentIndex() != CPPAGE_EST_MODEL_NONE) { + PDEstimate pde = context->athlete->getPDEstimateClosestFor(date, useModel->currentData().toString(), false, zones_->sport()); + cp = pde.CP; + aetp = pde.CP * CPPAGE_DEFAULT_FACTOR_AETP; + ftp = int(pde.FTP) > 0 ? pde.FTP : cp; + if (useCPForFTPCombo->currentIndex() == 0) { + ftp = cp; + } + wprime = int(pde.WPrime) > 0 ? pde.WPrime : CPPAGE_DEFAULT_WPRIME; + pmax = int(pde.PMax) > 0 ? pde.PMax : CPPAGE_DEFAULT_PMAX; + if (pde.from <= date && date <= pde.to) { + estOffset = 0; + } else if (date > pde.to) { + estOffset = pde.to.daysTo(date); + } else if (date < pde.from) { + estOffset = pde.from.daysTo(date); + } + if (startDate != nullptr) { + *startDate = pde.from; + } + } + + bool ret = false; + if (cp > 0) { + ret = true; + } else if (allowDefaults) { + QTreeWidgetItem *sourceItem = nullptr; + if (ranges->currentItem() != nullptr) { + sourceItem = ranges->currentItem(); + } else if (ranges->invisibleRootItem()->childCount() > 0) { + sourceItem = ranges->invisibleRootItem()->child(ranges->invisibleRootItem()->childCount() - 1); + } + + if (sourceItem != nullptr) { + cp = sourceItem->data(CPPAGE_RANGES_COL_CP, Qt::DisplayRole).toInt(); + aetp = sourceItem->data(CPPAGE_RANGES_COL_AETP, Qt::DisplayRole).toInt(); + ftp = sourceItem->data(CPPAGE_RANGES_COL_FTP, Qt::DisplayRole).toInt(); + wprime = sourceItem->data(CPPAGE_RANGES_COL_WPRIME, Qt::DisplayRole).toInt(); + pmax = sourceItem->data(CPPAGE_RANGES_COL_PMAX, Qt::DisplayRole).toInt(); + } else { + cp = CPPAGE_DEFAULT_CP; + aetp = cp * CPPAGE_DEFAULT_FACTOR_AETP; + ftp = cp; + wprime = CPPAGE_DEFAULT_WPRIME; + pmax = CPPAGE_DEFAULT_PMAX; + } + estOffset = 0; + defaults = true; + if (startDate != nullptr) { + *startDate = QDate(); + } + ret = true; + } + return ret; +} + + +void +CPPage::setEstimateStatus +(QTreeWidgetItem *item) +{ + bool preActive = active; + active = true; + + QModelIndex modelIndex = ranges->indexFromItem(item); + + int cp = item->data(CPPAGE_RANGES_COL_CP, Qt::DisplayRole).toInt(); + int ftp = item->data(CPPAGE_RANGES_COL_FTP, Qt::DisplayRole).toInt(); + int wprime = item->data(CPPAGE_RANGES_COL_WPRIME, Qt::DisplayRole).toInt(); + int pmax = item->data(CPPAGE_RANGES_COL_PMAX, Qt::DisplayRole).toInt(); + int estCp = item->data(CPPAGE_RANGES_COL_EST_CP, Qt::DisplayRole).toInt(); + int estFtp = item->data(CPPAGE_RANGES_COL_EST_FTP, Qt::DisplayRole).toInt(); + int estWprime = item->data(CPPAGE_RANGES_COL_EST_WPRIME, Qt::DisplayRole).toInt(); + int estPmax = item->data(CPPAGE_RANGES_COL_EST_PMAX, Qt::DisplayRole).toInt(); + int estOffset = item->data(CPPAGE_RANGES_COL_EST_OFFSET, Qt::DisplayRole).toInt(); + QString fitText; + QString toolTipText = "
"; + if (estCp > 0) { + if ( ( useModel->currentIndex() == CPPAGE_EST_MODEL_CP2 + && std::abs(cp - estCp) <= CPPAGE_EST_TOLERANCE_CP + && std::abs(wprime - estWprime) <= CPPAGE_EST_TOLERANCE_WPRIME) + || ( useModel->currentIndex() == CPPAGE_EST_MODEL_CP3 + && std::abs(cp - estCp) <= CPPAGE_EST_TOLERANCE_CP + && std::abs(wprime - estWprime) <= CPPAGE_EST_TOLERANCE_WPRIME + && std::abs(pmax - estPmax) <= CPPAGE_EST_TOLERANCE_PMAX) + || ( useModel->currentIndex() == CPPAGE_EST_MODEL_EXT + && std::abs(cp - estCp) <= CPPAGE_EST_TOLERANCE_CP + && std::abs(wprime - estWprime) <= CPPAGE_EST_TOLERANCE_WPRIME + && std::abs(ftp - estFtp) <= CPPAGE_EST_TOLERANCE_CP + && std::abs(pmax - estPmax) <= CPPAGE_EST_TOLERANCE_PMAX)) { + setRangeData(modelIndex, CPPAGE_RANGES_COL_EST_DEVIATION, CPPAGE_RANGES_EST_MATCH); + fitText = tr("🗹"); + toolTipText += tr("Estimate and settings match"); + } else { + setRangeData(modelIndex, CPPAGE_RANGES_COL_EST_DEVIATION, CPPAGE_RANGES_EST_DEVIATE); + fitText = tr("☐"); + toolTipText += tr("Estimate and settings differ"); + } + if (estOffset != 0) { + fitText += " "; + fitText += tr("⏲"); + toolTipText += "
"; + if (estOffset < 0) { + toolTipText += tr("Range is %1 days older than closest estimate").arg(-1 * estOffset); + } else if (estOffset > 0) { + toolTipText += tr("Range is %1 days younger than closest estimate").arg(estOffset); + } + toolTipText += ""; + } + } else { + setRangeData(modelIndex, CPPAGE_RANGES_COL_EST_DEVIATION, CPPAGE_RANGES_EST_NA); + fitText = tr("∅"); + toolTipText += tr("No estimate available"); + } + toolTipText += "
"; + setRangeData(modelIndex, CPPAGE_RANGES_COL_MODELFIT, fitText); + item->setToolTip(CPPAGE_RANGES_COL_MODELFIT, toolTipText); + active = preActive; +} + + +void +CPPage::setRangeData +(QModelIndex modelIndex, int column, QVariant data) +{ + ranges->model()->setData(modelIndex.siblingAtColumn(column), data, Qt::DisplayRole); +} + + +bool +CPPage::needsNewRange +() const +{ + zones_->setScheme(schemePage->getScheme()); + if (useModel->currentIndex() == CPPAGE_EST_MODEL_NONE) { + return false; + } + + QDate today = QDate::currentDate(); + int todayEstCp = 0; + int todayEstAetp = 0; + int todayEstFtp = 0; + int todayEstWprime = 0; + int todayEstPmax = 0; + int todayEstEstOffset = 0; + bool defaults = false; + bool todayOk = getValuesFor(today, false, todayEstCp, todayEstAetp, todayEstFtp, todayEstWprime, todayEstPmax, todayEstEstOffset, defaults); + + int activeRange = zones_->whichRange(today); + if (activeRange < 0) { + return todayOk; + } + + int currentSetCp = zones_->getZoneRange(activeRange).cp; + int currentSetFtp = zones_->getZoneRange(activeRange).ftp; + int currentSetWprime = zones_->getZoneRange(activeRange).wprime; + int currentSetPmax = zones_->getZoneRange(activeRange).pmax; + + return todayOk + && ! ( ( useModel->currentIndex() == CPPAGE_EST_MODEL_CP2 + && std::abs(todayEstCp - currentSetCp) <= CPPAGE_EST_TOLERANCE_CP + && std::abs(todayEstWprime - currentSetWprime) <= CPPAGE_EST_TOLERANCE_WPRIME) + || ( useModel->currentIndex() == CPPAGE_EST_MODEL_CP3 + && std::abs(todayEstCp - currentSetCp) <= CPPAGE_EST_TOLERANCE_CP + && std::abs(todayEstWprime - currentSetWprime) <= CPPAGE_EST_TOLERANCE_WPRIME + && std::abs(todayEstPmax - currentSetPmax) <= CPPAGE_EST_TOLERANCE_PMAX) + || ( useModel->currentIndex() == CPPAGE_EST_MODEL_EXT + && std::abs(todayEstCp - currentSetCp) <= CPPAGE_EST_TOLERANCE_CP + && std::abs(todayEstWprime - currentSetWprime) <= CPPAGE_EST_TOLERANCE_WPRIME + && std::abs(todayEstFtp - currentSetFtp) <= CPPAGE_EST_TOLERANCE_CP + && std::abs(todayEstPmax - currentSetPmax) <= CPPAGE_EST_TOLERANCE_PMAX)); +} + void CPPage::addClicked() { - // get current scheme zones_->setScheme(schemePage->getScheme()); - int cp = cpEdit->value(); - if( cp <= 0 ) { - QMessageBox err; - err.setText(tr("Critical Power must be > 0")); - err.setIcon(QMessageBox::Warning); - err.exec(); + QDate date = QDate::currentDate(); + int cp = 0; + int aetp = 0; + int ftp = 0; + int wprime = 0; + int pmax = 0; + int estOffset = 0; + bool defaults = false; + QDate startDate; + bool ok = getValuesFor(date, true, cp, aetp, ftp, wprime, pmax, estOffset, defaults, &startDate); + if (ok && startDate.isValid() && startDate < date) { + date = startDate; + estOffset = 0; + } + if (ok && ! defaults && estOffset == 0) { + QString text; + if (useModel->currentData().toString() == "cp2") { + text = getText(CPPAGE_INFO_MODEL_CP2, pmax); + } else if (useModel->currentData().toString() == "cp3") { + text = getText(CPPAGE_INFO_MODEL_CP3); + } + text += getText(CPPAGE_INFO_AETP, CPPAGE_DEFAULT_FACTOR_AETP * 100); + QMessageBox::information(this, tr("New range from estimate"), text); + } else if (! addDialogManual(date, cp, aetp, wprime, ftp, pmax)) { return; } - int wp = wEdit->value() ? wEdit->value() : 20000; - if (wp < 1000) wp *= 1000; // entered in kJ we want joules + int index = zones_->addZoneRange(date, cp, aetp, ftp, wprime, pmax); - int pmax = pmaxEdit->value() ? pmaxEdit->value() : 1000; - - int index = zones_->addZoneRange(dateEdit->date(), cpEdit->value(), aetEdit->value(), ftpEdit->value(), wp, pmax); - - // new item - QTreeWidgetItem *add = new QTreeWidgetItem; - add->setFlags(add->flags() & ~Qt::ItemIsEditable); + QTreeWidgetItem *add = new QTreeWidgetItem(); + add->setFlags(add->flags() | Qt::ItemIsEditable); ranges->invisibleRootItem()->insertChild(index, add); + ranges->setCurrentItem(add); - int column = 0; - - // date - add->setText(column++, dateEdit->date().toString(tr("MMM d, yyyy"))); - - // CP - add->setText(column++, QString("%1").arg(cpEdit->value())); - - // AeT - add->setText(column++, QString("%1").arg(aetEdit->value())); - - // FTP - add->setText(column++, QString("%1").arg(ftpEdit->value())); - - // W' - add->setText(column++, QString("%1").arg(wp)); - - // Pmax - add->setText(column++, QString("%1").arg(pmax)); - -} - -void -CPPage::editClicked() -{ - // get current scheme - zones_->setScheme(schemePage->getScheme()); - - int cp = cpEdit->value(); - - if( cp <= 0 ){ - QMessageBox err; - err.setText(tr("Critical Power must be > 0")); - err.setIcon(QMessageBox::Warning); - err.exec(); - return; + int estCp = 0; + int estAetp = 0; + int estFtp = 0; + int estWprime = 0; + int estPmax = 0; + if (getValuesFor(date, false, estCp, estAetp, estFtp, estWprime, estPmax, estOffset, defaults)) { + add->setData(CPPAGE_RANGES_COL_EST_CP, Qt::DisplayRole, estCp); + add->setData(CPPAGE_RANGES_COL_EST_FTP, Qt::DisplayRole, estFtp); + add->setData(CPPAGE_RANGES_COL_EST_WPRIME, Qt::DisplayRole, estWprime); + add->setData(CPPAGE_RANGES_COL_EST_PMAX, Qt::DisplayRole, estPmax); + add->setData(CPPAGE_RANGES_COL_EST_OFFSET, Qt::DisplayRole, estOffset); + } else { + add->setData(CPPAGE_RANGES_COL_EST_CP, Qt::DisplayRole, 0); } - - int ftp = ftpEdit->value() ? ftpEdit->value() : cp; - int wp = wEdit->value() ? wEdit->value() : 20000; - if (wp < 1000) wp *= 1000; // entered in kJ we want joules - - int pmax = pmaxEdit->value() ? pmaxEdit->value() : 1000; - - QTreeWidgetItem *edit = ranges->selectedItems().at(0); - int index = ranges->indexOfTopLevelItem(edit); - - - int columns = 0; - - // date - zones_->setStartDate(index, dateEdit->date()); - edit->setText(columns++, dateEdit->date().toString(tr("MMM d, yyyy"))); - - // CP - zones_->setCP(index, cp); - edit->setText(columns++, QString("%1").arg(cp)); - - // AeT - zones_->setAeT(index, aetEdit->value()); - edit->setText(columns++, QString("%1").arg(aetEdit->value())); - - // show FTP if we use FTP for Coggan Metrics - zones_->setFTP(index, ftp); - edit->setText(columns++, QString("%1").arg(ftp)); - - // W' - zones_->setWprime(index, wp); - edit->setText(columns++, QString("%1").arg(wp)); - - // Pmax - zones_->setPmax(index, pmax); - edit->setText(columns++, QString("%1").arg(pmax)); - + bool wasBlocked = ranges->model()->blockSignals(true); // Prevent adoption-dialog + add->setData(CPPAGE_RANGES_COL_STARTDATE, Qt::DisplayRole, date); + ranges->model()->blockSignals(wasBlocked); + add->setData(CPPAGE_RANGES_COL_CP, Qt::DisplayRole, cp); + add->setData(CPPAGE_RANGES_COL_AETP, Qt::DisplayRole, aetp); + add->setData(CPPAGE_RANGES_COL_FTP, Qt::DisplayRole, ftp); + add->setData(CPPAGE_RANGES_COL_WPRIME, Qt::DisplayRole, wprime); + add->setData(CPPAGE_RANGES_COL_PMAX, Qt::DisplayRole, pmax); + updateButtons(); } + void CPPage::deleteClicked() { @@ -1465,6 +1879,8 @@ CPPage::deleteClicked() int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); delete ranges->invisibleRootItem()->takeChild(index); zones_->deleteRange(index); + newZoneRequired->setVisible(needsNewRange()); + updateButtons(); } } @@ -1472,17 +1888,19 @@ void CPPage::defaultClicked() { if (ranges->currentItem()) { - int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); ZoneRange current = zones_->getZoneRange(index); // unbold QFont font; font.setWeight(QFont::Normal); - ranges->currentItem()->setFont(0, font); - ranges->currentItem()->setFont(1, font); - ranges->currentItem()->setFont(2, font); - + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_STARTDATE, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_CP, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_AETP, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_FTP, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_WPRIME, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_PMAX, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_MODELFIT, font); // set the range to use defaults on the scheme page zones_->setScheme(schemePage->getScheme()); @@ -1493,39 +1911,10 @@ CPPage::defaultClicked() // update the zones display rangeSelectionChanged(); + updateButtons(); } } -void -CPPage::rangeEdited() -{ - if (ranges->currentItem()) { - int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); - - QDate date = dateEdit->date(); - QDate odate = zones_->getStartDate(index); - - int cp = cpEdit->value(); - int ocp = zones_->getCP(index); - - int aet = aetEdit->value(); - int oaet = zones_->getAeT(index); - - int ftp = ftpEdit->value(); - int oftp = zones_->getFTP(index); - - int wp = wEdit->value(); - int owp = zones_->getWprime(index); - - int pmax = pmaxEdit->value(); - int opmax = zones_->getPmax(index); - - if (date != odate || cp != ocp || aet != oaet || ftp != oftp || wp != owp || pmax != opmax) - updateButton->show(); - else - updateButton->hide(); - } -} void CPPage::rangeSelectionChanged() @@ -1533,61 +1922,42 @@ CPPage::rangeSelectionChanged() active = true; // wipe away current contents of zones - foreach (QTreeWidgetItem *item, zones->invisibleRootItem()->takeChildren()) { - delete zones->itemWidget(item, 2); - delete item; - } + zones->clear(); // fill with current details - if (ranges->currentItem()) { + QTreeWidgetItem *item = ranges->currentItem(); + if (item != nullptr) { + adoptButton->setVisible( useModel->currentIndex() != CPPAGE_EST_MODEL_NONE + && item->data(CPPAGE_RANGES_COL_EST_DEVIATION, Qt::DisplayRole).toInt() == CPPAGE_RANGES_EST_DEVIATE); - - int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); + int index = ranges->invisibleRootItem()->indexOfChild(item); ZoneRange current = zones_->getZoneRange(index); - dateEdit->setDate(zones_->getStartDate(index)); - cpEdit->setValue(zones_->getCP(index)); - aetEdit->setValue(zones_->getAeT(index)); - ftpEdit->setValue(zones_->getFTP(index)); - wEdit->setValue(zones_->getWprime(index)); - pmaxEdit->setValue(zones_->getPmax(index)); - if (current.zonesSetFromCP) { - // reapply the scheme in case it has been changed zones_->setScheme(schemePage->getScheme()); zones_->setZonesFromCP(index); current = zones_->getZoneRange(index); defaultButton->hide(); - - } else defaultButton->show(); + } else { + defaultButton->show(); + } for (int i=0; i< current.zones.count(); i++) { - QTreeWidgetItem *add = new QTreeWidgetItem(zones->invisibleRootItem()); add->setFlags(add->flags() | Qt::ItemIsEditable); - // tab name - add->setText(0, current.zones[i].name); - // field name - add->setText(1, current.zones[i].desc); - - // low - QDoubleSpinBox *loedit = new QDoubleSpinBox(this); - loedit->setMinimum(0); - loedit->setMaximum(1000); - loedit->setSingleStep(1.0); - loedit->setDecimals(0); - loedit->setValue(current.zones[i].lo); - zones->setItemWidget(add, 2, loedit); - connect(loedit, SIGNAL(valueChanged(double)), this, SLOT(zonesChanged())); + add->setData(0, Qt::DisplayRole, current.zones[i].name); + add->setData(1, Qt::DisplayRole, current.zones[i].desc); + add->setData(2, Qt::DisplayRole, current.zones[i].lo); } - for(int i = 0; i < zones->columnCount(); i++) - zones->resizeColumnToContents(i); + } else { + adoptButton->setVisible(false); } active = false; + updateButtons(); } void @@ -1596,15 +1966,6 @@ CPPage::addZoneClicked() // no range selected if (!ranges->currentItem()) return; - // are we at maximum already? - if (zones->invisibleRootItem()->childCount() == 10) { - QMessageBox err; - err.setText(tr("Maximum of 10 zones reached.")); - err.setIcon(QMessageBox::Warning); - err.exec(); - return; - } - active = true; int index = zones->invisibleRootItem()->childCount(); @@ -1612,33 +1973,27 @@ CPPage::addZoneClicked() QTreeWidgetItem *add = new QTreeWidgetItem; add->setFlags(add->flags() | Qt::ItemIsEditable); - QDoubleSpinBox *loedit = new QDoubleSpinBox(this); - loedit->setMinimum(0); - loedit->setMaximum(1000); - loedit->setSingleStep(1.0); - loedit->setDecimals(0); - loedit->setValue(100); - zones->invisibleRootItem()->insertChild(index, add); - zones->setItemWidget(add, 2, loedit); - connect(loedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); // Short QString text = tr("New"); for (int i=0; zones->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(0, text); + add->setData(0, Qt::DisplayRole, text); // long text = tr("New"); for (int i=0; zones->findItems(text, Qt::MatchExactly, 1).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(1, text); + add->setData(1, Qt::DisplayRole, text); + + add->setData(2, Qt::DisplayRole, 100); active = false; zonesChanged(); + updateButtons(); } void @@ -1656,6 +2011,7 @@ CPPage::deleteZoneClicked() active = false; zonesChanged(); + updateButtons(); } void @@ -1667,16 +2023,19 @@ CPPage::zonesChanged() if (active == false) { // get the current zone range if (ranges->currentItem()) { - int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); ZoneRange current = zones_->getZoneRange(index); // embolden that range on the list to show it has been edited QFont font; font.setWeight(QFont::Black); - ranges->currentItem()->setFont(0, font); - ranges->currentItem()->setFont(1, font); - ranges->currentItem()->setFont(2, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_STARTDATE, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_CP, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_AETP, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_FTP, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_WPRIME, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_PMAX, font); + ranges->currentItem()->setFont(CPPAGE_RANGES_COL_MODELFIT, font); // show the default button to undo defaultButton->show(); @@ -1688,9 +2047,9 @@ CPPage::zonesChanged() QList zoneinfos; for (int i=0; i< zones->invisibleRootItem()->childCount(); i++) { QTreeWidgetItem *item = zones->invisibleRootItem()->child(i); - zoneinfos << ZoneInfo(item->text(0), - item->text(1), - ((QDoubleSpinBox*)zones->itemWidget(item, 2))->value(), + zoneinfos << ZoneInfo(item->data(0, Qt::DisplayRole).toString(), + item->data(1, Qt::DisplayRole).toString(), + item->data(2, Qt::DisplayRole).toInt(), 0); } @@ -1712,6 +2071,235 @@ CPPage::zonesChanged() } } + +void +CPPage::mkAdoptionRow +(QGridLayout *grid, int row, int labelId, const QString &unit, int cur, int est, QCheckBox *&accept, const QString &infoText) const +{ + QFormLayout dummy; + + accept = new QCheckBox(); + accept->setVisible(cur != est); + + QLineEdit *current = new QLineEdit(QString("%1 %2").arg(cur).arg(unit)); + current->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + current->setAlignment(Qt::AlignCenter); + current->setReadOnly(true); + current->setEnabled(false); + + if (cur != est) { + QLabel *relation = new QLabel(); + relation->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + if (cur < est) { + relation->setText("<"); + } else if (cur > est) { + relation->setText(">"); + } + QLineEdit *estimate = new QLineEdit(QString("%1 %2").arg(est).arg(unit)); + estimate->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + estimate->setAlignment(Qt::AlignCenter); + estimate->setReadOnly(true); + estimate->setEnabled(true); + + grid->addWidget(current, row, 2); + grid->addWidget(relation, row, 3); + grid->addWidget(estimate, row, 4); + + connect( + accept, &QCheckBox::toggled, + [=](bool checked) { + current->setEnabled(! checked); + estimate->setEnabled(checked); + } + ); + } else { + grid->addWidget(current, row, 2, 1, 3); + } + + grid->addWidget(mkInfoLabel(labelId, infoText), row, 0, dummy.labelAlignment()); + grid->addWidget(accept, row, 5, Qt::AlignCenter); + + accept->setCheckState(cur != est ? Qt::Checked : Qt::Unchecked); +} + + +void +CPPage::connectAdoptionDialogApplyButton +(QVector checkboxes, QPushButton *applyButton) const +{ + bool hasChecked = false; + for (int i = 0; i < checkboxes.size(); ++i) { + if (checkboxes[i] != nullptr) { + hasChecked |= checkboxes[i]->isChecked(); + connect( + checkboxes[i], &QCheckBox::toggled, + [=](bool checked) { + bool anyChecked = checked; + int j = 0; + while (! anyChecked && j < checkboxes.size()) { + if (checkboxes[j] != nullptr && checkboxes[j]->isChecked()) { + anyChecked = true; + } + ++j; + } + applyButton->setEnabled(anyChecked); + } + ); + } + } + applyButton->setEnabled(hasChecked); +} + + +bool +CPPage::modelHasFtp +() const +{ + return useModel->currentIndex() == CPPAGE_EST_MODEL_EXT; +} + + +bool +CPPage::modelHasPmax +() const +{ + return useModel->currentIndex() == CPPAGE_EST_MODEL_CP3 + || useModel->currentIndex() == CPPAGE_EST_MODEL_EXT; +} + + +QWidget* +CPPage::mkInfoLabel +(int labelId, const QString &infoText) const +{ + QWidget *ret = nullptr; + if (infoText.isEmpty()) { + ret = new QLabel(getText(labelId)); + } else { + ret = new QWidget(); + + QLabel *info = new QLabel(); + info->setPixmap(QPixmap(":images/toolbar/info.png")); + info->setToolTip(infoText); + + QHBoxLayout *labelLayout = new QHBoxLayout(ret); + labelLayout->setContentsMargins(0, 0, 0, 0); + labelLayout->addWidget(new QLabel(getText(labelId))); + labelLayout->addWidget(info); + } + return ret; +} + + +QString +CPPage::getText +(int id, int value) const +{ + switch (id) { + case CPPAGE_INFO_AETP: + return tr("The proposed value for AeTP is a very rough estimate, assuming %1 % of CP. Usually it is determined by a
  • Metabolic test
  • Lactate ramp test
  • Run / Cycling ‘conversational’ test
  • Run Decoupling test
").arg(value); + case CPPAGE_INFO_FTP_CP: + return tr("Updating FTP internally to match CP"); + case CPPAGE_INFO_MODEL_FTP: + case CPPAGE_INFO_MODEL_CP3: + return tr("Your selected model does not deliver values for
  • FTP, using CP instead
"); + case CPPAGE_INFO_MODEL_PMAX: + return tr("Your selected model does not deliver values for PMax, using default value of %1 W instead").arg(value); + case CPPAGE_INFO_MODEL_CP2: + return tr("Your selected model does not deliver values for
  • FTP, using CP instead
  • PMax, assuming a default of %1 W
").arg(value); + case CPPAGE_LABEL_STARTDATE: + return tr("Start Date"); + case CPPAGE_LABEL_CP: + return tr("Critical Power (CP)"); + case CPPAGE_LABEL_AETP: + return tr("Aerobic Threshold Power (AeTP)"); + case CPPAGE_LABEL_FTP: + return tr("Functional Threshold Power (FTP)"); + case CPPAGE_LABEL_WPRIME: + return tr("Anaerobic Work Capacity (W')"); + case CPPAGE_LABEL_PMAX: + return tr("Maximum Power (PMax)"); + default: + return QString("UNKNOWN MESSAGE %1").arg(id); + } +} + + +bool +CPPage::addDialogManual +(QDate &date, int &cp, int &aetp, int &wprime, int &ftp, int &pmax) const +{ + QDialog dialog; + dialog.setWindowTitle(tr("Manual range")); + + QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Apply + | QDialogButtonBox::Discard); + connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), &dialog, SLOT(accept())); + connect(buttonBox->button(QDialogButtonBox::Discard), SIGNAL(clicked()), &dialog, SLOT(reject())); + + QDateEdit *dateEdit = new QDateEdit(date); + dateEdit->setCalendarPopup(true); + + QSpinBox *cpEdit = new QSpinBox(); + cpEdit->setRange(1, 999); + cpEdit->setSingleStep(1); + cpEdit->setSuffix(" " + tr("W")); + cpEdit->setValue(cp); + + QSpinBox *aetpEdit = new QSpinBox(); + aetpEdit->setRange(1, 999); + aetpEdit->setSingleStep(1); + aetpEdit->setSuffix(" " + tr("W")); + aetpEdit->setValue(aetp); + + QSpinBox *ftpEdit = nullptr; + if (useCPForFTPCombo->currentIndex() != 0) { + ftpEdit = new QSpinBox(); + ftpEdit->setRange(1, 999); + ftpEdit->setSingleStep(1); + ftpEdit->setSuffix(" " + tr("W")); + ftpEdit->setValue(ftp); + } + + QSpinBox *wprimeEdit = new QSpinBox(); + wprimeEdit->setRange(1, 99999); + wprimeEdit->setSingleStep(100); + wprimeEdit->setSuffix(" " + tr("J")); + wprimeEdit->setValue(wprime); + + QSpinBox *pmaxEdit = new QSpinBox(); + pmaxEdit->setRange(1, 9999); + pmaxEdit->setSingleStep(10); + pmaxEdit->setSuffix(" " + tr("W")); + pmaxEdit->setValue(pmax); + + QFormLayout *form = new QFormLayout(&dialog); + form->addRow(getText(CPPAGE_LABEL_STARTDATE), dateEdit); + form->addRow(getText(CPPAGE_LABEL_CP), cpEdit); + form->addRow(getText(CPPAGE_LABEL_AETP), aetpEdit); + if (ftpEdit != nullptr) { + form->addRow(getText(CPPAGE_LABEL_FTP), ftpEdit); + } + form->addRow(getText(CPPAGE_LABEL_WPRIME), wprimeEdit); + form->addRow(getText(CPPAGE_LABEL_PMAX), pmaxEdit); + form->addItem(new QSpacerItem(1, 30 * dpiYFactor)); + form->addRow(buttonBox); + + if (dialog.exec() == QDialog::Accepted) { + date = dateEdit->date(); + cp = cpEdit->value(); + aetp = aetpEdit->value(); + ftp = ftpEdit != nullptr ? ftpEdit->value() : cp; + wprime = wprimeEdit->value(); + pmax = pmaxEdit->value(); + return true; + } else { + return false; + } +} + + + // // Zone Config page // @@ -1818,50 +2406,44 @@ HrSchemePage::HrSchemePage(HrZones *hrZones) : hrZones(hrZones) actionButtons->addWidget(addButton); actionButtons->addWidget(deleteButton); + ltDelegate.setRange(0, 1000); + ltDelegate.setSuffix(tr("%")); + ltDelegate.setShowSuffixOnEdit(true); + ltDelegate.setShowSuffixOnDisplay(true); + + trimpkDelegate.setRange(0, 10); + trimpkDelegate.setSingleStep(0.1); + trimpkDelegate.setDecimals(2); + trimpkDelegate.setShowSuffixOnEdit(false); + trimpkDelegate.setShowSuffixOnDisplay(false); + scheme = new QTreeWidget; scheme->headerItem()->setText(0, tr("Short")); scheme->headerItem()->setText(1, tr("Long")); scheme->headerItem()->setText(2, tr("Percent of LT")); scheme->headerItem()->setText(3, tr("Trimp k")); scheme->setColumnCount(4); - scheme->setSelectionMode(QAbstractItemView::SingleSelection); - scheme->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit - scheme->setUniformRowHeights(true); - scheme->setIndentation(0); - //scheme->header()->resizeSection(0,60); - //scheme->header()->resizeSection(1,180); - //scheme->header()->resizeSection(2,65); - //scheme->header()->resizeSection(3,65); + scheme->setItemDelegateForColumn(2, <Delegate); + scheme->setItemDelegateForColumn(3, &trimpkDelegate); + basicTreeWidgetStyle(scheme); // setup list for (int i=0; i< hrZones->getScheme().nzones_default; i++) { - QTreeWidgetItem *add = new QTreeWidgetItem(scheme->invisibleRootItem()); add->setFlags(add->flags() | Qt::ItemIsEditable); // tab name - add->setText(0, hrZones->getScheme().zone_default_name[i]); + add->setData(0, Qt::DisplayRole, hrZones->getScheme().zone_default_name[i]); // field name - add->setText(1, hrZones->getScheme().zone_default_desc[i]); + add->setData(1, Qt::DisplayRole, hrZones->getScheme().zone_default_desc[i]); // low - QDoubleSpinBox *loedit = new QDoubleSpinBox(this); - loedit->setMinimum(0); - loedit->setMaximum(1000); - loedit->setSingleStep(1.0); - loedit->setDecimals(0); - loedit->setValue(hrZones->getScheme().zone_default[i]); - scheme->setItemWidget(add, 2, loedit); + add->setData(2, Qt::DisplayRole, hrZones->getScheme().zone_default[i]); // trimp - QDoubleSpinBox *trimpedit = new QDoubleSpinBox(this); - trimpedit->setMinimum(0); - trimpedit->setMaximum(10); - trimpedit->setSingleStep(0.1); - trimpedit->setDecimals(2); - trimpedit->setValue(hrZones->getScheme().zone_default_trimp[i]); - scheme->setItemWidget(add, 3, trimpedit); + add->setData(3, Qt::DisplayRole, hrZones->getScheme().zone_default_trimp[i]); } + updateButtons(); mainLayout->addWidget(scheme); mainLayout->addLayout(actionButtons); @@ -1869,66 +2451,41 @@ HrSchemePage::HrSchemePage(HrZones *hrZones) : hrZones(hrZones) // button connect connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); + connect(scheme, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), this, SLOT(updateButtons())); } + void HrSchemePage::addClicked() { - // are we at maximum already? - if (scheme->invisibleRootItem()->childCount() == 10) { - QMessageBox err; - err.setText(tr("Maximum of 10 zones reached.")); - err.setIcon(QMessageBox::Warning); - err.exec(); - return; - } - int index = scheme->invisibleRootItem()->childCount(); // new item QTreeWidgetItem *add = new QTreeWidgetItem; add->setFlags(add->flags() | Qt::ItemIsEditable); - - QDoubleSpinBox *loedit = new QDoubleSpinBox(this); - loedit->setMinimum(0); - loedit->setMaximum(1000); - loedit->setSingleStep(1.0); - loedit->setDecimals(0); - loedit->setValue(100); - scheme->invisibleRootItem()->insertChild(index, add); - scheme->setItemWidget(add, 2, loedit); - - //trimp - QDoubleSpinBox *trimpedit = new QDoubleSpinBox(this); - trimpedit->setMinimum(0); - trimpedit->setMaximum(10); - trimpedit->setSingleStep(0.1); - trimpedit->setDecimals(2); - trimpedit->setValue(1); - - scheme->setItemWidget(add, 3, trimpedit); // Short QString text = tr("New"); for (int i=0; scheme->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(0, text); + add->setData(0, Qt::DisplayRole, text); // long text = tr("New"); for (int i=0; scheme->findItems(text, Qt::MatchExactly, 1).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(1, text); -} + add->setData(1, Qt::DisplayRole, text); -void -HrSchemePage::renameClicked() -{ - // which one is selected? - if (scheme->currentItem()) scheme->editItem(scheme->currentItem(), 0); + // lo + add->setData(2, Qt::DisplayRole, 100); + + // trimp + add->setData(3, Qt::DisplayRole, 1); + + updateButtons(); } void @@ -1937,9 +2494,19 @@ HrSchemePage::deleteClicked() if (scheme->currentItem()) { int index = scheme->invisibleRootItem()->indexOfChild(scheme->currentItem()); delete scheme->invisibleRootItem()->takeChild(index); + updateButtons(); } } +void +HrSchemePage::updateButtons +() +{ + addButton->setEnabled(scheme->invisibleRootItem()->childCount() < 10); + deleteButton->setEnabled(scheme->invisibleRootItem()->childCount() > 0 && scheme->currentItem() != nullptr); +} + + HrZoneScheme HrSchemePage::getScheme() { @@ -1949,12 +2516,11 @@ HrSchemePage::getScheme() // read back the details from the table for (int i=0; iinvisibleRootItem()->childCount(); i++) { - schemeitem add; - add.name = scheme->invisibleRootItem()->child(i)->text(0); - add.desc = scheme->invisibleRootItem()->child(i)->text(1); - add.lo = ((QDoubleSpinBox *)(scheme->itemWidget(scheme->invisibleRootItem()->child(i), 2)))->value(); - add.trimp = ((QDoubleSpinBox *)(scheme->itemWidget(scheme->invisibleRootItem()->child(i), 3)))->value(); + add.name = scheme->invisibleRootItem()->child(i)->data(0, Qt::DisplayRole).toString(); + add.desc = scheme->invisibleRootItem()->child(i)->data(1, Qt::DisplayRole).toString(); + add.lo = scheme->invisibleRootItem()->child(i)->data(2, Qt::DisplayRole).toInt(); + add.trimp = scheme->invisibleRootItem()->child(i)->data(3, Qt::DisplayRole).toDouble(); table.append(add); } @@ -1981,19 +2547,15 @@ LTPage::LTPage(Context *context, HrZones *hrZones, HrSchemePage *schemePage) : { active = false; - QGridLayout *mainLayout = new QGridLayout(this); + QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setSpacing(10 *dpiXFactor); - updateButton = new QPushButton(tr("Update")); - updateButton->hide(); addButton = new QPushButton(tr("+")); deleteButton = new QPushButton(tr("-")); #ifndef Q_OS_MAC addButton->setFixedSize(20*dpiXFactor,20*dpiYFactor); - updateButton->setFixedSize(60,20); deleteButton->setFixedSize(20*dpiXFactor,20*dpiYFactor); #else - updateButton->setText(tr("Update")); addButton->setText(tr("Add")); deleteButton->setText(tr("Delete")); #endif @@ -2012,109 +2574,87 @@ LTPage::LTPage(Context *context, HrZones *hrZones, HrSchemePage *schemePage) : QHBoxLayout *zoneButtons = new QHBoxLayout; zoneButtons->addStretch(); - zoneButtons->addWidget(addZoneButton); - zoneButtons->addWidget(deleteZoneButton); + zoneButtons->setSpacing(2 * dpiXFactor); zoneButtons->addWidget(defaultButton); + zoneButtons->addItem(new QSpacerItem(5 * dpiXFactor, defaultButton->sizeHint().height())); + zoneButtons->addWidget(addZoneButton, 0, Qt::AlignHCenter | Qt::AlignTop); + zoneButtons->addWidget(deleteZoneButton, 0, Qt::AlignHCenter | Qt::AlignTop); - QHBoxLayout *addLayout = new QHBoxLayout; - QLabel *dateLabel = new QLabel(tr("From Date")); - dateEdit = new QDateEdit; - dateEdit->setDate(QDate::currentDate()); - dateEdit->setCalendarPopup(true); + dateDelegate.setCalendarPopup(true); - addLayout->addWidget(dateLabel); - addLayout->addWidget(dateEdit); - addLayout->addStretch(); + ltDelegate.setRange(1, 240); + ltDelegate.setSuffix(tr("bpm")); + ltDelegate.setShowSuffixOnEdit(true); + ltDelegate.setShowSuffixOnDisplay(true); - QHBoxLayout *addLayout2 = new QHBoxLayout; - QLabel *ltLabel = new QLabel(tr("Lactate Threshold")); - QLabel *aetLabel = new QLabel(tr("Aerobic Threshold")); - QLabel *restHrLabel = new QLabel(tr("Rest HR")); - QLabel *maxHrLabel = new QLabel(tr("Max HR")); + aetDelegate.setRange(0, 240); + aetDelegate.setSuffix(tr("bpm")); + aetDelegate.setShowSuffixOnEdit(true); + aetDelegate.setShowSuffixOnDisplay(true); - ltEdit = new QDoubleSpinBox; - ltEdit->setMinimum(0); - ltEdit->setMaximum(240); - ltEdit->setSingleStep(1.0); - ltEdit->setDecimals(0); + restHrDelegate.setRange(0, 240); + restHrDelegate.setSuffix(tr("bpm")); + restHrDelegate.setShowSuffixOnEdit(true); + restHrDelegate.setShowSuffixOnDisplay(true); - aetEdit = new QDoubleSpinBox; - aetEdit->setMinimum(0); - aetEdit->setMaximum(240); - aetEdit->setSingleStep(1.0); - aetEdit->setDecimals(0); - - restHrEdit = new QDoubleSpinBox; - restHrEdit->setMinimum(0); - restHrEdit->setMaximum(240); - restHrEdit->setSingleStep(1.0); - restHrEdit->setDecimals(0); - - maxHrEdit = new QDoubleSpinBox; - maxHrEdit->setMinimum(0); - maxHrEdit->setMaximum(240); - maxHrEdit->setSingleStep(1.0); - maxHrEdit->setDecimals(0); + maxHrDelegate.setRange(0, 240); + maxHrDelegate.setSuffix(tr("bpm")); + maxHrDelegate.setShowSuffixOnEdit(true); + maxHrDelegate.setShowSuffixOnDisplay(true); QHBoxLayout *actionButtons = new QHBoxLayout; actionButtons->setSpacing(2 *dpiXFactor); - actionButtons->addWidget(ltLabel); - actionButtons->addWidget(ltEdit); - actionButtons->addWidget(aetLabel); - actionButtons->addWidget(aetEdit); - actionButtons->addWidget(restHrLabel); - actionButtons->addWidget(restHrEdit); - actionButtons->addWidget(maxHrLabel); - actionButtons->addWidget(maxHrEdit); actionButtons->addStretch(); - actionButtons->addWidget(updateButton); - actionButtons->addWidget(addButton); - actionButtons->addWidget(deleteButton); + actionButtons->addItem(new QSpacerItem(5 * dpiXFactor, defaultButton->sizeHint().height())); + actionButtons->addWidget(addButton, 0, Qt::AlignHCenter | Qt::AlignTop); + actionButtons->addWidget(deleteButton, 0, Qt::AlignHCenter | Qt::AlignTop); ranges = new QTreeWidget; - ranges->headerItem()->setText(0, tr("From Date")); + ranges->headerItem()->setText(0, tr("Start Date")); ranges->headerItem()->setText(1, tr("Lactate Threshold")); ranges->headerItem()->setText(2, tr("Aerobic Threshold")); ranges->headerItem()->setText(3, tr("Rest HR")); ranges->headerItem()->setText(4, tr("Max HR")); ranges->setColumnCount(5); - ranges->setSelectionMode(QAbstractItemView::SingleSelection); - ranges->setUniformRowHeights(true); - ranges->setIndentation(0); + ranges->setItemDelegateForColumn(0, &dateDelegate); + ranges->setItemDelegateForColumn(1, <Delegate); + ranges->setItemDelegateForColumn(2, &aetDelegate); + ranges->setItemDelegateForColumn(3, &restHrDelegate); + ranges->setItemDelegateForColumn(4, &maxHrDelegate); + basicTreeWidgetStyle(ranges); // setup list of ranges - for (int i=0; i< hrZones->getRangeSize(); i++) { - + for (int i = 0; i < hrZones->getRangeSize(); i++) { QTreeWidgetItem *add = new QTreeWidgetItem(ranges->invisibleRootItem()); - add->setFlags(add->flags() & ~Qt::ItemIsEditable); + add->setFlags(add->flags() | Qt::ItemIsEditable); // Embolden ranges with manually configured zones QFont font; font.setWeight(hrZones->getHrZoneRange(i).hrZonesSetFromLT ? QFont::Normal : QFont::Black); - // date - add->setText(0, hrZones->getStartDate(i).toString(tr("MMM d, yyyy"))); + add->setData(0, Qt::DisplayRole, hrZones->getStartDate(i)); add->setFont(0, font); - - // LT - add->setText(1, QString("%1").arg(hrZones->getLT(i))); + add->setData(1, Qt::DisplayRole, hrZones->getLT(i)); add->setFont(1, font); - - // AeT - add->setText(2, QString("%1").arg(hrZones->getAeT(i))); + add->setData(2, Qt::DisplayRole, hrZones->getAeT(i)); add->setFont(2, font); - - // Rest HR - add->setText(3, QString("%1").arg(hrZones->getRestHr(i))); + add->setData(3, Qt::DisplayRole, hrZones->getRestHr(i)); add->setFont(3, font); - - // Max HR - add->setText(4, QString("%1").arg(hrZones->getMaxHr(i))); + add->setData(4, Qt::DisplayRole, hrZones->getMaxHr(i)); add->setFont(4, font); } - for(int i = 0; i < ranges->columnCount(); i++) - ranges->resizeColumnToContents(i); + + zoneLoDelegate.setRange(0, 240); + zoneLoDelegate.setSuffix(tr("bpm")); + zoneLoDelegate.setShowSuffixOnEdit(true); + zoneLoDelegate.setShowSuffixOnDisplay(true); + + zoneTrimpDelegate.setRange(0, 10); + zoneTrimpDelegate.setSingleStep(0.1); + zoneTrimpDelegate.setDecimals(2); + zoneTrimpDelegate.setShowSuffixOnEdit(false); + zoneTrimpDelegate.setShowSuffixOnDisplay(false); zones = new QTreeWidget; zones->headerItem()->setText(0, tr("Short")); @@ -2122,103 +2662,115 @@ LTPage::LTPage(Context *context, HrZones *hrZones, HrSchemePage *schemePage) : zones->headerItem()->setText(2, tr("From BPM")); zones->headerItem()->setText(3, tr("Trimp k")); zones->setColumnCount(4); - zones->setSelectionMode(QAbstractItemView::SingleSelection); - zones->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit - zones->setUniformRowHeights(true); - zones->setIndentation(0); + zones->setItemDelegateForColumn(2, &zoneLoDelegate); + zones->setItemDelegateForColumn(3, &zoneTrimpDelegate); + basicTreeWidgetStyle(zones); - mainLayout->addLayout(addLayout, 0, 0); - mainLayout->addLayout(actionButtons, 1, 0); - mainLayout->addWidget(ranges, 2, 0); - mainLayout->addLayout(zoneButtons, 3, 0); - mainLayout->addWidget(zones, 4, 0); + mainLayout->addWidget(ranges); + mainLayout->addLayout(actionButtons); + mainLayout->addWidget(zones); + mainLayout->addLayout(zoneButtons); - // edit connect - connect(dateEdit, SIGNAL(dateChanged(QDate)), this, SLOT(rangeEdited())); - connect(ltEdit, SIGNAL(valueChanged(double)), this, SLOT(rangeEdited())); - connect(aetEdit, SIGNAL(valueChanged(double)), this, SLOT(rangeEdited())); - connect(restHrEdit, SIGNAL(valueChanged(double)), this, SLOT(rangeEdited())); - connect(maxHrEdit, SIGNAL(valueChanged(double)), this, SLOT(rangeEdited())); + updateButtons(); // button connect - connect(updateButton, SIGNAL(clicked()), this, SLOT(editClicked())); connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); connect(defaultButton, SIGNAL(clicked()), this, SLOT(defaultClicked())); connect(addZoneButton, SIGNAL(clicked()), this, SLOT(addZoneClicked())); connect(deleteZoneButton, SIGNAL(clicked()), this, SLOT(deleteZoneClicked())); + connect(ranges, SIGNAL(itemSelectionChanged()), this, SLOT(rangeSelectionChanged())); + connect(ranges, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), this, SLOT(updateButtons())); + connect(ranges->model(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(rangeChanged(const QModelIndex&))); connect(zones, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(zonesChanged())); + connect(zones, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), this, SLOT(updateButtons())); + connect(zones->model(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(zonesChanged())); } + void LTPage::addClicked() { - if(ltEdit->value() <= 0 ) { - QMessageBox err; - err.setText(tr("Lactate Threshold must be > 0")); - err.setIcon(QMessageBox::Warning); - err.exec(); + QDialog dialog; + + int lt = 1; + int aet = 0; + int restHr = 0; + int maxHr = 0; + + int index = -1; + if (ranges->currentItem()) { + index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); + } else { + index = ranges->invisibleRootItem()->childCount() - 1; + } + if (index >= 0) { + lt = hrZones->getLT(index); + aet = hrZones->getAeT(index); + maxHr = hrZones->getMaxHr(index); + restHr = hrZones->getRestHr(index); + } + + QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Apply + | QDialogButtonBox::Discard); + connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), &dialog, SLOT(accept())); + connect(buttonBox->button(QDialogButtonBox::Discard), SIGNAL(clicked()), &dialog, SLOT(reject())); + + QDateEdit *dateEdit = new QDateEdit(QDate::currentDate()); + dateEdit->setCalendarPopup(true); + + QSpinBox *ltEdit = new QSpinBox(); + ltEdit->setRange(1, 240); + ltEdit->setSuffix(" " + tr("bpm")); + ltEdit->setValue(lt); + + QSpinBox *aetEdit = new QSpinBox(); + aetEdit->setRange(0, 240); + aetEdit->setSuffix(" " + tr("bpm")); + aetEdit->setValue(aet); + + QSpinBox *restHrEdit = new QSpinBox(); + restHrEdit->setRange(0, 240); + restHrEdit->setSuffix(" " + tr("bpm")); + restHrEdit->setValue(restHr); + + QSpinBox *maxHrEdit = new QSpinBox(); + maxHrEdit->setRange(0, 240); + maxHrEdit->setSuffix(" " + tr("bpm")); + maxHrEdit->setValue(maxHr); + + QFormLayout *form = new QFormLayout(&dialog); + form->addRow(tr("Start Date"), dateEdit); + form->addRow(tr("Lactate Threshold"), ltEdit); + form->addRow(tr("Aerobic Threshold"), aetEdit); + form->addRow(tr("Rest HR"), restHrEdit); + form->addRow(tr("Max HR"), maxHrEdit); + form->addRow(buttonBox); + + int dialogRet = dialog.exec(); + if (dialogRet != QDialog::Accepted) { return; } // get current scheme hrZones->setScheme(schemePage->getScheme()); - int index = hrZones->addHrZoneRange(dateEdit->date(), ltEdit->value(), aetEdit->value(), restHrEdit->value(), maxHrEdit->value()); + index = hrZones->addHrZoneRange(dateEdit->date(), ltEdit->value(), aetEdit->value(), restHrEdit->value(), maxHrEdit->value()); // new item QTreeWidgetItem *add = new QTreeWidgetItem; - add->setFlags(add->flags() & ~Qt::ItemIsEditable); + add->setFlags(add->flags() | Qt::ItemIsEditable); ranges->invisibleRootItem()->insertChild(index, add); - // date - add->setText(0, dateEdit->date().toString(tr("MMM d, yyyy"))); - - // LT - add->setText(1, QString("%1").arg(ltEdit->value())); - // AeT - add->setText(2, QString("%1").arg(aetEdit->value())); - // Rest HR - add->setText(3, QString("%1").arg(restHrEdit->value())); - // Max HR - add->setText(4, QString("%1").arg(maxHrEdit->value())); + add->setData(0, Qt::DisplayRole, dateEdit->date()); + add->setData(1, Qt::DisplayRole, ltEdit->value()); + add->setData(2, Qt::DisplayRole, aetEdit->value()); + add->setData(3, Qt::DisplayRole, restHrEdit->value()); + add->setData(4, Qt::DisplayRole, maxHrEdit->value()); + updateButtons(); } -void -LTPage::editClicked() -{ - if(ltEdit->value() <= 0 ) { - QMessageBox err; - err.setText(tr("Lactate Threshold must be > 0")); - err.setIcon(QMessageBox::Warning); - err.exec(); - return; - } - - // get current scheme - hrZones->setScheme(schemePage->getScheme()); - - QTreeWidgetItem *edit = ranges->selectedItems().at(0); - int index = ranges->indexOfTopLevelItem(edit); - - // date - hrZones->setStartDate(index, dateEdit->date()); - edit->setText(0, dateEdit->date().toString(tr("MMM d, yyyy"))); - - // LT - hrZones->setLT(index, ltEdit->value()); - edit->setText(1, QString("%1").arg(ltEdit->value())); - // AeT - hrZones->setAeT(index, aetEdit->value()); - edit->setText(2, QString("%1").arg(aetEdit->value())); - // Rest HR - hrZones->setRestHr(index, restHrEdit->value()); - edit->setText(3, QString("%1").arg(restHrEdit->value())); - // Max HR - hrZones->setMaxHr(index, maxHrEdit->value()); - edit->setText(4, QString("%1").arg(maxHrEdit->value())); -} void LTPage::deleteClicked() @@ -2227,6 +2779,7 @@ LTPage::deleteClicked() int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); delete ranges->invisibleRootItem()->takeChild(index); hrZones->deleteRange(index); + updateButtons(); } } @@ -2234,7 +2787,6 @@ void LTPage::defaultClicked() { if (ranges->currentItem()) { - int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); HrZoneRange current = hrZones->getHrZoneRange(index); @@ -2245,7 +2797,7 @@ LTPage::defaultClicked() ranges->currentItem()->setFont(1, font); ranges->currentItem()->setFont(2, font); ranges->currentItem()->setFont(3, font); - + ranges->currentItem()->setFont(4, font); // set the range to use defaults on the scheme page hrZones->setScheme(schemePage->getScheme()); @@ -2256,109 +2808,79 @@ LTPage::defaultClicked() // update the zones display rangeSelectionChanged(); + updateButtons(); } } + void -LTPage::rangeEdited() +LTPage::rangeChanged +(const QModelIndex &modelIndex) { - if (ranges->currentItem()) { - int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); - - QDate date = dateEdit->date(); - QDate odate = hrZones->getStartDate(index); - - int lt = ltEdit->value(); - int olt = hrZones->getLT(index); - - int aet = aetEdit->value(); - int oaet = hrZones->getAeT(index); - - int maxhr = maxHrEdit->value(); - int omaxhr = hrZones->getMaxHr(index); - - int resthr = restHrEdit->value(); - int oresthr = hrZones->getRestHr(index); - - if (date != odate || lt != olt || aet != oaet || maxhr != omaxhr || resthr != oresthr) - updateButton->show(); - else - updateButton->hide(); + hrZones->setScheme(schemePage->getScheme()); + int index = modelIndex.row(); + switch (modelIndex.column()) { + case 0: + hrZones->setStartDate(index, modelIndex.data().toDate()); + break; + case 1: + hrZones->setLT(index, modelIndex.data().toInt()); + break; + case 2: + hrZones->setAeT(index, modelIndex.data().toInt()); + break; + case 3: + hrZones->setRestHr(index, modelIndex.data().toInt()); + break; + case 4: + hrZones->setMaxHr(index, modelIndex.data().toInt()); + break; + default: + break; } + rangeSelectionChanged(); } + void LTPage::rangeSelectionChanged() { active = true; // wipe away current contents of zones - foreach (QTreeWidgetItem *item, zones->invisibleRootItem()->takeChildren()) { - delete zones->itemWidget(item, 2); - delete item; - } + zones->clear(); // fill with current details if (ranges->currentItem()) { - int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); HrZoneRange current = hrZones->getHrZoneRange(index); - dateEdit->setDate(hrZones->getStartDate(index)); - ltEdit->setValue(hrZones->getLT(index)); - aetEdit->setValue(hrZones->getAeT(index)); - maxHrEdit->setValue(hrZones->getMaxHr(index)); - restHrEdit->setValue(hrZones->getRestHr(index)); - if (current.hrZonesSetFromLT) { - // reapply the scheme in case it has been changed hrZones->setScheme(schemePage->getScheme()); hrZones->setHrZonesFromLT(index); current = hrZones->getHrZoneRange(index); defaultButton->hide(); - - } else defaultButton->show(); + } else { + defaultButton->show(); + } for (int i=0; i< current.zones.count(); i++) { - QTreeWidgetItem *add = new QTreeWidgetItem(zones->invisibleRootItem()); add->setFlags(add->flags() | Qt::ItemIsEditable); - // tab name - add->setText(0, current.zones[i].name); - // field name - add->setText(1, current.zones[i].desc); - - // low - QDoubleSpinBox *loedit = new QDoubleSpinBox(this); - loedit->setMinimum(0); - loedit->setMaximum(1000); - loedit->setSingleStep(1.0); - loedit->setDecimals(0); - loedit->setValue(current.zones[i].lo); - zones->setItemWidget(add, 2, loedit); - connect(loedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); - - //trimp - QDoubleSpinBox *trimpedit = new QDoubleSpinBox(this); - trimpedit->setMinimum(0); - trimpedit->setMaximum(10); - trimpedit->setSingleStep(0.1); - trimpedit->setDecimals(2); - trimpedit->setValue(current.zones[i].trimp); - zones->setItemWidget(add, 3, trimpedit); - connect(trimpedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); - + add->setData(0, Qt::DisplayRole, current.zones[i].name); + add->setData(1, Qt::DisplayRole, current.zones[i].desc); + add->setData(2, Qt::DisplayRole, current.zones[i].lo); + add->setData(3, Qt::DisplayRole, current.zones[i].trimp); } - for(int i = 0; i < zones->columnCount(); i++) - zones->resizeColumnToContents(i); } active = false; } + void LTPage::addZoneClicked() { @@ -2381,43 +2903,29 @@ LTPage::addZoneClicked() QTreeWidgetItem *add = new QTreeWidgetItem; add->setFlags(add->flags() | Qt::ItemIsEditable); - QDoubleSpinBox *loedit = new QDoubleSpinBox(this); - loedit->setMinimum(0); - loedit->setMaximum(1000); - loedit->setSingleStep(1.0); - loedit->setDecimals(0); - loedit->setValue(100); - zones->invisibleRootItem()->insertChild(index, add); - zones->setItemWidget(add, 2, loedit); - connect(loedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); - - //trimp - QDoubleSpinBox *trimpedit = new QDoubleSpinBox(this); - trimpedit->setMinimum(0); - trimpedit->setMaximum(10); - trimpedit->setSingleStep(0.1); - trimpedit->setDecimals(2); - trimpedit->setValue(1); - zones->setItemWidget(add, 3, trimpedit); - connect(trimpedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); // Short QString text = tr("New"); for (int i=0; zones->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(0, text); + add->setData(0, Qt::DisplayRole, text); // long text = tr("New"); for (int i=0; zones->findItems(text, Qt::MatchExactly, 1).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(1, text); + add->setData(1, Qt::DisplayRole, text); + + add->setData(2, Qt::DisplayRole, 0); + add->setData(2, Qt::DisplayRole, 0); + active = false; zonesChanged(); + updateButtons(); } void @@ -2435,6 +2943,7 @@ LTPage::deleteZoneClicked() active = false; zonesChanged(); + updateButtons(); } void @@ -2446,17 +2955,19 @@ LTPage::zonesChanged() if (active == false) { // get the current zone range if (ranges->currentItem()) { - int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); HrZoneRange current = hrZones->getHrZoneRange(index); // embolden that range on the list to show it has been edited QFont font; font.setWeight(QFont::Black); + ranges->model()->blockSignals(true); ranges->currentItem()->setFont(0, font); ranges->currentItem()->setFont(1, font); ranges->currentItem()->setFont(2, font); ranges->currentItem()->setFont(3, font); + ranges->currentItem()->setFont(4, font); + ranges->model()->blockSignals(false); // show the default button to undo defaultButton->show(); @@ -2468,10 +2979,11 @@ LTPage::zonesChanged() QList zoneinfos; for (int i=0; i< zones->invisibleRootItem()->childCount(); i++) { QTreeWidgetItem *item = zones->invisibleRootItem()->child(i); - zoneinfos << HrZoneInfo(item->text(0), - item->text(1), - ((QDoubleSpinBox*)zones->itemWidget(item, 2))->value(), - 0, ((QDoubleSpinBox*)zones->itemWidget(item, 3))->value()); + zoneinfos << HrZoneInfo(item->data(0, Qt::DisplayRole).toString(), + item->data(1, Qt::DisplayRole).toString(), + item->data(2, Qt::DisplayRole).toInt(), + 0, + item->data(3, Qt::DisplayRole).toDouble()); } // now sort the list @@ -2489,9 +3001,22 @@ LTPage::zonesChanged() // now replace the current range struct hrZones->setHrZoneRange(index, current); } + updateButtons(); } } + +void +LTPage::updateButtons +() +{ + addButton->setEnabled(true); + deleteButton->setEnabled(ranges->currentItem() != nullptr); + addZoneButton->setEnabled(zones->invisibleRootItem()->childCount() < 10 && ranges->currentItem() != nullptr); + deleteZoneButton->setEnabled(zones->invisibleRootItem()->childCount() > 0 && zones->currentItem() != nullptr); +} + + // // Pace Zone Config page // @@ -2587,39 +3112,34 @@ PaceSchemePage::PaceSchemePage(PaceZones* paceZones) : paceZones(paceZones) actionButtons->addWidget(addButton); actionButtons->addWidget(deleteButton); + fromDelegate.setRange(0, 1000); + fromDelegate.setSingleStep(1); + fromDelegate.setSuffix(tr("%")); + fromDelegate.setShowSuffixOnEdit(true); + fromDelegate.setShowSuffixOnDisplay(true); + scheme = new QTreeWidget; scheme->headerItem()->setText(0, tr("Short")); scheme->headerItem()->setText(1, tr("Long")); scheme->headerItem()->setText(2, tr("Percent of CV")); scheme->setColumnCount(3); - scheme->setSelectionMode(QAbstractItemView::SingleSelection); - scheme->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit - scheme->setUniformRowHeights(true); - scheme->setIndentation(0); - //scheme->header()->resizeSection(0,90); - //scheme->header()->resizeSection(1,200); - //scheme->header()->resizeSection(2,80); + scheme->setItemDelegateForColumn(2, &fromDelegate); + basicTreeWidgetStyle(scheme); // setup list for (int i=0; i< paceZones->getScheme().nzones_default; i++) { - QTreeWidgetItem *add = new QTreeWidgetItem(scheme->invisibleRootItem()); add->setFlags(add->flags() | Qt::ItemIsEditable); // tab name - add->setText(0, paceZones->getScheme().zone_default_name[i]); + add->setData(0, Qt::DisplayRole, paceZones->getScheme().zone_default_name[i]); // field name - add->setText(1, paceZones->getScheme().zone_default_desc[i]); + add->setData(1, Qt::DisplayRole, paceZones->getScheme().zone_default_desc[i]); // low - QDoubleSpinBox *loedit = new QDoubleSpinBox(this); - loedit->setMinimum(0); - loedit->setMaximum(1000); - loedit->setSingleStep(1.0); - loedit->setDecimals(0); - loedit->setValue(paceZones->getScheme().zone_default[i]); - scheme->setItemWidget(add, 2, loedit); + add->setData(2, Qt::DisplayRole, paceZones->getScheme().zone_default[i]); } + updateButtons(); mainLayout->addWidget(scheme); mainLayout->addLayout(actionButtons); @@ -2627,56 +3147,38 @@ PaceSchemePage::PaceSchemePage(PaceZones* paceZones) : paceZones(paceZones) // button connect connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); + connect(scheme, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), this, SLOT(updateButtons())); } + void PaceSchemePage::addClicked() { - // are we at maximum already? - if (scheme->invisibleRootItem()->childCount() == 10) { - QMessageBox err; - err.setText(tr("Maximum of 10 zones reached.")); - err.setIcon(QMessageBox::Warning); - err.exec(); - return; - } - int index = scheme->invisibleRootItem()->childCount(); // new item QTreeWidgetItem *add = new QTreeWidgetItem; add->setFlags(add->flags() | Qt::ItemIsEditable); - QDoubleSpinBox *loedit = new QDoubleSpinBox(this); - loedit->setMinimum(0); - loedit->setMaximum(1000); - loedit->setSingleStep(1.0); - loedit->setDecimals(0); - loedit->setValue(100); - scheme->invisibleRootItem()->insertChild(index, add); - scheme->setItemWidget(add, 2, loedit); // Short QString text = tr("New"); for (int i=0; scheme->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(0, text); + add->setData(0, Qt::DisplayRole, text); // long text = tr("New"); for (int i=0; scheme->findItems(text, Qt::MatchExactly, 1).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(1, text); -} + add->setData(1, Qt::DisplayRole, text); -void -PaceSchemePage::renameClicked() -{ - // which one is selected? - if (scheme->currentItem()) scheme->editItem(scheme->currentItem(), 0); + add->setData(2, Qt::DisplayRole, 100); + + updateButtons(); } void @@ -2685,9 +3187,19 @@ PaceSchemePage::deleteClicked() if (scheme->currentItem()) { int index = scheme->invisibleRootItem()->indexOfChild(scheme->currentItem()); delete scheme->invisibleRootItem()->takeChild(index); + updateButtons(); } } + +void +PaceSchemePage::updateButtons +() +{ + addButton->setEnabled(scheme->invisibleRootItem()->childCount() < 10); + deleteButton->setEnabled(scheme->invisibleRootItem()->childCount() > 0 && scheme->currentItem() != nullptr); +} + // just for sorting struct paceschemeitem { QString name, desc; @@ -2705,11 +3217,10 @@ PaceSchemePage::getScheme() // read back the details from the table for (int i=0; iinvisibleRootItem()->childCount(); i++) { - paceschemeitem add; - add.name = scheme->invisibleRootItem()->child(i)->text(0); - add.desc = scheme->invisibleRootItem()->child(i)->text(1); - add.lo = ((QDoubleSpinBox *)(scheme->itemWidget(scheme->invisibleRootItem()->child(i), 2)))->value(); + add.name = scheme->invisibleRootItem()->child(i)->data(0, Qt::DisplayRole).toString(); + add.desc = scheme->invisibleRootItem()->child(i)->data(1, Qt::DisplayRole).toString(); + add.lo = scheme->invisibleRootItem()->child(i)->data(2, Qt::DisplayRole).toInt(); table.append(add); } @@ -2736,19 +3247,15 @@ CVPage::CVPage(PaceZones* paceZones, PaceSchemePage *schemePage) : metricPace = appsettings->value(this, paceZones->paceSetting(), GlobalContext::context()->useMetricUnits).toBool(); - QGridLayout *mainLayout = new QGridLayout(this); + QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setSpacing(10 *dpiXFactor); - updateButton = new QPushButton(tr("Update")); - updateButton->hide(); addButton = new QPushButton(tr("+")); deleteButton = new QPushButton(tr("-")); #ifndef Q_OS_MAC - updateButton->setFixedSize(60,20); addButton->setFixedSize(20*dpiXFactor,20*dpiYFactor); deleteButton->setFixedSize(20*dpiXFactor,20*dpiYFactor); #else - updateButton->setText(tr("Update")); addButton->setText(tr("Add")); deleteButton->setText(tr("Delete")); #endif @@ -2766,63 +3273,48 @@ CVPage::CVPage(PaceZones* paceZones, PaceSchemePage *schemePage) : #endif QHBoxLayout *zoneButtons = new QHBoxLayout; + zoneButtons->setSpacing(2 * dpiXFactor); zoneButtons->addStretch(); - zoneButtons->setSpacing(0); - zoneButtons->addWidget(addZoneButton); - zoneButtons->addWidget(deleteZoneButton); - - QHBoxLayout *addLayout = new QHBoxLayout; - QLabel *dateLabel = new QLabel(tr("From Date")); - QLabel *cpLabel = new QLabel(tr("Critical Velocity")); - QLabel *aetLabel = new QLabel(tr("Aerobic Threshold")); - dateEdit = new QDateEdit; - dateEdit->setDate(QDate::currentDate()); - dateEdit->setCalendarPopup(true); - - cvEdit = new QTimeEdit(); - cvEdit->setMinimumTime(QTime::fromString("00:00", "mm:ss")); - cvEdit->setMaximumTime(QTime::fromString("20:00", "mm:ss")); - cvEdit->setDisplayFormat("mm:ss"); - - aetEdit = new QTimeEdit(); - aetEdit->setMinimumTime(QTime::fromString("00:00", "mm:ss")); - aetEdit->setMaximumTime(QTime::fromString("20:00", "mm:ss")); - aetEdit->setDisplayFormat("mm:ss"); - - per = new QLabel(this); - per->setText(paceZones->paceUnits(metricPace)); + zoneButtons->addWidget(defaultButton); + zoneButtons->addItem(new QSpacerItem(5 * dpiXFactor, defaultButton->sizeHint().height())); + zoneButtons->addWidget(addZoneButton, 0, Qt::AlignCenter | Qt::AlignTop); + zoneButtons->addWidget(deleteZoneButton, 0, Qt::AlignCenter | Qt::AlignTop); QHBoxLayout *actionButtons = new QHBoxLayout; - actionButtons->setSpacing(2 *dpiXFactor); - actionButtons->addWidget(cpLabel); - actionButtons->addWidget(cvEdit); - actionButtons->addWidget(aetLabel); - actionButtons->addWidget(aetEdit); - actionButtons->addWidget(per); + actionButtons->setSpacing(2 * dpiXFactor); actionButtons->addStretch(); - actionButtons->addWidget(updateButton); - actionButtons->addWidget(addButton); - actionButtons->addWidget(deleteButton); - actionButtons->addWidget(defaultButton); + actionButtons->addItem(new QSpacerItem(5 * dpiXFactor, defaultButton->sizeHint().height())); + actionButtons->addWidget(addButton, 0, Qt::AlignCenter | Qt::AlignTop); + actionButtons->addWidget(deleteButton, 0, Qt::AlignCenter | Qt::AlignTop); - addLayout->addWidget(dateLabel); - addLayout->addWidget(dateEdit); - addLayout->addStretch(); + dateDelegate.setCalendarPopup(true); + + cvDelegate.setFormat("mm:ss"); + cvDelegate.setTimeRange(QTime(0, 0, 0), QTime(0, 20, 0)); + cvDelegate.setSuffix(paceZones->paceUnits(metricPace)); + cvDelegate.setShowSuffixOnEdit(true); + cvDelegate.setShowSuffixOnDisplay(true); + + aetDelegate.setFormat("mm:ss"); + aetDelegate.setTimeRange(QTime(0, 0, 0), QTime(0, 20, 0)); + aetDelegate.setSuffix(paceZones->paceUnits(metricPace)); + aetDelegate.setShowSuffixOnEdit(true); + aetDelegate.setShowSuffixOnDisplay(true); ranges = new QTreeWidget; - ranges->headerItem()->setText(0, tr("From Date")); + ranges->headerItem()->setText(0, tr("Start Date")); ranges->headerItem()->setText(1, tr("Critical Velocity")); ranges->headerItem()->setText(2, tr("Aerobic Threshold")); ranges->setColumnCount(3); - ranges->setSelectionMode(QAbstractItemView::SingleSelection); - ranges->setUniformRowHeights(true); - ranges->setIndentation(0); + ranges->setItemDelegateForColumn(0, &dateDelegate); + ranges->setItemDelegateForColumn(1, &cvDelegate); + ranges->setItemDelegateForColumn(2, &aetDelegate); + basicTreeWidgetStyle(ranges); // setup list of ranges - for (int i=0; i< paceZones->getRangeSize(); i++) { - + for (int i = 0; i < paceZones->getRangeSize(); i++) { QTreeWidgetItem *add = new QTreeWidgetItem(ranges->invisibleRootItem()); - add->setFlags(add->flags() & ~Qt::ItemIsEditable); + add->setFlags(add->flags() | Qt::ItemIsEditable); // Embolden ranges with manually configured zones QFont font; @@ -2830,143 +3322,152 @@ CVPage::CVPage(PaceZones* paceZones, PaceSchemePage *schemePage) : QFont::Normal : QFont::Black); // date - add->setText(0, paceZones->getStartDate(i).toString(tr("MMM d, yyyy"))); + add->setData(0, Qt::DisplayRole, paceZones->getStartDate(i)); add->setFont(0, font); // CV - double kph = paceZones->getCV(i); - add->setText(1, QString("%1 %2 %3 %4") - .arg(paceZones->kphToPaceString(kph, true)) - .arg(paceZones->paceUnits(true)) - .arg(paceZones->kphToPaceString(kph, false)) - .arg(paceZones->paceUnits(false))); + add->setData(1, Qt::DisplayRole, paceZones->kphToPaceTime(paceZones->getCV(i), metricPace)); add->setFont(1, font); // AeT - kph = paceZones->getAeT(i); - add->setText(2, QString("%1 %2 %3 %4") - .arg(paceZones->kphToPaceString(kph, true)) - .arg(paceZones->paceUnits(true)) - .arg(paceZones->kphToPaceString(kph, false)) - .arg(paceZones->paceUnits(false))); + add->setData(2, Qt::DisplayRole, paceZones->kphToPaceTime(paceZones->getAeT(i), metricPace)); add->setFont(2, font); } - for(int i = 0; i < ranges->columnCount(); i++) - ranges->resizeColumnToContents(i); + + zoneFromDelegate.setTimeRange(QTime(0, 0, 0), QTime(0, 20, 0)); + zoneFromDelegate.setFormat("mm:ss"); + zoneFromDelegate.setSuffix(paceZones->paceUnits(metricPace)); + zoneFromDelegate.setShowSuffixOnEdit(true); + zoneFromDelegate.setShowSuffixOnDisplay(true); zones = new QTreeWidget; zones->headerItem()->setText(0, tr("Short")); zones->headerItem()->setText(1, tr("Long")); zones->headerItem()->setText(2, tr("From")); zones->setColumnCount(3); - zones->setSelectionMode(QAbstractItemView::SingleSelection); - zones->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit - zones->setUniformRowHeights(true); - zones->setIndentation(0); + zones->setItemDelegateForColumn(2, &zoneFromDelegate); + basicTreeWidgetStyle(zones); - mainLayout->addLayout(addLayout, 0,0); - mainLayout->addLayout(actionButtons, 1,0); - mainLayout->addWidget(ranges, 2,0); - mainLayout->addLayout(zoneButtons, 3,0); - mainLayout->addWidget(zones, 4,0); + mainLayout->addWidget(ranges); + mainLayout->addLayout(actionButtons); + mainLayout->addWidget(zones); + mainLayout->addLayout(zoneButtons); - // edit connect - connect(dateEdit, SIGNAL(dateChanged(QDate)), this, SLOT(rangeEdited())); - connect(cvEdit, SIGNAL(timeChanged(QTime)), this, SLOT(rangeEdited())); - connect(aetEdit, SIGNAL(timeChanged(QTime)), this, SLOT(rangeEdited())); + updateButtons(); // button connect - connect(updateButton, SIGNAL(clicked()), this, SLOT(editClicked())); connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); connect(defaultButton, SIGNAL(clicked()), this, SLOT(defaultClicked())); connect(addZoneButton, SIGNAL(clicked()), this, SLOT(addZoneClicked())); connect(deleteZoneButton, SIGNAL(clicked()), this, SLOT(deleteZoneClicked())); + connect(ranges, SIGNAL(itemSelectionChanged()), this, SLOT(rangeSelectionChanged())); + connect(ranges, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), this, SLOT(updateButtons())); +#if QT_VERSION < 0x060000 + connect(ranges->model(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector&)), + this, SLOT(rangeChanged(const QModelIndex&, const QModelIndex&, const QVector&))); +#else + connect(ranges->model(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QList&)), + this, SLOT(rangeChanged(const QModelIndex&, const QModelIndex&, const QList&))); +#endif + connect(zones, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), this, SLOT(updateButtons())); connect(zones, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(zonesChanged())); } + void CVPage::addClicked() { - if( paceZones->kphFromTime(cvEdit, metricPace) <= 0 ) { - QMessageBox err; - err.setText(tr("Critical Velocity must be > 0")); - err.setIcon(QMessageBox::Warning); - err.exec(); - return; - } - // get current scheme paceZones->setScheme(schemePage->getScheme()); - int index = paceZones->addZoneRange(dateEdit->date(), paceZones->kphFromTime(cvEdit, metricPace), paceZones->kphFromTime(aetEdit, metricPace)); + QDate date = QDate::currentDate(); + QTime cv; + QTime aet; + + QTreeWidgetItem *sourceItem = nullptr; + if (ranges->currentItem() != nullptr) { + sourceItem = ranges->currentItem(); + } else if (ranges->invisibleRootItem()->childCount() > 0) { + sourceItem = ranges->invisibleRootItem()->child(ranges->invisibleRootItem()->childCount() - 1); + } + + if (sourceItem != nullptr) { + cv = sourceItem->data(1, Qt::DisplayRole).toTime(); + aet = sourceItem->data(2, Qt::DisplayRole).toTime(); + } else { + if (paceZones->isSwim()) { + if (metricPace) { + cv = QTime(0, 1, 36); + aet = QTime(0, 1, 38); + } else { + cv = QTime(0, 1, 28); + aet = QTime(0, 1, 30); + } + } else { + if (metricPace) { + cv = QTime(0, 4, 0); + aet = QTime(0, 4, 27); + } else { + cv = QTime(0, 6, 26); + aet = QTime(0, 7, 9); + } + } + } + + QDialog dialog; + dialog.setWindowTitle(tr("New range")); + + QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Apply + | QDialogButtonBox::Discard); + connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), &dialog, SLOT(accept())); + connect(buttonBox->button(QDialogButtonBox::Discard), SIGNAL(clicked()), &dialog, SLOT(reject())); + + QDateEdit *dateEdit = new QDateEdit(date); + dateEdit->setCalendarPopup(true); + + QTimeEdit *cvEdit = new QTimeEdit(); + cvEdit->setMinimumTime(QTime(0, 0, 0)); + cvEdit->setMaximumTime(QTime(0, 20, 0)); + cvEdit->setDisplayFormat("mm:ss '" + paceZones->paceUnits(metricPace) + "'"); + cvEdit->setTime(cv); + + QTimeEdit *aetEdit = new QTimeEdit(); + aetEdit->setMinimumTime(QTime(0, 0, 0)); + aetEdit->setMaximumTime(QTime(0, 20, 0)); + aetEdit->setDisplayFormat("mm:ss '" + paceZones->paceUnits(metricPace) + "'"); + aetEdit->setTime(aet); + + QFormLayout *form = new QFormLayout(&dialog); + form->addRow(tr("Start Date"), dateEdit); + form->addRow(tr("Critical Velocity (CV)"), cvEdit); + form->addRow(tr("Aerobic Threshold (AeT)"), aetEdit); + form->addItem(new QSpacerItem(1, 30 * dpiYFactor)); + form->addRow(buttonBox); + + if (dialog.exec() == QDialog::Accepted) { + date = dateEdit->date(); + cv = cvEdit->time(); + aet = aetEdit->time(); + } else { + return; + } + + int index = paceZones->addZoneRange(date, paceZones->kphFromTime(cv, metricPace), paceZones->kphFromTime(aet, metricPace)); // new item QTreeWidgetItem *add = new QTreeWidgetItem; - add->setFlags(add->flags() & ~Qt::ItemIsEditable); + add->setFlags(add->flags() | Qt::ItemIsEditable); ranges->invisibleRootItem()->insertChild(index, add); - // date - add->setText(0, dateEdit->date().toString(tr("MMM d, yyyy"))); - - // CV - double kph = paceZones->kphFromTime(cvEdit, metricPace); - add->setText(1, QString("%1 %2 %3 %4") - .arg(paceZones->kphToPaceString(kph, true)) - .arg(paceZones->paceUnits(true)) - .arg(paceZones->kphToPaceString(kph, false)) - .arg(paceZones->paceUnits(false))); - - // AeT - kph = paceZones->kphFromTime(aetEdit, metricPace); - add->setText(2, QString("%1 %2 %3 %4") - .arg(paceZones->kphToPaceString(kph, true)) - .arg(paceZones->paceUnits(true)) - .arg(paceZones->kphToPaceString(kph, false)) - .arg(paceZones->paceUnits(false))); + add->setData(0, Qt::DisplayRole, date); + add->setData(1, Qt::DisplayRole, cv); + add->setData(2, Qt::DisplayRole, aet); + updateButtons(); } -void -CVPage::editClicked() -{ - if( paceZones->kphFromTime(cvEdit, metricPace) <= 0 ) { - QMessageBox err; - err.setText(tr("Critical Velocity must be > 0")); - err.setIcon(QMessageBox::Warning); - err.exec(); - return; - } - - // get current scheme - paceZones->setScheme(schemePage->getScheme()); - - QTreeWidgetItem *edit = ranges->selectedItems().at(0); - int index = ranges->indexOfTopLevelItem(edit); - - // date - paceZones->setStartDate(index, dateEdit->date()); - edit->setText(0, dateEdit->date().toString(tr("MMM d, yyyy"))); - - // CV - double kph = paceZones->kphFromTime(cvEdit, metricPace); - paceZones->setCV(index, kph); - edit->setText(1, QString("%1 %2 %3 %4") - .arg(paceZones->kphToPaceString(kph, true)) - .arg(paceZones->paceUnits(true)) - .arg(paceZones->kphToPaceString(kph, false)) - .arg(paceZones->paceUnits(false))); - - // AeT - kph = paceZones->kphFromTime(aetEdit, metricPace); - paceZones->setAeT(index, kph); - edit->setText(2, QString("%1 %2 %3 %4") - .arg(paceZones->kphToPaceString(kph, true)) - .arg(paceZones->paceUnits(true)) - .arg(paceZones->kphToPaceString(kph, false)) - .arg(paceZones->paceUnits(false))); -} void CVPage::deleteClicked() @@ -2975,6 +3476,7 @@ CVPage::deleteClicked() int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); delete ranges->invisibleRootItem()->takeChild(index); paceZones->deleteRange(index); + updateButtons(); } } @@ -2993,7 +3495,6 @@ CVPage::defaultClicked() ranges->currentItem()->setFont(1, font); ranges->currentItem()->setFont(2, font); - // set the range to use defaults on the scheme page paceZones->setScheme(schemePage->getScheme()); paceZones->setZonesFromCV(index); @@ -3003,26 +3504,40 @@ CVPage::defaultClicked() // update the zones display rangeSelectionChanged(); + + updateButtons(); } } void -CVPage::rangeEdited() +CVPage::rangeChanged +#if QT_VERSION < 0x060000 +(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) +#else +(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles) +#endif { - if (ranges->currentItem()) { - int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); + Q_UNUSED(bottomRight) - QDate date = dateEdit->date(); - QDate odate = paceZones->getStartDate(index); - QTime cv = cvEdit->time(); - QTime ocv = QTime::fromString(paceZones->kphToPaceString(paceZones->getCV(index), metricPace), "mm:ss"); - QTime aet = aetEdit->time(); - QTime oaet = QTime::fromString(paceZones->kphToPaceString(paceZones->getAeT(index), metricPace), "mm:ss"); - - if (date != odate || cv != ocv || aet != oaet) - updateButton->show(); - else - updateButton->hide(); + if ( roles.length() > 0 + && ! roles.contains(Qt::DisplayRole) + && ! roles.contains(Qt::EditRole)) { + return; + } + paceZones->setScheme(schemePage->getScheme()); + int index = topLeft.row(); + switch (topLeft.column()) { + case 0: + paceZones->setStartDate(index, topLeft.data(Qt::DisplayRole).toDate()); + break; + case 1: + paceZones->setCV(index, paceZones->kphFromTime(topLeft.data(Qt::DisplayRole).toTime(), metricPace)); + break; + case 2: + paceZones->setAeT(index, paceZones->kphFromTime(topLeft.data(Qt::DisplayRole).toTime(), metricPace)); + break; + default: + break; } } @@ -3032,22 +3547,14 @@ CVPage::rangeSelectionChanged() active = true; // wipe away current contents of zones - foreach (QTreeWidgetItem *item, zones->invisibleRootItem()->takeChildren()) { - delete zones->itemWidget(item, 2); - delete item; - } + zones->clear(); // fill with current details if (ranges->currentItem()) { - int index = ranges->invisibleRootItem()->indexOfChild(ranges->currentItem()); PaceZoneRange current = paceZones->getZoneRange(index); - dateEdit->setDate(paceZones->getStartDate(index)); - cvEdit->setTime(QTime::fromString(paceZones->kphToPaceString(paceZones->getCV(index), metricPace), "mm:ss")); - aetEdit->setTime(QTime::fromString(paceZones->kphToPaceString(paceZones->getAeT(index), metricPace), "mm:ss")); if (current.zonesSetFromCV) { - // reapply the scheme in case it has been changed paceZones->setScheme(schemePage->getScheme()); paceZones->setZonesFromCV(index); @@ -3058,25 +3565,17 @@ CVPage::rangeSelectionChanged() } else defaultButton->show(); for (int i=0; i< current.zones.count(); i++) { - QTreeWidgetItem *add = new QTreeWidgetItem(zones->invisibleRootItem()); add->setFlags(add->flags() | Qt::ItemIsEditable); // tab name - add->setText(0, current.zones[i].name); + add->setData(0, Qt::DisplayRole, current.zones[i].name); // field name - add->setText(1, current.zones[i].desc); + add->setData(1, Qt::DisplayRole, current.zones[i].desc); // low - QTimeEdit *loedit = new QTimeEdit(QTime::fromString(paceZones->kphToPaceString(current.zones[i].lo, metricPace), "mm:ss"), this); - loedit->setMinimumTime(QTime::fromString("00:00", "mm:ss")); - loedit->setMaximumTime(QTime::fromString("20:00", "mm:ss")); - loedit->setDisplayFormat("mm:ss"); - zones->setItemWidget(add, 2, loedit); - connect(loedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); + add->setData(2, Qt::DisplayRole, QTime::fromString(paceZones->kphToPaceString(current.zones[i].lo, metricPace), "mm:ss")); } - for(int i = 0; i < zones->columnCount(); i++) - zones->resizeColumnToContents(i); } active = false; @@ -3103,32 +3602,25 @@ CVPage::addZoneClicked() // new item QTreeWidgetItem *add = new QTreeWidgetItem; add->setFlags(add->flags() | Qt::ItemIsEditable); - - QTimeEdit *loedit = new QTimeEdit(QTime::fromString("00:00", "mm:ss"), this); - loedit->setMinimumTime(QTime::fromString("00:00", "mm:ss")); - loedit->setMaximumTime(QTime::fromString("20:00", "mm:ss")); - loedit->setDisplayFormat("mm:ss"); - zones->invisibleRootItem()->insertChild(index, add); - zones->setItemWidget(add, 2, loedit); - connect(loedit, SIGNAL(editingFinished()), this, SLOT(zonesChanged())); // Short QString text = tr("New"); for (int i=0; zones->findItems(text, Qt::MatchExactly, 0).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(0, text); + add->setData(0, Qt::DisplayRole, text); // long text = tr("New"); for (int i=0; zones->findItems(text, Qt::MatchExactly, 1).count() > 0; i++) { text = QString(tr("New (%1)")).arg(i+1); } - add->setText(1, text); + add->setData(1, Qt::DisplayRole, text); active = false; zonesChanged(); + updateButtons(); } void @@ -3146,6 +3638,7 @@ CVPage::deleteZoneClicked() active = false; zonesChanged(); + updateButtons(); } void @@ -3178,12 +3671,12 @@ CVPage::zonesChanged() QList zoneinfos; for (int i=0; i< zones->invisibleRootItem()->childCount(); i++) { QTreeWidgetItem *item = zones->invisibleRootItem()->child(i); - QTimeEdit *loTimeEdit = (QTimeEdit*)zones->itemWidget(item, 2); - double kph = loTimeEdit->time() == QTime(0,0,0) ? 0.0 : paceZones->kphFromTime(loTimeEdit, metricPace); - zoneinfos << PaceZoneInfo(item->text(0), - item->text(1), - kph, - 0); + QTime time = item->data(2, Qt::DisplayRole).toTime(); + double kph = time == QTime(0,0,0) ? 0.0 : paceZones->kphFromTime(time, metricPace); + zoneinfos << PaceZoneInfo(item->data(0, Qt::DisplayRole).toString(), + item->data(1, Qt::DisplayRole).toString(), + kph, + 0); } // now sort the list @@ -3200,10 +3693,23 @@ CVPage::zonesChanged() // now replace the current range struct paceZones->setZoneRange(index, current); + updateButtons(); } } } + +void +CVPage::updateButtons +() +{ + addButton->setEnabled(true); + deleteButton->setEnabled(ranges->currentItem() != nullptr); + addZoneButton->setEnabled(zones->invisibleRootItem()->childCount() < 10 && ranges->currentItem() != nullptr); + deleteZoneButton->setEnabled(zones->invisibleRootItem()->childCount() > 0 && zones->currentItem() != nullptr); +} + + // // Season Editor // @@ -3644,3 +4150,20 @@ AutoImportPage::browseImportDir() } } } + + +extern void +basicTreeWidgetStyle +(QTreeWidget *tree) +{ + tree->setSelectionMode(QAbstractItemView::SingleSelection); + tree->setEditTriggers( QAbstractItemView::DoubleClicked + | QAbstractItemView::SelectedClicked + | QAbstractItemView::AnyKeyPressed); + tree->setUniformRowHeights(true); + tree->setIndentation(0); + tree->setAlternatingRowColors(true); + for (int i = 0; i < tree->columnCount(); ++i) { + tree->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } +} diff --git a/src/Gui/AthletePages.h b/src/Gui/AthletePages.h index 14bd95e61..2a977c029 100644 --- a/src/Gui/AthletePages.h +++ b/src/Gui/AthletePages.h @@ -51,6 +51,7 @@ #include "RideAutoImportConfig.h" #include "RemoteControl.h" #include "Measures.h" +#include "StyledItemDelegates.h" class MeasuresPage : public QWidget { @@ -222,69 +223,122 @@ class CredentialsPage : public QScrollArea class SchemePage : public QWidget { Q_OBJECT - G_OBJECT - public: SchemePage(Zones *zones); + ZoneScheme getScheme(); qint32 saveClicked(); - public slots: + private slots: void addClicked(); void deleteClicked(); - void renameClicked(); + void updateButtons(); private: Zones *zones; QTreeWidget *scheme; - QPushButton *addButton, *renameButton, *deleteButton; + QPushButton *addButton, *deleteButton; + SpinBoxEditDelegate zoneFromDelegate; }; + +// Compatibility helper for Qt5 +// exposes methods that turned public in Qt6 from protected in Qt5 +#if QT_VERSION < 0x060000 +class TreeWidget6 : public QTreeWidget +{ + Q_OBJECT + + public: + TreeWidget6(QWidget *parent = nullptr): QTreeWidget(parent) { + } + + QModelIndex indexFromItem(const QTreeWidgetItem *item, int column = 0) const { + return QTreeWidget::indexFromItem(item, column); + } + + QTreeWidgetItem* itemFromIndex(const QModelIndex &index) const { + return QTreeWidget::itemFromIndex(index); + } + +}; +#else +typedef QTreeWidget TreeWidget6; +#endif + + class CPPage : public QWidget { Q_OBJECT - G_OBJECT - public: CPPage(Context *context, Zones *zones, SchemePage *schemePage); - QComboBox *useCPForFTPCombo; + qint32 saveClicked(); - struct { - int cpforftp; - } b4; + QComboBox *useModel; + QComboBox *useCPForFTPCombo; + + struct { + int modelIdx; + int cpforftp; + } b4; public slots: void addClicked(); - void editClicked(); void deleteClicked(); void defaultClicked(); - void rangeEdited(); void rangeSelectionChanged(); void addZoneClicked(); void deleteZoneClicked(); void zonesChanged(); void initializeRanges(); + private slots: +#if QT_VERSION < 0x060000 + void rangeChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()); +#else + void rangeChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles = QList()); +#endif + + void adopt(); + void updateButtons(); + private: bool active; - QDateEdit *dateEdit; - QDoubleSpinBox *cpEdit; - QDoubleSpinBox *aetEdit; - QDoubleSpinBox *ftpEdit; - QDoubleSpinBox *wEdit; - QDoubleSpinBox *pmaxEdit; + DateEditDelegate dateDelegate; + SpinBoxEditDelegate cpDelegate; + SpinBoxEditDelegate aetDelegate; + SpinBoxEditDelegate ftpDelegate; + SpinBoxEditDelegate wDelegate; + SpinBoxEditDelegate pmaxDelegate; + NoEditDelegate statusDelegate; + + SpinBoxEditDelegate zoneFromDelegate; Context *context; Zones *zones_; SchemePage *schemePage; - QTreeWidget *ranges; + TreeWidget6 *ranges; QTreeWidget *zones; - QPushButton *addButton, *updateButton, *deleteButton; + QPushButton *addButton, *deleteButton; + QPushButton *adoptButton; QPushButton *addZoneButton, *deleteZoneButton, *defaultButton; + QPushButton *newZoneRequired; + + bool getValuesFor(const QDate &date, bool allowDefaults, int &cp, int &aetp, int &ftp, int &wprime, int &pmax, int &estOffset, bool &defaults, QDate *startDate = nullptr) const; + void setEstimateStatus(QTreeWidgetItem *item); + void setRangeData(QModelIndex modelIndex, int column, QVariant data); + bool needsNewRange() const; + void mkAdoptionRow(QGridLayout *grid, int row, int labelId, const QString &unit, int cur, int est, QCheckBox *&accept, const QString &infoText = QString()) const; + void connectAdoptionDialogApplyButton(QVector checkboxes, QPushButton *applyButton) const; + bool modelHasFtp() const; + bool modelHasPmax() const; + QWidget *mkInfoLabel(int labelId, const QString &infoText = QString()) const; + QString getText(int id, int value = 0) const; + bool addDialogManual(QDate &date, int &cp, int &aetp, int &wprime, int &ftp, int &pmax) const; }; class ZonePage : public QWidget @@ -324,93 +378,89 @@ class ZonePage : public QWidget }; + // // Heartrate Zones // class HrSchemePage : public QWidget { Q_OBJECT - G_OBJECT - public: HrSchemePage(HrZones *hrZones); + HrZoneScheme getScheme(); qint32 saveClicked(); - public slots: + private slots: void addClicked(); void deleteClicked(); - void renameClicked(); + void updateButtons(); private: HrZones *hrZones; QTreeWidget *scheme; - QPushButton *addButton, *renameButton, *deleteButton; + QPushButton *addButton, *deleteButton; + + SpinBoxEditDelegate ltDelegate; + DoubleSpinBoxEditDelegate trimpkDelegate; }; class LTPage : public QWidget { Q_OBJECT - G_OBJECT - public: LTPage(Context *context, HrZones *hrZones, HrSchemePage *schemePage); - public slots: + private slots: void addClicked(); - void editClicked(); void deleteClicked(); void defaultClicked(); - void rangeEdited(); + void rangeChanged(const QModelIndex &modelIndex); void rangeSelectionChanged(); void addZoneClicked(); void deleteZoneClicked(); void zonesChanged(); + void updateButtons(); private: bool active; - QDateEdit *dateEdit; - QDoubleSpinBox *ltEdit; - QDoubleSpinBox *aetEdit; - QDoubleSpinBox *restHrEdit; - QDoubleSpinBox *maxHrEdit; + DateEditDelegate dateDelegate; + SpinBoxEditDelegate ltDelegate; + SpinBoxEditDelegate aetDelegate; + SpinBoxEditDelegate restHrDelegate; + SpinBoxEditDelegate maxHrDelegate; + + SpinBoxEditDelegate zoneLoDelegate; + DoubleSpinBoxEditDelegate zoneTrimpDelegate; Context *context; HrZones *hrZones; HrSchemePage *schemePage; QTreeWidget *ranges; QTreeWidget *zones; - QPushButton *addButton, *updateButton, *deleteButton; + QPushButton *addButton, *deleteButton; QPushButton *addZoneButton, *deleteZoneButton, *defaultButton; }; class HrZonePage : public QWidget { Q_OBJECT - G_OBJECT - public: - HrZonePage(Context *); ~HrZonePage(); qint32 saveClicked(); - public slots: - - protected: - Context *context; QTabWidget *tabs; private: - QLabel *sportLabel; QComboBox *sportCombo; @@ -420,7 +470,6 @@ class HrZonePage : public QWidget QHash ltPage; private slots: - void changeSport(); }; @@ -431,58 +480,63 @@ class HrZonePage : public QWidget class PaceSchemePage : public QWidget { Q_OBJECT - G_OBJECT - public: PaceSchemePage(PaceZones* paceZones); + PaceZoneScheme getScheme(); qint32 saveClicked(); - public slots: + private slots: void addClicked(); void deleteClicked(); - void renameClicked(); + void updateButtons(); private: PaceZones* paceZones; QTreeWidget *scheme; - QPushButton *addButton, *renameButton, *deleteButton; + QPushButton *addButton, *deleteButton; + SpinBoxEditDelegate fromDelegate; }; class CVPage : public QWidget { Q_OBJECT - G_OBJECT - public: CVPage(PaceZones* paceZones, PaceSchemePage *schemePage); + bool metricPace; - public slots: + private slots: void addClicked(); - void editClicked(); void deleteClicked(); void defaultClicked(); - void rangeEdited(); +#if QT_VERSION < 0x060000 + void rangeChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()); +#else + void rangeChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles = QList()); +#endif void rangeSelectionChanged(); void addZoneClicked(); void deleteZoneClicked(); void zonesChanged(); + void updateButtons(); private: bool active; - QDateEdit *dateEdit; - QTimeEdit *cvEdit; - QTimeEdit *aetEdit; + DateEditDelegate dateDelegate; + TimeEditDelegate cvDelegate; + TimeEditDelegate aetDelegate; + + TimeEditDelegate zoneFromDelegate; PaceZones* paceZones; PaceSchemePage *schemePage; QTreeWidget *ranges; QTreeWidget *zones; - QPushButton *addButton, *updateButton, *deleteButton; + QPushButton *addButton, *deleteButton; QPushButton *addZoneButton, *deleteZoneButton, *defaultButton; QLabel *per; }; @@ -601,4 +655,6 @@ class AutoImportPage : public QWidget }; +extern void basicTreeWidgetStyle(QTreeWidget *tree); + #endif diff --git a/src/Gui/StyledItemDelegates.cpp b/src/Gui/StyledItemDelegates.cpp index 57de82011..cfee81079 100644 --- a/src/Gui/StyledItemDelegates.cpp +++ b/src/Gui/StyledItemDelegates.cpp @@ -1,5 +1,8 @@ #include "StyledItemDelegates.h" +#include +#include + // NoEditDelegate ///////////////////////////////////////////////////////////////// @@ -49,3 +52,503 @@ UniqueLabelEditDelegate::setModelData model->setData(index, newData, Qt::EditRole); model->setData(modifiedIndex, true, Qt::EditRole); } + + +// SpinBoxEditDelegate //////////////////////////////////////////////////////////// + +SpinBoxEditDelegate::SpinBoxEditDelegate +(QObject *parent) +: QStyledItemDelegate(parent) +{ +} + + +void +SpinBoxEditDelegate::setMininum +(int minimum) +{ + this->minimum = minimum; +} + + +void +SpinBoxEditDelegate::setMaximum +(int maximum) +{ + this->maximum = maximum; +} + + +void +SpinBoxEditDelegate::setRange +(int minimum, int maximum) +{ + this->minimum = minimum; + this->maximum = maximum; +} + + +void +SpinBoxEditDelegate::setSingleStep +(int val) +{ + singleStep = val; +} + + +void +SpinBoxEditDelegate::setSuffix +(const QString &suffix) +{ + this->suffix = suffix; +} + + +void +SpinBoxEditDelegate::setShowSuffixOnEdit +(bool show) +{ + showSuffixOnEdit = show; +} + + +void +SpinBoxEditDelegate::setShowSuffixOnDisplay +(bool show) +{ + showSuffixOnDisplay = show; +} + + +QWidget* +SpinBoxEditDelegate::createEditor +(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + QSpinBox *spinbox = new QSpinBox(parent); + spinbox->setRange(minimum, maximum); + spinbox->setSingleStep(singleStep); + if (suffix.length() > 0) { + spinbox->setSuffix(" " + suffix); + } else { + spinbox->setSuffix(""); + } + + return spinbox; +} + + +void +SpinBoxEditDelegate::setEditorData +(QWidget *editor, const QModelIndex &index) const +{ + int value = index.model()->data(index, Qt::EditRole).toInt(); + QSpinBox *spinbox = static_cast(editor); + spinbox->setValue(value); +} + + +void +SpinBoxEditDelegate::setModelData +(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + QSpinBox *spinbox = static_cast(editor); + spinbox->interpretText(); + int newValue = spinbox->value(); + if (model->data(index, Qt::EditRole).toInt() != newValue) { + model->setData(index, newValue, Qt::EditRole); + } +} + + +QString +SpinBoxEditDelegate::displayText +(const QVariant &value, const QLocale &locale) const +{ + Q_UNUSED(locale) + + QString ret = QString::number(value.toInt()); + if (showSuffixOnDisplay && ! suffix.isEmpty()) { + ret += " " + suffix; + } + return ret; +} + + +QSize +SpinBoxEditDelegate::sizeHint +(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + QSpinBox widget; + if ((showSuffixOnEdit || showSuffixOnDisplay) && ! suffix.isEmpty()) { + widget.setSuffix(" " + suffix); + } + widget.setMaximum(maximum); + widget.setValue(maximum); + return widget.sizeHint(); +} + + +// DoubleSpinBoxEditDelegate ////////////////////////////////////////////////////// + +DoubleSpinBoxEditDelegate::DoubleSpinBoxEditDelegate +(QObject *parent) +: QStyledItemDelegate(parent) +{ +} + + +void +DoubleSpinBoxEditDelegate::setMininum +(double minimum) +{ + this->minimum = minimum; +} + + +void +DoubleSpinBoxEditDelegate::setMaximum +(double maximum) +{ + this->maximum = maximum; +} + + +void +DoubleSpinBoxEditDelegate::setRange +(double minimum, double maximum) +{ + this->minimum = minimum; + this->maximum = maximum; +} + + +void +DoubleSpinBoxEditDelegate::setSingleStep +(double val) +{ + singleStep = val; +} + + +void +DoubleSpinBoxEditDelegate::setDecimals +(int prec) +{ + this->prec = prec; +} + + +void +DoubleSpinBoxEditDelegate::setSuffix +(const QString &suffix) +{ + this->suffix = suffix; +} + + +void +DoubleSpinBoxEditDelegate::setShowSuffixOnEdit +(bool show) +{ + showSuffixOnEdit = show; +} + + +void +DoubleSpinBoxEditDelegate::setShowSuffixOnDisplay +(bool show) +{ + showSuffixOnDisplay = show; +} + + +QWidget* +DoubleSpinBoxEditDelegate::createEditor +(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + QDoubleSpinBox *spinbox = new QDoubleSpinBox(parent); + spinbox->setRange(minimum, maximum); + spinbox->setSingleStep(singleStep); + spinbox->setDecimals(prec); + if (suffix.length() > 0) { + spinbox->setSuffix(" " + suffix); + } else { + spinbox->setSuffix(""); + } + + return spinbox; +} + + +void +DoubleSpinBoxEditDelegate::setEditorData +(QWidget *editor, const QModelIndex &index) const +{ + double value = index.model()->data(index, Qt::EditRole).toDouble(); + QDoubleSpinBox *spinbox = static_cast(editor); + spinbox->setValue(value); +} + + +void +DoubleSpinBoxEditDelegate::setModelData +(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + QDoubleSpinBox *spinbox = static_cast(editor); + spinbox->interpretText(); + double newValue = spinbox->value(); + if (model->data(index, Qt::EditRole).toDouble() != newValue) { + model->setData(index, newValue, Qt::EditRole); + } +} + + +QString +DoubleSpinBoxEditDelegate::displayText +(const QVariant &value, const QLocale &locale) const +{ + Q_UNUSED(locale) + + QString ret = QString::number(value.toDouble(), 'f', prec); + if (showSuffixOnDisplay && ! suffix.isEmpty()) { + ret += " " + suffix; + } + return ret; +} + + +QSize +DoubleSpinBoxEditDelegate::sizeHint +(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + QDoubleSpinBox widget; + if ((showSuffixOnEdit || showSuffixOnDisplay) && ! suffix.isEmpty()) { + widget.setSuffix(" " + suffix); + } + widget.setMaximum(maximum); + widget.setValue(maximum); + widget.setDecimals(prec); + return widget.sizeHint(); +} + + +// DateEditDelegate /////////////////////////////////////////////////////////////// + +DateEditDelegate::DateEditDelegate +(QObject *parent) +: QStyledItemDelegate(parent) +{ +} + + +void +DateEditDelegate::setCalendarPopup +(bool enable) +{ + calendarPopup = enable; +} + + +QWidget* +DateEditDelegate::createEditor +(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + QDateEdit *dateEdit = new QDateEdit(parent); + dateEdit->setCalendarPopup(calendarPopup); + + return dateEdit; +} + + +void +DateEditDelegate::setEditorData +(QWidget *editor, const QModelIndex &index) const +{ + QDate value = index.model()->data(index, Qt::EditRole).toDate(); + QDateEdit *dateEdit = static_cast(editor); + dateEdit->setDate(value); +} + + +void +DateEditDelegate::setModelData +(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + QDateEdit *dateEdit = static_cast(editor); + QDate newValue = dateEdit->date(); + if (model->data(index, Qt::EditRole).toDate() != newValue) { + model->setData(index, newValue, Qt::EditRole); + } +} + + +QString +DateEditDelegate::displayText +(const QVariant &value, const QLocale &locale) const +{ + return value.toDate().toString(locale.dateFormat(QLocale::ShortFormat)); +} + + +QSize +DateEditDelegate::sizeHint +(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + QDateEdit widget; + return widget.sizeHint(); +} + + +// TimeEditDelegate /////////////////////////////////////////////////////////////// + +TimeEditDelegate::TimeEditDelegate +(QObject *parent) +: QStyledItemDelegate(parent) +{ +} + + +void +TimeEditDelegate::setFormat +(const QString &format) +{ + this->format = format; +} + + +void +TimeEditDelegate::setSuffix +(const QString &suffix) +{ + this->suffix = suffix; +} + + +void +TimeEditDelegate::setShowSuffixOnEdit +(bool show) +{ + showSuffixOnEdit = show; +} + + +void +TimeEditDelegate::setShowSuffixOnDisplay +(bool show) +{ + showSuffixOnDisplay = show; +} + + +void +TimeEditDelegate::setTimeRange +(const QTime &min, const QTime &max) +{ + this->min = min; + this->max = max; +} + + +QWidget* +TimeEditDelegate::createEditor +(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + QTimeEdit *timeEdit = new QTimeEdit(parent); + QString f = format; + if (f.isEmpty()) { + f = "mm:ss"; + } + if (showSuffixOnEdit && ! suffix.isEmpty()) { + f += " '" + suffix + "'"; + } + timeEdit->setDisplayFormat(f); + if (min.isValid()) { + timeEdit->setMinimumTime(min); + } + if (max.isValid()) { + timeEdit->setMaximumTime(max); + } + + return timeEdit; +} + + +void +TimeEditDelegate::setEditorData +(QWidget *editor, const QModelIndex &index) const +{ + QTimeEdit *timeEdit = static_cast(editor); + timeEdit->setTime(index.data(Qt::DisplayRole).toTime()); +} + + +void +TimeEditDelegate::setModelData +(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + QTimeEdit *timeEdit = static_cast(editor); + QTime newValue = timeEdit->time(); + if (model->data(index, Qt::EditRole).toTime() != newValue) { + model->setData(index, newValue, Qt::EditRole); + } +} + + +QString +TimeEditDelegate::displayText +(const QVariant &value, const QLocale &locale) const +{ + Q_UNUSED(locale) + + QString f = format; + if (f.isEmpty()) { + f = "mm:ss"; + } + if (showSuffixOnDisplay && ! suffix.isEmpty()) { + f += " '" + suffix + "'"; + } + + return value.toTime().toString(f); +} + + +QSize +TimeEditDelegate::sizeHint +(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + QTimeEdit widget; + widget.setTime(QTime(0, 0, 0)); + QString f = format; + if (f.isEmpty()) { + f = "mm:ss"; + } + if ((showSuffixOnEdit || showSuffixOnDisplay) && ! suffix.isEmpty()) { + f += " '" + suffix + "'"; + } + widget.setDisplayFormat(f); + return widget.sizeHint(); +} diff --git a/src/Gui/StyledItemDelegates.h b/src/Gui/StyledItemDelegates.h index 90fbd20a9..319099d10 100644 --- a/src/Gui/StyledItemDelegates.h +++ b/src/Gui/StyledItemDelegates.h @@ -2,6 +2,7 @@ #define STYLEDITEMSDELEGATES_H #include +#include class NoEditDelegate: public QStyledItemDelegate @@ -25,4 +26,113 @@ private: int uniqueColumn; }; + + +class SpinBoxEditDelegate: public QStyledItemDelegate +{ +public: + SpinBoxEditDelegate(QObject *parent = nullptr); + + void setMininum(int minimum); + void setMaximum(int maximum); + void setRange(int minimum, int maximum); + void setSingleStep(int val); + void setSuffix(const QString &suffix); + void setShowSuffixOnEdit(bool show); + void setShowSuffixOnDisplay(bool show); + + virtual QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual QString displayText(const QVariant &value, const QLocale &locale) const override; + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +private: + int minimum = 0; + int maximum = 100; + int singleStep = 1; + QString suffix; + bool showSuffixOnEdit = true; + bool showSuffixOnDisplay = true; +}; + + + +class DoubleSpinBoxEditDelegate: public QStyledItemDelegate +{ +public: + DoubleSpinBoxEditDelegate(QObject *parent = nullptr); + + void setMininum(double minimum); + void setMaximum(double maximum); + void setRange(double minimum, double maximum); + void setSingleStep(double val); + void setDecimals(int prec); + void setSuffix(const QString &suffix); + void setShowSuffixOnEdit(bool show); + void setShowSuffixOnDisplay(bool show); + + virtual QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual QString displayText(const QVariant &value, const QLocale &locale) const override; + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +private: + double minimum = 0; + double maximum = 100; + double singleStep = 1; + double prec = 0; + QString suffix; + bool showSuffixOnEdit = true; + bool showSuffixOnDisplay = true; +}; + + + +class DateEditDelegate: public QStyledItemDelegate +{ +public: + DateEditDelegate(QObject *parent = nullptr); + + void setCalendarPopup(bool enable); + + virtual QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual QString displayText(const QVariant &value, const QLocale &locale) const override; + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +private: + bool calendarPopup = false; +}; + + + +class TimeEditDelegate: public QStyledItemDelegate +{ +public: + TimeEditDelegate(QObject *parent = nullptr); + + void setFormat(const QString &format); + void setSuffix(const QString &suffix); + void setShowSuffixOnEdit(bool show); + void setShowSuffixOnDisplay(bool show); + void setTimeRange(const QTime &min, const QTime &max); + + virtual QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + virtual QString displayText(const QVariant &value, const QLocale &locale) const override; + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +private: + QString format; + QString suffix; + bool showSuffixOnEdit = true; + bool showSuffixOnDisplay = true; + QTime min; + QTime max; +}; + #endif diff --git a/src/Metrics/PaceZones.cpp b/src/Metrics/PaceZones.cpp index bc8925634..d95799ec7 100644 --- a/src/Metrics/PaceZones.cpp +++ b/src/Metrics/PaceZones.cpp @@ -1043,7 +1043,17 @@ PaceZones::kphFromTime(QTimeEdit *cvedit, bool metric) const // get the value from a time edit and convert // it to kph so we can store it in the zones file - double secs = cvedit->time().secsTo(QTime(0,0,0)) * -1; + return kphFromTime(cvedit->time(), metric); +} + + +double +PaceZones::kphFromTime +(const QTime &time, bool metric) const +{ + // convert a time to kph so we can store it in the zones file + + double secs = QTime(0, 0, 0).secsTo(time); if (secs == 0) return 0; // avoid division by zero if (swim) return (metric ? 1.00f : METERS_PER_YARD ) * (360.00f / secs); @@ -1051,12 +1061,20 @@ PaceZones::kphFromTime(QTimeEdit *cvedit, bool metric) const return (metric ? 1.00f : KM_PER_MILE ) * (3600.00f / secs); } + QString PaceZones::kphToPaceString(double kph, bool metric) const { return kphToPace(kph, metric, swim); } + +QTime +PaceZones::kphToPaceTime(double kph, bool metric) const +{ + return ::kphToPaceTime(kph, metric, swim); +} + QString PaceZones::paceUnits(bool metric) const { diff --git a/src/Metrics/PaceZones.h b/src/Metrics/PaceZones.h index 5200a8f06..8633d4372 100644 --- a/src/Metrics/PaceZones.h +++ b/src/Metrics/PaceZones.h @@ -216,7 +216,9 @@ class PaceZones : public QObject // convert to/from Pace double kphFromTime(QTimeEdit *cvedit, bool metric) const; + double kphFromTime(const QTime &time, bool metric) const; QString kphToPaceString(double kph, bool metric) const; + QTime kphToPaceTime(double kph, bool metric) const; QString paceUnits(bool metric) const; QString paceSetting() const; static bool isPaceUnit(QString unit); diff --git a/src/Metrics/Zones.cpp b/src/Metrics/Zones.cpp index 7159c8f5a..677cccffe 100644 --- a/src/Metrics/Zones.cpp +++ b/src/Metrics/Zones.cpp @@ -1123,3 +1123,9 @@ Zones::useCPforFTPSetting() const { return GC_USE_CP_FOR_FTP + ((sport_.isEmpty() || sport_ == "Bike") ? "" : sport_.toLower()); } + +QString +Zones::useCPModelSetting() const +{ + return GC_USE_CP_MODEL + ((sport_.isEmpty() || sport_ == "Bike") ? "" : sport_.toLower()); +} diff --git a/src/Metrics/Zones.h b/src/Metrics/Zones.h index 6d7e5dd05..4c0ef2f1d 100644 --- a/src/Metrics/Zones.h +++ b/src/Metrics/Zones.h @@ -220,6 +220,8 @@ class Zones : public QObject // USE_CP_FOR_FTP setting differenciated by sport QString useCPforFTPSetting() const; + + QString useCPModelSetting() const; }; QColor zoneColor(int zone, int num_zones); diff --git a/src/Resources/application.qrc b/src/Resources/application.qrc index 76cb23423..2460e96fc 100644 --- a/src/Resources/application.qrc +++ b/src/Resources/application.qrc @@ -39,6 +39,7 @@ images/toolbar/main/train.png images/toolbar/clear.png images/toolbar/edit.png + images/toolbar/info.png images/toolbar/select.png images/toolbar/new doc.png images/toolbar/properties.png diff --git a/unittests/season/season.pro b/unittests/Core/season/season.pro similarity index 74% rename from unittests/season/season.pro rename to unittests/Core/season/season.pro index 1abdab4fb..0911d6842 100644 --- a/unittests/season/season.pro +++ b/unittests/Core/season/season.pro @@ -4,4 +4,4 @@ SOURCES = testSeason.cpp GC_OBJS = Season \ Utils -include(../unittests.pri) +include(../../unittests.pri) diff --git a/unittests/season/testSeason.cpp b/unittests/Core/season/testSeason.cpp similarity index 100% rename from unittests/season/testSeason.cpp rename to unittests/Core/season/testSeason.cpp diff --git a/unittests/seasonOffset/seasonOffset.pro b/unittests/Core/seasonOffset/seasonOffset.pro similarity index 71% rename from unittests/seasonOffset/seasonOffset.pro rename to unittests/Core/seasonOffset/seasonOffset.pro index d4db54e91..2b525e181 100644 --- a/unittests/seasonOffset/seasonOffset.pro +++ b/unittests/Core/seasonOffset/seasonOffset.pro @@ -3,4 +3,4 @@ QT += testlib widgets SOURCES = testSeasonOffset.cpp GC_OBJS = Season -include(../unittests.pri) +include(../../unittests.pri) diff --git a/unittests/seasonOffset/testSeasonOffset.cpp b/unittests/Core/seasonOffset/testSeasonOffset.cpp similarity index 100% rename from unittests/seasonOffset/testSeasonOffset.cpp rename to unittests/Core/seasonOffset/testSeasonOffset.cpp diff --git a/unittests/seasonParser/seasonParser.pro b/unittests/Core/seasonParser/seasonParser.pro similarity index 82% rename from unittests/seasonParser/seasonParser.pro rename to unittests/Core/seasonParser/seasonParser.pro index 1acc6e416..d984093d9 100644 --- a/unittests/seasonParser/seasonParser.pro +++ b/unittests/Core/seasonParser/seasonParser.pro @@ -6,4 +6,4 @@ GC_OBJS = Seasons \ Season \ Utils -include(../unittests.pri) +include(../../unittests.pri) diff --git a/unittests/seasonParser/seasons.xml b/unittests/Core/seasonParser/seasons.xml similarity index 100% rename from unittests/seasonParser/seasons.xml rename to unittests/Core/seasonParser/seasons.xml diff --git a/unittests/seasonParser/testSeasonParser.cpp b/unittests/Core/seasonParser/testSeasonParser.cpp similarity index 100% rename from unittests/seasonParser/testSeasonParser.cpp rename to unittests/Core/seasonParser/testSeasonParser.cpp diff --git a/unittests/Core/units/testUnits.cpp b/unittests/Core/units/testUnits.cpp new file mode 100644 index 000000000..fd7f6ca86 --- /dev/null +++ b/unittests/Core/units/testUnits.cpp @@ -0,0 +1,56 @@ +#include "Core/Units.h" + +#include + + +class TestUnits: public QObject +{ + Q_OBJECT + +private slots: + void kphToPaceTimeRunTrash() { + QCOMPARE(kphToPaceTime(0.01, true), QTime(0, 0, 0)); + QCOMPARE(kphToPaceTime(100, true), QTime()); + QCOMPARE(kphToPaceTime(0.01, false), QTime(0, 0, 0)); + QCOMPARE(kphToPaceTime(160, false), QTime()); + } + + void kphToPaceTimeRunMetric() { + QCOMPARE(kphToPaceTime(30, true), QTime(0, 2, 0)); + QCOMPARE(kphToPaceTime(24, true), QTime(0, 2, 30)); + QCOMPARE(kphToPaceTime(15, true), QTime(0, 4, 0)); + QCOMPARE(kphToPaceTime(13.8, true), QTime(0, 4, 21)); + } + + void kphToPaceTimeRunImperial() { + QCOMPARE(kphToPaceTime(30, false), QTime(0, 3, 13)); + QCOMPARE(kphToPaceTime(24, false), QTime(0, 4, 1)); + QCOMPARE(kphToPaceTime(15, false), QTime(0, 6, 26)); + QCOMPARE(kphToPaceTime(13.8, false), QTime(0, 7, 0)); + } + + void kphToPaceStringRunTrash() { + QCOMPARE(kphToPace(0.01, true), "00:00"); + QCOMPARE(kphToPace(100, true), "xx:xx"); + QCOMPARE(kphToPace(0.01, false), "00:00"); + QCOMPARE(kphToPace(160, false), "xx:xx"); + } + + void kphToPaceStringRunMetric() { + QCOMPARE(kphToPace(30, true), "02:00"); + QCOMPARE(kphToPace(24, true), "02:30"); + QCOMPARE(kphToPace(15, true), "04:00"); + QCOMPARE(kphToPace(13.8, true), "04:21"); + } + + void kphToPaceStringRunImperial() { + QCOMPARE(kphToPace(30, false), "03:13"); + QCOMPARE(kphToPace(24, false), "04:01"); + QCOMPARE(kphToPace(15, false), "06:26"); + QCOMPARE(kphToPace(13.8, false), "07:00"); + } +}; + + +QTEST_MAIN(TestUnits) +#include "testUnits.moc" diff --git a/unittests/Core/units/units.pro b/unittests/Core/units/units.pro new file mode 100644 index 000000000..d4320ec46 --- /dev/null +++ b/unittests/Core/units/units.pro @@ -0,0 +1,6 @@ +QT += testlib core + +SOURCES = testUnits.cpp +GC_OBJS = Units + +include(../../unittests.pri) diff --git a/unittests/unittests.pri.in b/unittests/unittests.pri.in index 155cc0124..ff6c22d9d 100644 --- a/unittests/unittests.pri.in +++ b/unittests/unittests.pri.in @@ -5,7 +5,7 @@ GC_UNITTESTS = active include(../src/gcconfig.pri) -GC_SRC_DIR = ../../src +GC_SRC_DIR = ../../../src GC_OBJECTS_DIR = $$GC_SRC_DIR/$(OBJECTS_DIR) INCLUDEPATH += $$GC_SRC_DIR win32 { diff --git a/unittests/unittests.pro b/unittests/unittests.pro index 163faab6c..91aa4901a 100644 --- a/unittests/unittests.pro +++ b/unittests/unittests.pro @@ -5,9 +5,10 @@ exists(unittests.pri) { } equals(GC_UNITTESTS, active) { - SUBDIRS += seasonOffset \ - season \ - seasonParser + SUBDIRS += Core/seasonOffset \ + Core/season \ + Core/seasonParser \ + Core/units CONFIG += ordered } else { message("Unittests are disabled; to enable copy unittests/unittests.pri.in to unittests/unittests.pri")