Add planned workouts / activities (#4666)

* Extended the ManualActivityWizard by a flow to add planned activities
* Changed the grouping of fields on the pages to allow code reuse
  between completed and planned activities
* Added a summary page to Manual ActivityWizard (both planned and
  completed activities)
* Added an additional menu entry "Plan Activity..." to Activity menu
* Allowing WorkoutFilterBox to be used as widget in any dialog without
  modifying the context
* Added option to InfoWidget to use it without parts that can modify the
  TrainDB (rating and tags)
* See https://groups.google.com/g/golden-cheetah-users/c/Yz8g2J1Ue6w/m/U5OXTBv3BQAJ
This commit is contained in:
Joachim Kohlhammer
2025-07-04 16:22:04 +02:00
committed by GitHub
parent b5fe5a32c1
commit 56ff6ff73e
10 changed files with 1166 additions and 366 deletions

View File

@@ -1121,7 +1121,7 @@ QIcon colouredIconFromPNG(QString filename, QColor color)
QPixmap
svgAsColouredPixmap
svgAsColoredPixmap
(const QString &file, const QSize &size, int margin, const QColor &color)
{
QSvgRenderer renderer(file);

View File

@@ -33,8 +33,7 @@
// A selection of distinct colours, user can adjust also
extern QIcon colouredIconFromPNG(QString filename, QColor color);
extern QPixmap colouredPixmapFromPNG(QString filename, QColor color);
extern QPixmap svgAsColouredPixmap(const QString &file, const QSize &size, int margin, const QColor &color);
extern QPixmap svgAsColoredPixmap(const QString &file, const QSize &size, int margin, const QColor &color);
// dialog scaling
extern double dpiXFactor, dpiYFactor;

View File

@@ -525,6 +525,11 @@ MainWindow::MainWindow(const QDir &home)
rideMenu->addAction(tr("&Download from device..."), this, SLOT(downloadRide()), QKeySequence("Ctrl+D"));
rideMenu->addAction(tr("&Import from file..."), this, SLOT (importFile()), QKeySequence("Ctrl+I"));
rideMenu->addAction(tr("&Manual entry..."), this, SLOT(manualRide()), QKeySequence("Ctrl+M"));
QAction *actionPlan = new QAction(tr("&Plan activity..."));
connect(context, &Context::start, this, [=]() { actionPlan->setEnabled(false); }); // The dialog can change the contexts workout
connect(context, &Context::stop, this, [=]() { actionPlan->setEnabled(true); }); // temporarily which might cause unwanted effects
connect(actionPlan, &QAction::triggered, this, [=]() { planActivity(); });
rideMenu->addAction(actionPlan);
rideMenu->addSeparator ();
rideMenu->addAction(tr("&Export..."), this, SLOT(exportRide()), QKeySequence("Ctrl+E"));
rideMenu->addAction(tr("&Batch Processing..."), this, SLOT(batchProcessing()), QKeySequence("Ctrl+B"));
@@ -1676,6 +1681,14 @@ MainWindow::manualRide()
wizard.exec();
}
void
MainWindow::planActivity()
{
ManualActivityWizard wizard(currentAthleteTab->context, true);
wizard.exec();
}
void
MainWindow::batchProcessing()
{

View File

@@ -253,6 +253,7 @@ class MainWindow : public QMainWindow
void saveAllFilesSilent(Context *);
void downloadRide();
void manualRide();
void planActivity();
void exportRide();
void batchProcessing();
void generateHeatMap();

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
/*
* Copyright (c) 2009 Eric Murray (ericm@lne.com)
* 2012 Mark Liversedge (liversedge@gmail.com)
* Copyright (c) 2025 Joachim Kohlhammer (joachim.kohlhammer@gmx.de)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
@@ -30,6 +31,11 @@
#include <QWizardPage>
#include "StyledItemDelegates.h"
#include "MultiFilterProxyModel.h"
#include "WorkoutFilterBox.h"
#include "ErgFile.h"
#include "ErgFilePlot.h"
#include "InfoWidget.h"
class Context;
@@ -42,16 +48,25 @@ class ManualActivityWizard : public QWizard
enum {
Finalize = -1,
PageBasics,
PageSpecifics
PageWorkout,
PageMetrics,
PageSummary,
PageBusyRejection
};
ManualActivityWizard(Context *context, QWidget *parent = nullptr);
ManualActivityWizard(Context *context, bool plan = false, QWidget *parent = nullptr);
protected:
virtual void done(int result) override;
private:
Context *context;
bool plan = false;
void field2MetricDouble(RideFile &rideFile, const QString &fieldName, const QString &metricName) const;
void field2MetricInt(RideFile &rideFile, const QString &fieldName, const QString &metricName) const;
void field2TagString(RideFile &rideFile, const QString &fieldName, const QString &tagName) const;
void field2TagInt(RideFile &rideFile, const QString &fieldName, const QString &tagName) const;
};
@@ -60,39 +75,61 @@ class ManualActivityPageBasics : public QWizardPage
Q_OBJECT
public:
ManualActivityPageBasics(Context *context, QWidget *parent = nullptr);
ManualActivityPageBasics(Context *context, bool plan = false, QWidget *parent = nullptr);
virtual void initializePage() override;
virtual int nextId() const override;
virtual bool isComplete() const override;
private slots:
void updateVisibility();
void checkDateTime();
void sportsChanged();
private:
Context *context;
QLabel *distanceLabel;
QDoubleSpinBox *distanceEdit;
QLabel *swimDistanceLabel;
QSpinBox *swimDistanceEdit;
QLabel *durationLabel;
QTimeEdit *durationEdit;
QCheckBox *paceIntervals;
LapsEditorWidget *lapsEditor;
QLabel *averageCadenceLabel;
QSpinBox *averageCadenceEdit;
QLabel *averagePowerLabel;
QSpinBox *averagePowerEdit;
bool plan = false;
QLabel *duplicateActivityLabel;
};
class ManualActivityPageSpecifics : public QWizardPage
class ManualActivityPageWorkout : public QWizardPage
{
Q_OBJECT
public:
ManualActivityPageSpecifics(Context *context, QWidget *parent = nullptr);
ManualActivityPageWorkout(Context *context, QWidget *parent = nullptr);
virtual ~ManualActivityPageWorkout();
virtual bool eventFilter(QObject *watched, QEvent *event) override;
virtual void cleanupPage() override;
virtual void initializePage() override;
virtual int nextId() const override;
private:
Context *context = nullptr;
ErgFile *ergFile = nullptr;
QAbstractTableModel *workoutModel = nullptr;
MultiFilterProxyModel *sortModel = nullptr;
WorkoutFilterBox *workoutFilterBox;
QTreeView *workoutTree;
QStackedWidget *contentStack;
ErgFilePlot *ergFilePlot;
InfoWidget *infoWidget;
ErgFile *backupWorkout = nullptr;
void resetFields();
private slots:
void selectionChanged();
};
class ManualActivityPageMetrics : public QWizardPage
{
Q_OBJECT
public:
ManualActivityPageMetrics(Context *context, bool plan = false, QWidget *parent = nullptr);
virtual void cleanupPage() override;
virtual void initializePage() override;
@@ -104,12 +141,29 @@ class ManualActivityPageSpecifics : public QWizardPage
private:
Context *context;
bool plan = false;
std::pair<double, double> getDurationDistance() const;
private:
QComboBox *estimateBy;
QSpinBox *estimationDayEdit;
QLabel *distanceLabel;
QDoubleSpinBox *distanceEdit;
QLabel *swimDistanceLabel;
QSpinBox *swimDistanceEdit;
QLabel *durationLabel;
QTimeEdit *durationEdit;
QCheckBox *paceIntervals;
LapsEditorWidget *lapsEditor;
QLabel *stressHL;
QLabel *averageCadenceLabel;
QSpinBox *averageCadenceEdit;
QLabel *averagePowerLabel;
QSpinBox *averagePowerEdit;
QLabel *estimateByLabel;
QComboBox *estimateByEdit;
QLabel *estimationDaysLabel;
QSpinBox *estimationDaysEdit;
QLabel *workLabel;
QSpinBox *workEdit;
QLabel *bikeStressLabel;
QSpinBox *bikeStressEdit;
@@ -121,5 +175,26 @@ class ManualActivityPageSpecifics : public QWizardPage
QSpinBox *triScoreEdit;
};
#endif // _GC_ManualActivityWizard_h
class ManualActivityPageSummary : public QWizardPage
{
Q_OBJECT
public:
ManualActivityPageSummary(bool plan = false, QWidget *parent = nullptr);
virtual void cleanupPage() override;
virtual void initializePage() override;
virtual int nextId() const override;
private:
bool plan = false;
QFormLayout *form = nullptr;
bool addRowString(const QString &label, const QString &fieldName);
bool addRowInt(const QString &label, const QString &fieldName, const QString &unit = QString(), double metricFactor = 1.0);
bool addRowDouble(const QString &label, const QString &fieldName, const QString &unit = QString(), double metricFactor = 1.0);
bool addRow(const QString &label, const QString &value);
};
#endif // _GC_ManualActivityWizard_h

View File

@@ -33,20 +33,22 @@
InfoWidget::InfoWidget
(QList<QColor> powerZoneColors, QList<QString> powerZoneNames, QWidget *parent)
(QList<QColor> powerZoneColors, QList<QString> powerZoneNames, bool showRating, bool showTags, QWidget *parent)
: QFrame(parent)
{
QGridLayout *l = new QGridLayout(this);
int row = 0;
ratingWidget = new RatingWidget();
ratingWidget->setAlignment(Qt::AlignHCenter);
connect(ratingWidget, SIGNAL(rated(int)), this, SLOT(rated(int)));
QFont ratingFont = ratingWidget->font();
ratingFont.setPointSizeF(ratingFont.pointSizeF() * 1.5);
ratingWidget->setFont(ratingFont);
l->addWidget(ratingWidget, row, 0, 1, -1);
++row;
if (showRating) {
ratingWidget = new RatingWidget();
ratingWidget->setAlignment(Qt::AlignHCenter);
connect(ratingWidget, SIGNAL(rated(int)), this, SLOT(rated(int)));
QFont ratingFont = ratingWidget->font();
ratingFont.setPointSizeF(ratingFont.pointSizeF() * 1.5);
ratingWidget->setFont(ratingFont);
l->addWidget(ratingWidget, row, 0, 1, -1);
++row;
}
ErgOverview *eo = new ErgOverview();
connect(this, SIGNAL(relayErgFileSelected(ErgFileBase*)), eo, SLOT(setContent(ErgFileBase*)));
@@ -74,10 +76,12 @@ InfoWidget::InfoWidget
l->addWidget(powerZonesWidget, row, 0, 1, -1);
++row;
tagBar = new TagBar(trainDB, GCColor::invertColor(GColor(CTRAINPLOTBACKGROUND)), this);
connect(trainDB, SIGNAL(tagsChanged(int, int, int)), tagBar, SLOT(tagStoreChanged(int, int, int)));
l->addWidget(tagBar, row, 0, 1, -1);
++row;
if (showTags) {
tagBar = new TagBar(trainDB, GCColor::invertColor(GColor(CTRAINPLOTBACKGROUND)), this);
connect(trainDB, SIGNAL(tagsChanged(int, int, int)), tagBar, SLOT(tagStoreChanged(int, int, int)));
l->addWidget(tagBar, row, 0, 1, -1);
++row;
}
descriptionLabel = new QLabel();
descriptionLabel->setWordWrap(true);
@@ -109,12 +113,16 @@ void
InfoWidget::changeEvent
(QEvent *event)
{
if (event->type() == QEvent::FontChange) {
QFont ratingFont = font();
ratingFont.setPointSizeF(ratingFont.pointSizeF() * 1.5);
ratingWidget->setFont(ratingFont);
if (ratingWidget != nullptr) {
if (event->type() == QEvent::FontChange) {
QFont ratingFont = font();
ratingFont.setPointSizeF(ratingFont.pointSizeF() * 1.5);
ratingWidget->setFont(ratingFont);
}
}
if (tagBar != nullptr) {
tagBar->setColor(GCColor::invertColor(GColor(CTRAINPLOTBACKGROUND)));
}
tagBar->setColor(GCColor::invertColor(GColor(CTRAINPLOTBACKGROUND)));
}
@@ -147,7 +155,9 @@ InfoWidget::setContent
}
when = QString("<b>%1</b>").arg(when);
lastRunLabel->setText(tr("Last Run: %1").arg(when));
ratingWidget->setRating(rating);
if (ratingWidget != nullptr) {
ratingWidget->setRating(rating);
}
if (filepath == ergFileBase.filename()) {
// Stop if the workout is only reselected
@@ -158,10 +168,12 @@ InfoWidget::setContent
// Common fields
workoutTagWrapper.setFilepath(ergFileBase.filename());
if (! ergFileBase.filename().isEmpty()) {
tagBar->setTaggable(&workoutTagWrapper);
} else {
tagBar->setTaggable(nullptr);
if (tagBar != nullptr) {
if (! ergFileBase.filename().isEmpty()) {
tagBar->setTaggable(&workoutTagWrapper);
} else {
tagBar->setTaggable(nullptr);
}
}
if (! ergFileBase.description().isEmpty()) {

View File

@@ -40,7 +40,7 @@ class InfoWidget : public QFrame
Q_OBJECT
public:
InfoWidget(QList<QColor> powerZoneColors, QList<QString> powerZoneNames, QWidget *parent = nullptr);
InfoWidget(QList<QColor> powerZoneColors, QList<QString> powerZoneNames, bool showRating = true, bool showTags = true, QWidget *parent = nullptr);
~InfoWidget();
void setContent(const ErgFileBase &ergFileBase, int rating, qlonglong lastRun);
@@ -59,13 +59,13 @@ class InfoWidget : public QFrame
virtual void changeEvent(QEvent *event);
private:
RatingWidget *ratingWidget;
RatingWidget *ratingWidget = nullptr;
PowerInfoWidget *powerInfoWidget;
QLabel *slpLabel;
PowerZonesWidget *powerZonesWidget;
QLabel *descriptionLabel;
QLabel *lastRunLabel;
TagBar *tagBar;
TagBar *tagBar = nullptr;
QString filepath;
WorkoutTagWrapper workoutTagWrapper;

View File

@@ -45,10 +45,12 @@ WorkoutFilterBox::WorkoutFilterBox(QWidget *parent, Context *context) : FilterEd
this->setFilterCommands(workoutFilterCommands());
this->setMenuProvider(new WorkoutMenuProvider(this, QDir(gcroot).canonicalPath() + "/workoutfilters.xml"));
connect(this, &FilterEditor::returnPressed, this, &WorkoutFilterBox::processInput);
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
if (context != nullptr) {
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
// set appearance
configChanged(CONFIG_APPEARANCE);
// set appearance
configChanged(CONFIG_APPEARANCE);
}
}
WorkoutFilterBox::~WorkoutFilterBox()
@@ -85,18 +87,28 @@ WorkoutFilterBox::processInput()
while (input.length() > 0 && (input.back().isSpace() || input.back() == ',')) {
input.chop(1);
}
if (context) {
if (input.length() > 0) {
QList<ModelFilter *> filters = parseWorkoutFilter(input, ok, msg);
if (ok) {
bool clear = false;
if (input.length() > 0) {
QList<ModelFilter *> filters = parseWorkoutFilter(input, ok, msg);
if (ok) {
if (context != nullptr) {
context->setWorkoutFilters(filters);
} else {
workoutFilterErrorAction->setVisible(true);
workoutFilterErrorAction->setToolTip(tr("ERROR: %1").arg(msg));
context->clearWorkoutFilters();
emit workoutFiltersChanged(filters);
}
} else {
workoutFilterErrorAction->setVisible(true);
workoutFilterErrorAction->setToolTip(tr("ERROR: %1").arg(msg));
clear = true;
}
} else {
clear = true;
}
if (clear) {
if (context != nullptr) {
context->clearWorkoutFilters();
} else {
emit workoutFiltersRemoved();
}
}
if (errorActionWasVisible != workoutFilterErrorAction->isVisible()) {

View File

@@ -39,6 +39,10 @@ public slots:
void clear();
void setText(const QString &text);
signals:
void workoutFiltersChanged(QList<ModelFilter*> &f); // only emitted if no context given
void workoutFiltersRemoved(); // only emitted if no context given
private slots:
void processInput();
void configChanged(qint32 topic);