mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 08:08:42 +00:00
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:
committed by
GitHub
parent
b5fe5a32c1
commit
56ff6ff73e
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user