Semi automatic creation of ranges for power zones (#4543)

* Changed the UI of CPPage to inline-editing of all values in all tables
* Added a sports-specific selector for the model (cp2, cp3, ext, manual)
* Allowed to create new ranges either manually or based on the estimated
  values of the model
* Added option to reset each ranges values to those of the selected
  model
* Added message to create a new range if todays estimate differs from
  those of the currently active one
* Refined semi automatic power zones
* Added a dialog to inspect and accept only some values while adoption
* Added info messages
  * when the model does not provide FTP or PMAX
  * that AeTP is only a very rough estimate
* Added a tolerance in comparison before proposing new values
* Using the following order for defaults when adding a new manual range
  * selected row
  * last row
  * predefined defaults
* Zones-Tab: To prevent crashes, a message is shown instead of the real
  interface if a metric refresh is ongoing
* Changed Pace- and HR-Tabs to use inline editing
* Moved the unittests into the same structure as the sourcecode
* Added a simple (incomplete) unittest for kphToPace
* Improved setting the column width
* En-/Disabling the action buttons (add, delete, ...) based on the
  contents state
* Changed the layout to prevent jumping widgets when showing / hiding
  buttons
* Fixed compiler warnings from Visual-C++
* Adopt dialog: Refined layout
* Fixed the unit of "From BPM" on HR-Page
* Set the default mode to manual
Fixes #1381
This commit is contained in:
Joachim Kohlhammer
2024-10-17 23:12:15 +02:00
committed by GitHub
parent d6e570ce3c
commit a5829c5c13
26 changed files with 2329 additions and 1001 deletions

View File

@@ -471,7 +471,7 @@ AthleteDirectoryStructure::upgradedDirectoriesHaveData() {
}
QList<PDEstimate>
Athlete::getPDEstimates()
Athlete::getPDEstimates() const
{
// returns whatever estimator has, if not running
QList<PDEstimate> 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;
}

View File

@@ -100,8 +100,9 @@ class Athlete : public QObject
CloudServiceAutoDownload *cloudAutoDownload;
// Estimates
PDEstimate getPDEstimateFor(QDate, QString model, bool wpk, QString sport);
QList<PDEstimate> getPDEstimates();
PDEstimate getPDEstimateFor(QDate, QString model, bool wpk, QString sport) const;
PDEstimate getPDEstimateClosestFor(QDate date, QString model, bool wpk, QString sport) const;
QList<PDEstimate> getPDEstimates() const;
// PMC Data
PMCData *getPMCFor(QString metricName, int stsDays = -1, int ltsDays = -1); // no Specification used!

View File

@@ -289,6 +289,7 @@
#define GC_CRANKLENGTH "<athlete-preferences>crankLength"
#define GC_WHEELSIZE "<athlete-preferences>wheelsize"
#define GC_USE_CP_FOR_FTP "<athlete-preferences>cp/useforftp" // use CP for FTP
#define GC_USE_CP_MODEL "<athlete-preferences>cp/useModel"
#define GC_NETWORKFILESTORE_FOLDER "<athlete-preferences>networkfilestore/folder" // folder to sync with
#define GC_AUTOBACKUP_FOLDER "<athlete-preferences>autobackup/folder"
#define GC_AUTOBACKUP_PERIOD "<athlete-preferences>autobackup/period" // how often is the Athlete Folder backuped up / 0 == never

View File

@@ -19,18 +19,18 @@
#include "Units.h"
#include <cmath>
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)

View File

@@ -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 <QString>
#include <QTime>
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

View File

@@ -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("<center><h1>%1</h1>%2</center>")
.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

File diff suppressed because it is too large Load Diff

View File

@@ -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<int> &roles = QVector<int>());
#else
void rangeChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles = QList<int>());
#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<QCheckBox*> 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<QString, LTPage*> 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<int> &roles = QVector<int>());
#else
void rangeChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles = QList<int>());
#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

View File

@@ -1,5 +1,8 @@
#include "StyledItemDelegates.h"
#include <QDoubleSpinBox>
#include <QDateEdit>
// 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<QSpinBox*>(editor);
spinbox->setValue(value);
}
void
SpinBoxEditDelegate::setModelData
(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
QSpinBox *spinbox = static_cast<QSpinBox*>(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<QDoubleSpinBox*>(editor);
spinbox->setValue(value);
}
void
DoubleSpinBoxEditDelegate::setModelData
(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
QDoubleSpinBox *spinbox = static_cast<QDoubleSpinBox*>(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<QDateEdit*>(editor);
dateEdit->setDate(value);
}
void
DateEditDelegate::setModelData
(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
QDateEdit *dateEdit = static_cast<QDateEdit*>(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<QTimeEdit*>(editor);
timeEdit->setTime(index.data(Qt::DisplayRole).toTime());
}
void
TimeEditDelegate::setModelData
(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
QTimeEdit *timeEdit = static_cast<QTimeEdit*>(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();
}

View File

@@ -2,6 +2,7 @@
#define STYLEDITEMSDELEGATES_H
#include <QStyledItemDelegate>
#include <QTime>
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

View File

@@ -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
{

View File

@@ -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);

View File

@@ -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());
}

View File

@@ -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);

View File

@@ -39,6 +39,7 @@
<file>images/toolbar/main/train.png</file>
<file>images/toolbar/clear.png</file>
<file>images/toolbar/edit.png</file>
<file>images/toolbar/info.png</file>
<file>images/toolbar/select.png</file>
<file>images/toolbar/new doc.png</file>
<file>images/toolbar/properties.png</file>

View File

@@ -4,4 +4,4 @@ SOURCES = testSeason.cpp
GC_OBJS = Season \
Utils
include(../unittests.pri)
include(../../unittests.pri)

View File

@@ -3,4 +3,4 @@ QT += testlib widgets
SOURCES = testSeasonOffset.cpp
GC_OBJS = Season
include(../unittests.pri)
include(../../unittests.pri)

View File

@@ -6,4 +6,4 @@ GC_OBJS = Seasons \
Season \
Utils
include(../unittests.pri)
include(../../unittests.pri)

View File

@@ -0,0 +1,56 @@
#include "Core/Units.h"
#include <QTest>
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"

View File

@@ -0,0 +1,6 @@
QT += testlib core
SOURCES = testUnits.cpp
GC_OBJS = Units
include(../../unittests.pri)

View File

@@ -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 {

View File

@@ -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")