Multi Metrics support for Agenda (#4793)

Agenda used to display entries similar to the Calendar: Primary line for
a field, secondary line for one metric, tertiary line for a multi-line
field.
As the agenda has more space available per line, this change adds
support for multiple metrics in the secondary line. Defaults are
Duration and TriScore.
This commit is contained in:
Joachim Kohlhammer
2026-01-06 19:41:10 +01:00
committed by GitHub
parent 6fde458393
commit 24269f0dbf
8 changed files with 226 additions and 177 deletions

View File

@@ -48,7 +48,6 @@ AgendaWindow::AgendaWindow(Context *context)
setAgendaFutureDays(7);
setPrimaryMainField("Route");
setPrimaryFallbackField("Workout Code");
setSecondaryMetric("workout_time");
setShowSecondaryLabel(true);
setTertiaryField("Notes");
setShowTertiaryFor(0);
@@ -69,7 +68,7 @@ AgendaWindow::AgendaWindow(Context *context)
connect(context, &Context::rideDeleted, this, &AgendaWindow::updateActivitiesIfInRange);
connect(context, &Context::rideChanged, this, &AgendaWindow::updateActivitiesIfInRange);
connect(context, &Context::configChanged, this, &AgendaWindow::configChanged);
connect(agendaView, &AgendaView::showInTrainMode, this, [context](const CalendarEntry &activity) {
connect(agendaView, &AgendaView::showInTrainMode, this, [context](const AgendaEntry &activity) {
for (RideItem *rideItem : context->athlete->rideCache->rides()) {
if (rideItem != nullptr && rideItem->fileName == activity.reference) {
QString filter = buildWorkoutFilter(rideItem);
@@ -82,7 +81,7 @@ AgendaWindow::AgendaWindow(Context *context)
}
}
});
connect(agendaView, &AgendaView::viewActivity, this, [context](const CalendarEntry &activity) {
connect(agendaView, &AgendaView::viewActivity, this, [context](const AgendaEntry &activity) {
for (RideItem *rideItem : context->athlete->rideCache->rides()) {
if (rideItem != nullptr && rideItem->fileName == activity.reference) {
context->notifyRideSelected(rideItem);
@@ -181,18 +180,34 @@ AgendaWindow::setPrimaryFallbackField
void
AgendaWindow::setSecondaryMetric
(const QString &name)
AgendaWindow::setSecondaryMetrics
(const QString &metrics)
{
secondaryCombo->setCurrentIndex(std::max(0, secondaryCombo->findData(name)));
multiMetricSelector->setSymbols(metrics.split(',', Qt::SkipEmptyParts));
}
void
AgendaWindow::setSecondaryMetrics
(const QStringList &metrics)
{
multiMetricSelector->setSymbols(metrics);
}
QString
AgendaWindow::getSecondaryMetric
AgendaWindow::getSecondaryMetrics
() const
{
return secondaryCombo->currentData(Qt::UserRole).toString();
return multiMetricSelector->getSymbols().join(',');
}
QStringList
AgendaWindow::getSecondaryMetricsList
() const
{
return multiMetricSelector->getSymbols();
}
@@ -290,7 +305,7 @@ AgendaWindow::configChanged
if ( (what & CONFIG_FIELDS)
|| (what & CONFIG_USERMETRICS)) {
updatePrimaryConfigCombos();
updateSecondaryConfigCombo();
multiMetricSelector->updateMetrics();
updateTertiaryConfigCombo();
}
if (what & CONFIG_APPEARANCE) {
@@ -370,27 +385,31 @@ AgendaWindow::mkControls
agendaPastDaysSpin = new QSpinBox();
agendaPastDaysSpin->setMaximum(31);
agendaPastDaysSpin->setSuffix(" " + tr("day(s)"));
agendaFutureDaysSpin = new QSpinBox();
agendaFutureDaysSpin->setMaximum(31);
agendaFutureDaysSpin->setSuffix(" " + tr("day(s)"));
primaryMainCombo = new QComboBox();
primaryFallbackCombo = new QComboBox();
secondaryCombo = new QComboBox();
showSecondaryLabelCheck = new QCheckBox(tr("Show Label"));
QStringList summaryMetrics { "workout_time", "triscore" };
multiMetricSelector = new MultiMetricSelector(tr("Available Metrics"), tr("Selected Metrics"), summaryMetrics);
multiMetricSelector->setContentsMargins(10 * dpiXFactor, 10 * dpiYFactor, 10 * dpiXFactor, 10 * dpiYFactor);
multiMetricSelector->setMinimumHeight(300 * dpiYFactor);
QPushButton *gotoMetrics = new QPushButton(tr("Configure Metrics"));
showSecondaryLabelCheck = new QCheckBox(tr("Show Names of Metrics"));
showTertiaryForCombo = new QComboBox();
tertiaryCombo = new QComboBox();
updatePrimaryConfigCombos();
updateSecondaryConfigCombo();
showTertiaryForCombo->addItem(tr("all dates"));
showTertiaryForCombo->addItem(tr("today"));
showTertiaryForCombo->addItem(tr("no dates"));
updateTertiaryConfigCombo();
primaryMainCombo->setCurrentText("Route");
primaryFallbackCombo->setCurrentText("Workout Code");
int secondaryIndex = secondaryCombo->findData("workout_time");
if (secondaryIndex >= 0) {
secondaryCombo->setCurrentIndex(secondaryIndex);
}
activityMaxTertiaryLinesSpin = new QSpinBox();
activityMaxTertiaryLinesSpin->setRange(1, 5);
eventMaxTertiaryLinesSpin = new QSpinBox();
@@ -407,7 +426,7 @@ AgendaWindow::mkControls
activityForm->addRow(tr("Fallback Field"), primaryFallbackCombo);
activityForm->addItem(new QSpacerItem(0, 20 * dpiYFactor));
activityForm->addRow(new QLabel(HLO + tr("Metric Line") + HLC));
activityForm->addRow(tr("Metric"), secondaryCombo);
activityForm->addRow("", gotoMetrics);
activityForm->addRow("", showSecondaryLabelCheck);
activityForm->addItem(new QSpacerItem(0, 20 * dpiYFactor));
activityForm->addRow(new QLabel(HLO + tr("Detail Line") + HLC));
@@ -430,13 +449,15 @@ AgendaWindow::mkControls
QTabWidget *controlsTabs = new QTabWidget();
controlsTabs->addTab(activityScroller, tr("Activities"));
controlsTabs->addTab(multiMetricSelector, tr("Metrics"));
controlsTabs->addTab(eventScroller, tr("Events"));
connect(agendaPastDaysSpin, &QSpinBox::valueChanged, this, &AgendaWindow::setAgendaPastDays);
connect(agendaFutureDaysSpin, &QSpinBox::valueChanged, this, &AgendaWindow::setAgendaFutureDays);
connect(primaryMainCombo, &QComboBox::currentIndexChanged, this, &AgendaWindow::updateActivities);
connect(primaryFallbackCombo, &QComboBox::currentIndexChanged, this, &AgendaWindow::updateActivities);
connect(secondaryCombo, &QComboBox::currentIndexChanged, this, &AgendaWindow::updateActivities);
connect(multiMetricSelector, &MultiMetricSelector::selectedChanged, this, &AgendaWindow::updateActivities);
connect(gotoMetrics, &QPushButton::clicked, this, [controlsTabs]() { controlsTabs->setCurrentIndex(1); });
connect(showTertiaryForCombo, &QComboBox::currentIndexChanged, this, &AgendaWindow::updateActivities);
connect(tertiaryCombo, &QComboBox::currentIndexChanged, this, &AgendaWindow::updateActivities);
connect(activityMaxTertiaryLinesSpin, &QSpinBox::valueChanged, this, &AgendaWindow::setActivityMaxTertiaryLines);
@@ -473,27 +494,6 @@ AgendaWindow::updatePrimaryConfigCombos
}
void
AgendaWindow::updateSecondaryConfigCombo
()
{
QString symbol = getSecondaryMetric();
secondaryCombo->blockSignals(true);
secondaryCombo->clear();
const RideMetricFactory &factory = RideMetricFactory::instance();
for (const QString &metricSymbol : factory.allMetrics()) {
if (metricSymbol.startsWith("compatibility_")) {
continue;
}
secondaryCombo->addItem(Utils::unprotect(factory.rideMetric(metricSymbol)->name()), metricSymbol);
}
secondaryCombo->blockSignals(false);
setSecondaryMetric(symbol);
}
void
AgendaWindow::updateTertiaryConfigCombo
()
@@ -514,26 +514,33 @@ AgendaWindow::updateTertiaryConfigCombo
}
QHash<QDate, QList<CalendarEntry>>
QHash<QDate, QList<AgendaEntry>>
AgendaWindow::getActivities
(const QDate &firstDay, const QDate &today, const QDate &lastDay) const
{
QHash<QDate, QList<CalendarEntry>> activities;
QHash<QDate, QList<AgendaEntry>> activities;
const RideMetricFactory &factory = RideMetricFactory::instance();
const RideMetric *rideMetric = factory.rideMetric(getSecondaryMetric());
QString rideMetricName;
QString rideMetricUnit;
if (rideMetric != nullptr) {
rideMetricName = rideMetric->name();
if ( ! rideMetric->isTime()
&& ! rideMetric->isDate()) {
rideMetricUnit = rideMetric->units(GlobalContext::context()->useMetricUnits);
QList<RideMetric const *> rideMetrics;
QStringList rideMetricNames;
QStringList rideMetricUnits;
for (const QString &metric : getSecondaryMetricsList()) {
RideMetric const *rideMetric = factory.rideMetric(metric);
if (rideMetric != nullptr) {
rideMetrics << rideMetric;
rideMetricNames << rideMetric->name();
if ( ! rideMetric->isTime()
&& ! rideMetric->isDate()) {
rideMetricUnits << rideMetric->units(GlobalContext::context()->useMetricUnits);
} else {
rideMetricUnits << "";
}
}
}
int showTertiaryFor = getShowTertiaryFor();
for (RideItem *rideItem : context->athlete->rideCache->rides()) {
if ( rideItem == nullptr
|| ! rideItem->planned
|| rideItem->dateTime.date() < firstDay
|| rideItem->dateTime.date() > lastDay
|| rideItem->hasLinkedActivity()) {
@@ -545,7 +552,7 @@ AgendaWindow::getActivities
}
QString sport = rideItem->sport;
CalendarEntry activity;
AgendaEntry activity;
QString primaryMain = rideItem->getText(getPrimaryMainField(), "").trimmed();
if (! primaryMain.isEmpty()) {
@@ -560,42 +567,41 @@ AgendaWindow::getActivities
activity.primary = tr("<unknown>");
}
}
if (rideMetric != nullptr && rideMetric->isRelevantForRide(rideItem)) {
activity.secondary = rideItem->getStringForSymbol(getSecondaryMetric(), GlobalContext::context()->useMetricUnits);
if (! rideMetricUnit.isEmpty()) {
activity.secondary += " " + rideMetricUnit;
for (int i = 0; i < rideMetrics.count(); ++i) {
RideMetric const *rideMetric = rideMetrics[i];
if (rideMetric->isRelevantForRide(rideItem)) {
QString value = rideItem->getStringForSymbol(rideMetric->symbol(), GlobalContext::context()->useMetricUnits);
if (! rideMetricUnits.value(i, "").isEmpty()) {
value.append(" " + rideMetricUnits.value(i, ""));
}
activity.secondaryValues << Utils::unprotect(value);
if (isShowSecondaryLabel()) {
activity.secondaryLabels << Utils::unprotect(rideMetricNames.value(i, ""));
} else {
activity.secondaryLabels << "";
}
}
if (isShowSecondaryLabel()) {
activity.secondaryMetric = rideMetricName;
}
} else {
activity.secondary = tr("N/A");
activity.secondaryMetric = "";
}
if (activity.secondaryValues.count() == 0) {
activity.secondaryValues << tr("N/A");
activity.secondaryLabels << "";
}
if (showTertiaryFor == 0 || (showTertiaryFor == 1 && rideItem->dateTime.date() == today)) {
activity.tertiary = rideItem->getText(getTertiaryField(), "").trimmed();
activity.tertiary = Utils::unprotect(activity.tertiary);
}
activity.primary = Utils::unprotect(activity.primary);
activity.secondary = Utils::unprotect(activity.secondary);
activity.secondaryMetric = Utils::unprotect(activity.secondaryMetric);
activity.iconFile = IconManager::instance().getFilepath(rideItem);
if (rideItem->color.alpha() < 255 || rideItem->planned) {
activity.color = GColor(CCALPLANNED);
} else {
activity.color = rideItem->color;
}
activity.color = GColor(CCALPLANNED);
activity.reference = rideItem->fileName;
activity.start = rideItem->dateTime.time();
activity.durationSecs = rideItem->getForSymbol("workout_time", GlobalContext::context()->useMetricUnits);
activity.type = rideItem->planned ? ENTRY_TYPE_PLANNED_ACTIVITY : ENTRY_TYPE_ACTIVITY;
activity.isRelocatable = rideItem->planned;
activity.hasTrainMode = rideItem->planned && sport == "Bike" && ! buildWorkoutFilter(rideItem).isEmpty();
activity.type = ENTRY_TYPE_PLANNED_ACTIVITY;
activity.hasTrainMode = sport == "Bike" && ! buildWorkoutFilter(rideItem).isEmpty();
activities[rideItem->dateTime.date()] << activity;
}
for (auto dayIt = activities.begin(); dayIt != activities.end(); ++dayIt) {
std::sort(dayIt.value().begin(), dayIt.value().end(), [](const CalendarEntry &a, const CalendarEntry &b) {
std::sort(dayIt.value().begin(), dayIt.value().end(), [](const AgendaEntry &a, const AgendaEntry &b) {
if (a.start == b.start) {
return a.primary < b.primary;
} else {
@@ -607,12 +613,12 @@ AgendaWindow::getActivities
}
std::pair<QList<CalendarEntry>, QList<CalendarEntry>>
std::pair<QList<AgendaEntry>, QList<AgendaEntry>>
AgendaWindow::getPhases
(const Season &season, const QDate &firstDay) const
{
QList<CalendarEntry> ongoingPhases;
QList<CalendarEntry> futurePhases;
QList<AgendaEntry> ongoingPhases;
QList<AgendaEntry> futurePhases;
for (const Phase &phase : season.phases) {
if (phase.getAbsoluteStart().isValid() && phase.getAbsoluteEnd().isValid()) {
QString phaseType;
@@ -623,37 +629,36 @@ AgendaWindow::getPhases
default:
phaseType = Phase::types[static_cast<int>(phase.getType()) - static_cast<int>(Phase::phase)];
}
CalendarEntry entry;
AgendaEntry entry;
entry.primary = phase.getName();
entry.iconFile = ":images/breeze/network-mobile-100.svg";
entry.color = GColor(CCALPHASE);
entry.reference = phase.id().toString();
entry.start = QTime(0, 0, 1);
entry.type = ENTRY_TYPE_PHASE;
entry.isRelocatable = false;
entry.spanStart = phase.getAbsoluteStart();
entry.spanEnd = phase.getAbsoluteEnd();
int duration = entry.spanStart.daysTo(entry.spanEnd);
ShowDaysAsUnit unit = showDaysAs(duration);
if (unit == ShowDaysAsUnit::Days) {
if (duration > 1) {
entry.secondary = tr("%1 • %2 days").arg(phaseType).arg(duration);
entry.secondaryValues << tr("%1 • %2 days").arg(phaseType).arg(duration);
} else {
entry.secondary = tr("%1 • %2 day").arg(phaseType).arg(duration);
entry.secondaryValues << tr("%1 • %2 day").arg(phaseType).arg(duration);
}
} else if (unit == ShowDaysAsUnit::Weeks) {
duration = daysToWeeks(duration);
if (duration > 1) {
entry.secondary = tr("%1 • %2 weeks").arg(phaseType).arg(duration);
entry.secondaryValues << tr("%1 • %2 weeks").arg(phaseType).arg(duration);
} else {
entry.secondary = tr("%1 • %2 week").arg(phaseType).arg(duration);
entry.secondaryValues << tr("%1 • %2 week").arg(phaseType).arg(duration);
}
} else {
duration = daysToMonths(duration);
if (duration > 1) {
entry.secondary = tr("%1 • %2 months").arg(phaseType).arg(duration);
entry.secondaryValues << tr("%1 • %2 months").arg(phaseType).arg(duration);
} else {
entry.secondary = tr("%1 • %2 month").arg(phaseType).arg(duration);
entry.secondaryValues << tr("%1 • %2 month").arg(phaseType).arg(duration);
}
}
if (phase.getAbsoluteStart() <= firstDay && phase.getAbsoluteEnd() >= firstDay) {
@@ -663,10 +668,10 @@ AgendaWindow::getPhases
}
}
}
std::sort(ongoingPhases.begin(), ongoingPhases.end(), [](const CalendarEntry &a, const CalendarEntry &b) {
std::sort(ongoingPhases.begin(), ongoingPhases.end(), [](const AgendaEntry &a, const AgendaEntry &b) {
return a.spanEnd < b.spanEnd;
});
std::sort(futurePhases.begin(), futurePhases.end(), [](const CalendarEntry &a, const CalendarEntry &b) {
std::sort(futurePhases.begin(), futurePhases.end(), [](const AgendaEntry &a, const AgendaEntry &b) {
return a.spanStart < b.spanStart;
});
@@ -674,11 +679,11 @@ AgendaWindow::getPhases
}
QHash<QDate, QList<CalendarEntry>>
QHash<QDate, QList<AgendaEntry>>
AgendaWindow::getEvents
(const QDate &firstDay) const
{
QHash<QDate, QList<CalendarEntry>> events;
QHash<QDate, QList<AgendaEntry>> events;
QList<Season> tmpSeasons = context->athlete->seasons->seasons;
std::sort(tmpSeasons.begin(), tmpSeasons.end(), Season::LessThanForStarts);
for (const Season &s : tmpSeasons) {
@@ -686,36 +691,34 @@ AgendaWindow::getEvents
if ( ( ( firstDay.isValid()
&& event.date >= firstDay)
|| ! firstDay.isValid())) {
CalendarEntry entry;
AgendaEntry entry;
entry.primary = event.name;
if (event.priority == 0) {
entry.iconFile = ":images/breeze/task-process-4.svg";
entry.secondary = tr("Uncategorized");
entry.secondaryValues << tr("Uncategorized");
} else if (event.priority == 1) {
entry.iconFile = ":images/breeze/task-process-4.svg";
entry.secondary = tr("Category A");
entry.secondaryValues << tr("Category A");
} else if (event.priority == 2) {
entry.iconFile = ":images/breeze/task-process-3.svg";
entry.secondary = tr("Category B");
entry.secondaryValues << tr("Category B");
} else if (event.priority == 3) {
entry.iconFile = ":images/breeze/task-process-2.svg";
entry.secondary = tr("Category C");
entry.secondaryValues << tr("Category C");
} else if (event.priority == 4) {
entry.iconFile = ":images/breeze/task-process-1.svg";
entry.secondary = tr("Category D");
entry.secondaryValues << tr("Category D");
} else {
entry.iconFile = ":images/breeze/task-process-0.svg";
entry.secondary = tr("Category E");
entry.secondaryValues << tr("Category E");
}
entry.tertiary = event.description.trimmed();
entry.color = GColor(CCALEVENT);
entry.reference = event.id;
entry.start = QTime(0, 0, 0);
entry.durationSecs = 0;
entry.type = ENTRY_TYPE_EVENT;
entry.spanStart = event.date;
entry.spanEnd = event.date;
entry.isRelocatable = false;
events[event.date] << entry;
}
}
@@ -733,9 +736,9 @@ AgendaWindow::updateActivities
agendaView->updateDate();
return;
}
QHash<QDate, QList<CalendarEntry>> activities = getActivities(agendaView->firstVisibleDay(), agendaView->selectedDate(), agendaView->lastVisibleDay());
std::pair<QList<CalendarEntry>, QList<CalendarEntry>> phases;
QHash<QDate, QList<CalendarEntry>> events;
QHash<QDate, QList<AgendaEntry>> activities = getActivities(agendaView->firstVisibleDay(), agendaView->selectedDate(), agendaView->lastVisibleDay());
std::pair<QList<AgendaEntry>, QList<AgendaEntry>> phases;
QHash<QDate, QList<AgendaEntry>> events;
QString seasonName;
tertiaryCombo->setEnabled(getShowTertiaryFor() != 2);
@@ -765,7 +768,7 @@ AgendaWindow::updateActivitiesIfInRange
void
AgendaWindow::editPhaseEntry
(const CalendarEntry &entry)
(const AgendaEntry &entry)
{
if (entry.type != ENTRY_TYPE_PHASE) {
return;
@@ -799,7 +802,7 @@ AgendaWindow::editPhaseEntry
void
AgendaWindow::editEventEntry
(const CalendarEntry &entry)
(const AgendaEntry &entry)
{
if (entry.type != ENTRY_TYPE_EVENT) {
return;
@@ -810,13 +813,14 @@ AgendaWindow::editEventEntry
for (SeasonEvent &event : s.events) {
// FIXME: Ugly comparison required because SeasonEvent::id is not populated
if ( event.name == entry.primary
&& ( (event.priority == 0 && entry.secondary == tr("Uncategorized"))
|| (event.priority == 1 && entry.secondary == tr("Category A"))
|| (event.priority == 2 && entry.secondary == tr("Category B"))
|| (event.priority == 3 && entry.secondary == tr("Category C"))
|| (event.priority == 4 && entry.secondary == tr("Category D"))
&& entry.secondaryValues.count() == 1
&& ( (event.priority == 0 && entry.secondaryValues[0] == tr("Uncategorized"))
|| (event.priority == 1 && entry.secondaryValues[0] == tr("Category A"))
|| (event.priority == 2 && entry.secondaryValues[0] == tr("Category B"))
|| (event.priority == 3 && entry.secondaryValues[0] == tr("Category C"))
|| (event.priority == 4 && entry.secondaryValues[0] == tr("Category D"))
|| ( (event.priority < 0 || event.priority > 4)
&& entry.secondary == tr("Category E")))
&& entry.secondaryValues[0] == tr("Category E")))
&& event.description.trimmed() == entry.tertiary
&& event.id == entry.reference
&& event.date == entry.spanStart

View File

@@ -42,7 +42,9 @@ class AgendaWindow : public GcChartWindow
Q_PROPERTY(int agendaFutureDays READ getAgendaFutureDays WRITE setAgendaFutureDays USER true)
Q_PROPERTY(QString primaryMainField READ getPrimaryMainField WRITE setPrimaryMainField USER true)
Q_PROPERTY(QString primaryFallbackField READ getPrimaryFallbackField WRITE setPrimaryFallbackField USER true)
Q_PROPERTY(QString secondaryMetric READ getSecondaryMetric WRITE setSecondaryMetric USER true)
Q_PROPERTY(QString secondaryMetrics READ getSecondaryMetrics WRITE setSecondaryMetrics USER true)
Q_PROPERTY(bool showSecondaryLabel READ isShowSecondaryLabel WRITE setShowSecondaryLabel USER true)
Q_PROPERTY(int showTertiaryFor READ getShowTertiaryFor WRITE setShowTertiaryFor USER true)
Q_PROPERTY(QString tertiaryField READ getTertiaryField WRITE setTertiaryField USER true)
@@ -59,7 +61,8 @@ class AgendaWindow : public GcChartWindow
QString getPrimaryMainField() const;
QString getPrimaryFallbackField() const;
QString getSecondaryMetric() const;
QString getSecondaryMetrics() const;
QStringList getSecondaryMetricsList() const;
bool isShowSecondaryLabel() const;
int getShowTertiaryFor() const;
QString getTertiaryField() const;
@@ -71,7 +74,8 @@ class AgendaWindow : public GcChartWindow
void setAgendaFutureDays(int days);
void setPrimaryMainField(const QString &name);
void setPrimaryFallbackField(const QString &name);
void setSecondaryMetric(const QString &name);
void setSecondaryMetrics(const QString &metrics);
void setSecondaryMetrics(const QStringList &metrics);
void setShowSecondaryLabel(bool showSecondaryLabel);
void setShowTertiaryFor(int showFor);
void setTertiaryField(const QString &name);
@@ -90,7 +94,7 @@ class AgendaWindow : public GcChartWindow
QSpinBox *agendaFutureDaysSpin;
QComboBox *primaryMainCombo;
QComboBox *primaryFallbackCombo;
QComboBox *secondaryCombo;
MultiMetricSelector *multiMetricSelector;
QCheckBox *showSecondaryLabelCheck;
QComboBox *showTertiaryForCombo;
QComboBox *tertiaryCombo;
@@ -100,17 +104,16 @@ class AgendaWindow : public GcChartWindow
void mkControls();
void updatePrimaryConfigCombos();
void updateSecondaryConfigCombo();
void updateTertiaryConfigCombo();
QHash<QDate, QList<CalendarEntry>> getActivities(const QDate &firstDay, const QDate &today, const QDate &lastDay) const;
std::pair<QList<CalendarEntry>, QList<CalendarEntry>> getPhases(const Season &season, const QDate &firstDay) const;
QHash<QDate, QList<CalendarEntry>> getEvents(const QDate &firstDay) const;
QHash<QDate, QList<AgendaEntry>> getActivities(const QDate &firstDay, const QDate &today, const QDate &lastDay) const;
std::pair<QList<AgendaEntry>, QList<AgendaEntry>> getPhases(const Season &season, const QDate &firstDay) const;
QHash<QDate, QList<AgendaEntry>> getEvents(const QDate &firstDay) const;
private slots:
void updateActivities();
void updateActivitiesIfInRange(RideItem *rideItem);
void editPhaseEntry(const CalendarEntry &entry);
void editEventEntry(const CalendarEntry &entry);
void editPhaseEntry(const AgendaEntry &entry);
void editEventEntry(const AgendaEntry &entry);
};
#endif

View File

@@ -377,7 +377,7 @@ ActivityTree::ActivityTree
QVariant data = item->data(1, AgendaEntryDelegate::EntryRole);
if (! data.isNull()) {
CalendarEntry entry = data.value<CalendarEntry>();
AgendaEntry entry = data.value<AgendaEntry>();
if (entry.type == ENTRY_TYPE_PLANNED_ACTIVITY) {
emit viewActivity(entry);
}
@@ -406,7 +406,7 @@ ActivityTree::setFutureDays
void
ActivityTree::fillEntries
(const QHash<QDate, QList<CalendarEntry>> &activities)
(const QHash<QDate, QList<AgendaEntry>> &activities)
{
QDate today = selectedDate();
QDate pastFirst = today.addDays(-pastDays);
@@ -417,13 +417,13 @@ ActivityTree::fillEntries
bool todayHasPlanned = false;
QList<QDate> missedDays;
QList<QDate> upcomingDays;
QHash<QDate, QList<CalendarEntry>> missedEntries;
QList<CalendarEntry> todayEntries;
QHash<QDate, QList<CalendarEntry>> upcomingEntries;
QHash<QDate, QList<AgendaEntry>> missedEntries;
QList<AgendaEntry> todayEntries;
QHash<QDate, QList<AgendaEntry>> upcomingEntries;
for (QDate date = pastFirst; date <= futureLast; date = date.addDays(1)) {
QList<CalendarEntry> dateEntries;
QList<AgendaEntry> dateEntries;
dateEntries = activities.value(date);
for (const CalendarEntry &entry : dateEntries) {
for (const AgendaEntry &entry : dateEntries) {
if (entry.type == ENTRY_TYPE_PLANNED_ACTIVITY) {
if (date < today) {
if (! missedDays.contains(date)) {
@@ -547,29 +547,30 @@ ActivityTree::createContextMenu
if (entryData.isNull()) {
return nullptr;
}
CalendarEntry entry = entryData.value<CalendarEntry>();
AgendaEntry entry = entryData.value<AgendaEntry>();
if (entry.type != ENTRY_TYPE_PLANNED_ACTIVITY) {
return nullptr;
}
QMenu *contextMenu = new QMenu(this);
contextMenu->addAction(tr("View planned activity..."), this, [this, entry]() {
emit viewActivity(entry);
});
if (entry.hasTrainMode) {
contextMenu->addSeparator();
contextMenu->addAction(tr("Show in train mode..."), this, [this, entry]() {
emit showInTrainMode(entry);
});
}
contextMenu->addAction(tr("View planned activity..."), this, [this, entry]() {
emit viewActivity(entry);
});
return contextMenu;
}
void
ActivityTree::addEntries
(const QDate &today, const QDate &date, const QList<CalendarEntry> &activities, QTreeWidgetItem *parent, const Fonts &fonts)
(const QDate &today, const QDate &date, const QList<AgendaEntry> &activities, QTreeWidgetItem *parent, const Fonts &fonts)
{
int activityIdx = 0;
for (const CalendarEntry &activity : activities) {
for (const AgendaEntry &activity : activities) {
QTreeWidgetItem *entryItem = new QTreeWidgetItem();
QString diffStr;
int diff = date.daysTo(today);
@@ -607,7 +608,7 @@ ActivityTree::addEntries
void
PhaseTree::fillEntries
(const std::pair<QList<CalendarEntry>, QList<CalendarEntry>> &phases)
(const std::pair<QList<AgendaEntry>, QList<AgendaEntry>> &phases)
{
QDate today = selectedDate();
if (! today.isValid()) {
@@ -665,7 +666,7 @@ PhaseTree::createContextMenu
if (entryData.isNull()) {
return nullptr;
}
CalendarEntry entry = entryData.value<CalendarEntry>();
AgendaEntry entry = entryData.value<AgendaEntry>();
if (entry.type != ENTRY_TYPE_PHASE) {
return nullptr;
}
@@ -679,9 +680,9 @@ PhaseTree::createContextMenu
void
PhaseTree::addEntries
(const QDate &today, const QList<CalendarEntry> &phases, QTreeWidgetItem *parent, const Fonts &fonts)
(const QDate &today, const QList<AgendaEntry> &phases, QTreeWidgetItem *parent, const Fonts &fonts)
{
for (const CalendarEntry &phase : phases) {
for (const AgendaEntry &phase : phases) {
QTreeWidgetItem *entryItem = new QTreeWidgetItem();
QString diffStr;
int diffStart = today.daysTo(phase.spanStart);
@@ -760,7 +761,7 @@ PhaseTree::addEntries
void
EventTree::fillEntries
(const QHash<QDate, QList<CalendarEntry>> &events)
(const QHash<QDate, QList<AgendaEntry>> &events)
{
QDate today = selectedDate();
if (! today.isValid()) {
@@ -807,7 +808,7 @@ EventTree::createContextMenu
if (entryData.isNull()) {
return nullptr;
}
CalendarEntry entry = entryData.value<CalendarEntry>();
AgendaEntry entry = entryData.value<AgendaEntry>();
if (entry.type != ENTRY_TYPE_EVENT) {
return nullptr;
}
@@ -821,10 +822,10 @@ EventTree::createContextMenu
void
EventTree::addEntries
(const QDate &today, const QList<CalendarEntry> &events, QTreeWidgetItem *parent, const Fonts &fonts)
(const QDate &today, const QList<AgendaEntry> &events, QTreeWidgetItem *parent, const Fonts &fonts)
{
bool firstEntry = true;
for (const CalendarEntry &event : events) {
for (const AgendaEntry &event : events) {
QTreeWidgetItem *entryItem = new QTreeWidgetItem();
int diffStart = today.daysTo(event.spanStart);
QString diffStr;
@@ -899,16 +900,16 @@ AgendaView::AgendaView
activityTree = new ActivityTree();
connect(activityTree, &ActivityTree::dayChanged, this, [this](const QDate &date) { emit dayChanged(date); });
connect(activityTree, &ActivityTree::showInTrainMode, this, [this](const CalendarEntry &activity) { emit showInTrainMode(activity); });
connect(activityTree, &ActivityTree::viewActivity, this, [this](const CalendarEntry &activity) { emit viewActivity(activity); });
connect(activityTree, &ActivityTree::showInTrainMode, this, [this](const AgendaEntry &activity) { emit showInTrainMode(activity); });
connect(activityTree, &ActivityTree::viewActivity, this, [this](const AgendaEntry &activity) { emit viewActivity(activity); });
phaseTree = new PhaseTree();
connect(phaseTree, &PhaseTree::dayChanged, this, [this](const QDate &date) { emit dayChanged(date); });
connect(phaseTree, &PhaseTree::editPhaseEntry, this, [this](const CalendarEntry &phase) { emit editPhaseEntry(phase); });
connect(phaseTree, &PhaseTree::editPhaseEntry, this, [this](const AgendaEntry &phase) { emit editPhaseEntry(phase); });
eventTree = new EventTree();
connect(eventTree, &EventTree::dayChanged, this, [this](const QDate &date) { emit dayChanged(date); });
connect(eventTree, &EventTree::editEventEntry, this, [this](const CalendarEntry &event) { emit editEventEntry(event); });
connect(eventTree, &EventTree::editEventEntry, this, [this](const AgendaEntry &event) { emit editEventEntry(event); });
QGridLayout* headLayout = new QGridLayout();
headLayout->setColumnStretch(0, 1);
@@ -965,7 +966,7 @@ AgendaView::setFutureDays
void
AgendaView::fillEntries
(const QHash<QDate, QList<CalendarEntry>> &activities, std::pair<QList<CalendarEntry>, QList<CalendarEntry>> &phases , const QHash<QDate, QList<CalendarEntry>> &events, const QString &seasonName, bool isFiltered)
(const QHash<QDate, QList<AgendaEntry>> &activities, std::pair<QList<AgendaEntry>, QList<AgendaEntry>> &phases , const QHash<QDate, QList<AgendaEntry>> &events, const QString &seasonName, bool isFiltered)
{
if (! seasonName.isNull()) {
seasonLabel->setText(tr("Season: %1").arg(seasonName));

View File

@@ -92,13 +92,13 @@ public:
void setPastDays(int days);
void setFutureDays(int days);
void fillEntries(const QHash<QDate, QList<CalendarEntry>> &activities);
void fillEntries(const QHash<QDate, QList<AgendaEntry>> &activities);
QDate firstVisibleDay() const;
QDate lastVisibleDay() const;
signals:
void showInTrainMode(const CalendarEntry &activity);
void viewActivity(const CalendarEntry &activity);
void showInTrainMode(const AgendaEntry &activity);
void viewActivity(const AgendaEntry &activity);
protected:
QMenu *createContextMenu(const QModelIndex &index) override;
@@ -107,7 +107,7 @@ private:
int pastDays = 7;
int futureDays = 7;
void addEntries(const QDate &today, const QDate &date, const QList<CalendarEntry> &activities, QTreeWidgetItem *parent, const Fonts &fonts);
void addEntries(const QDate &today, const QDate &date, const QList<AgendaEntry> &activities, QTreeWidgetItem *parent, const Fonts &fonts);
};
@@ -115,16 +115,16 @@ class PhaseTree : public AgendaTree {
Q_OBJECT
public:
void fillEntries(const std::pair<QList<CalendarEntry>, QList<CalendarEntry>> &phases);
void fillEntries(const std::pair<QList<AgendaEntry>, QList<AgendaEntry>> &phases);
signals:
void editPhaseEntry(const CalendarEntry &entry);
void editPhaseEntry(const AgendaEntry &entry);
protected:
QMenu *createContextMenu(const QModelIndex &index) override;
private:
void addEntries(const QDate &today, const QList<CalendarEntry> &phases, QTreeWidgetItem *parent, const Fonts &fonts);
void addEntries(const QDate &today, const QList<AgendaEntry> &phases, QTreeWidgetItem *parent, const Fonts &fonts);
};
@@ -132,16 +132,16 @@ class EventTree : public AgendaTree {
Q_OBJECT
public:
void fillEntries(const QHash<QDate, QList<CalendarEntry>> &events);
void fillEntries(const QHash<QDate, QList<AgendaEntry>> &events);
signals:
void editEventEntry(const CalendarEntry &entry);
void editEventEntry(const AgendaEntry &entry);
protected:
virtual QMenu *createContextMenu(const QModelIndex &index);
private:
void addEntries(const QDate &today, const QList<CalendarEntry> &phases, QTreeWidgetItem *parent, const Fonts &fonts);
void addEntries(const QDate &today, const QList<AgendaEntry> &phases, QTreeWidgetItem *parent, const Fonts &fonts);
};
@@ -155,7 +155,7 @@ public:
void setDateRange(const DateRange &dateRange);
void setPastDays(int days);
void setFutureDays(int days);
void fillEntries(const QHash<QDate, QList<CalendarEntry>> &activities, std::pair<QList<CalendarEntry>, QList<CalendarEntry>> &phases, const QHash<QDate, QList<CalendarEntry>> &events, const QString &seasonName, bool isFiltered);
void fillEntries(const QHash<QDate, QList<AgendaEntry>> &activities, std::pair<QList<AgendaEntry>, QList<AgendaEntry>> &phases, const QHash<QDate, QList<AgendaEntry>> &events, const QString &seasonName, bool isFiltered);
QDate firstVisibleDay() const;
QDate lastVisibleDay() const;
@@ -166,10 +166,10 @@ public slots:
void setEventMaxTertiaryLines(int maxTertiaryLines);
signals:
void showInTrainMode(const CalendarEntry &activity);
void viewActivity(const CalendarEntry &activity);
void editPhaseEntry(const CalendarEntry &entry);
void editEventEntry(const CalendarEntry &entry);
void showInTrainMode(const AgendaEntry &activity);
void viewActivity(const AgendaEntry &activity);
void editPhaseEntry(const AgendaEntry &entry);
void editEventEntry(const AgendaEntry &entry);
void dayChanged(const QDate &date);
protected:

View File

@@ -35,6 +35,22 @@
#define ENTRY_TYPE_OTHER 99
struct AgendaEntry {
QString primary;
QStringList secondaryValues;
QStringList secondaryLabels;
QString tertiary;
QString iconFile;
QColor color;
QString reference;
QTime start;
int type = 0;
bool hasTrainMode = false;
QDate spanStart = QDate();
QDate spanEnd = QDate();
};
struct CalendarEntry {
QString primary;
QString secondary;
@@ -80,6 +96,7 @@ struct CalendarSummary {
QList<std::pair<QString, QString>> keyValues;
};
Q_DECLARE_METATYPE(AgendaEntry)
Q_DECLARE_METATYPE(CalendarEntry)
Q_DECLARE_METATYPE(CalendarEntryLayout)
Q_DECLARE_METATYPE(CalendarDay)

View File

@@ -36,7 +36,7 @@ static bool toolTipHeadlineEntry(const QPoint &pos, QAbstractItemView *view, con
static bool toolTipDayEntry(const QPoint &pos, QAbstractItemView *view, const CalendarDay &day, int idx);
static bool toolTipMore(const QPoint &pos, QAbstractItemView *view, const CalendarDay &day);
static QRect paintHeadline(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index, HitTester &headlineTester, const QString &dateFormat, int pressedEntryIdx, int leftMargin, int rightMargin, int topMargin, int lineSpacing, int radius);
static void paintMetric(QPainter *painter, const QRect &rect, const QFont::Weight &valueWeight, const QFont::Weight &labelWeight, const QString &value, const QString &label);
static int paintMetric(QPainter *painter, const QRect &rect, const QFont::Weight &valueWeight, const QFont::Weight &labelWeight, const QString &value, const QString &label, bool first = true);
//////////////////////////////////////////////////////////////////////////////
@@ -1318,7 +1318,7 @@ AgendaEntryDelegate::paint
if (column < 0) {
column = index.column();
}
CalendarEntry entry = index.data(EntryRole).value<CalendarEntry>();
AgendaEntry entry = index.data(EntryRole).value<AgendaEntry>();
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
@@ -1360,7 +1360,6 @@ AgendaEntryDelegate::paint
QFontMetrics line1FM(line1Font);
const int lineSpacing = attributes.lineSpacing * dpiYFactor;
const int lineHeight = line1FM.height();
// const int radius = 4 * dpiXFactor;
const int iconInnerSpacing = 4 * dpiXFactor;
const int iconTextSpacing = attributes.iconTextSpacing * dpiXFactor;
const int iconWidth = 2 * lineHeight + lineSpacing;
@@ -1396,9 +1395,16 @@ AgendaEntryDelegate::paint
painter->setFont(line1Font);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, primary);
painter->restore();
if (! entry.secondary.isEmpty()) {
textRect.translate(0, lineHeight + lineSpacing);
paintMetric(painter, textRect, secondaryWeight, secondaryMetricWeight, entry.secondary, entry.secondaryMetric);
textRect.translate(0, lineHeight + lineSpacing);
int advance = 0;
for (int i = 0; i < entry.secondaryValues.count(); ++i) {
QString value = entry.secondaryValues[i];
QString label = entry.secondaryLabels.value(i, "");
textRect.setLeft(textRect.left() + advance);
if (textRect.width() <= 20 * dpiXFactor) {
break;
}
advance = paintMetric(painter, textRect, secondaryWeight, secondaryMetricWeight, value, label, i == 0);
}
if (! entry.tertiary.isEmpty()) {
painter->save();
@@ -1447,7 +1453,7 @@ AgendaEntryDelegate::sizeHint
if (! data.isNull()) {
const int lineSpacing = attributes.lineSpacing * dpiYFactor;
const int lineHeight = option.fontMetrics.height();
CalendarEntry entry = data.value<CalendarEntry>();
AgendaEntry entry = data.value<AgendaEntry>();
int tertiaryHeight = 0;
if (! entry.tertiary.isEmpty()) {
const int iconWidth = 2 * lineHeight + lineSpacing;
@@ -1485,7 +1491,7 @@ AgendaEntryDelegate::hasToolTip
if (! index.isValid()) {
return false;
}
CalendarEntry entry = index.data(EntryRole).value<CalendarEntry>();
AgendaEntry entry = index.data(EntryRole).value<AgendaEntry>();
QString text(entry.tertiary.trimmed());
if (text.isEmpty()) {
return false;
@@ -1872,24 +1878,41 @@ paintHeadline
}
static void
static int
paintMetric
(QPainter *painter, const QRect &rect, const QFont::Weight &valueWeight, const QFont::Weight &labelWeight, const QString &value, const QString &label)
(QPainter *painter, const QRect &rect, const QFont::Weight &valueWeight, const QFont::Weight &labelWeight, const QString &value, const QString &label, bool first)
{
int advance = 0;
painter->save();
QFont font = painter->font();
font.setWeight(valueWeight);
painter->setFont(font);
QFontMetrics valueFM(font);
int valueWidth = valueFM.horizontalAdvance(value);
painter->drawText(rect, Qt::AlignLeft | Qt::AlignTop, value);
QString fullValue(value);
if (! first) {
fullValue.prepend("");
}
int valueWidth = valueFM.horizontalAdvance(fullValue);
advance += valueWidth;
painter->drawText(rect, Qt::AlignLeft | Qt::AlignTop, fullValue);
if (! label.isEmpty()) {
QRect labelRect(rect);
labelRect.setX(rect.x() + valueWidth + valueFM.horizontalAdvance(" "));
int spaceAdvance = valueFM.horizontalAdvance(" ");
advance += spaceAdvance;
labelRect.setX(rect.x() + valueWidth + spaceAdvance);
font.setWeight(labelWeight);
QFontMetrics labelFM(font);
int labelWidth;
painter->setFont(font);
if (labelWeight <= QFont::Light) {
QRect labelBounds = painter->boundingRect(labelRect, Qt::AlignLeft | Qt::AlignTop, label);
labelWidth = labelBounds.width();
} else {
labelWidth = labelFM.horizontalAdvance(label);
}
painter->drawText(labelRect, Qt::AlignLeft | Qt::AlignTop, label);
advance += labelWidth;
}
painter->restore();
return advance;
}

View File

@@ -239,18 +239,18 @@ class AgendaEntryDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
enum Roles {
EntryRole = Qt::UserRole, // [CalendarEntry] Entry to be displayed
EntryDateRole // [bool] Date of the CalendarEntry
EntryRole = Qt::UserRole, // [AgendaEntry] Entry to be displayed
EntryDateRole // [bool] Date of the AgendaEntry
};
struct Attributes {
QMargins padding; // Padding of the element
QFont::Weight primaryWeight = QFont::Medium; // Primary row
QFont::Weight primaryHoverWeight = QFont::DemiBold; // Primary row (hovered)
QFont::Weight secondaryWeight = QFont::Light; // Secondary row
QFont::Weight secondaryHoverWeight = QFont::DemiBold; // Secondary row (hovered)
QFont::Weight primaryHoverWeight = QFont::Medium; // Primary row (hovered)
QFont::Weight secondaryWeight = QFont::Medium; // Secondary row
QFont::Weight secondaryHoverWeight = QFont::Medium; // Secondary row (hovered)
QFont::Weight secondaryMetricWeight = QFont::ExtraLight; // Metric in the secondary row
QFont::Weight secondaryMetricHoverWeight = QFont::Normal; // Metric in the secondary row (hovered)
QFont::Weight secondaryMetricHoverWeight = QFont::ExtraLight; // Metric in the secondary row (hovered)
int lineSpacing = 2; // Vertical spacing between primary and secondary row (dpiYFactor not applied)
int iconTextSpacing = 10; // Horizontal spacing between icon and text (dpiXFactor not applied)
float tertiaryDimLevel = 0.5; // Dimming amount for tertiary row

View File

@@ -199,6 +199,7 @@ MultiMetricSelector::MultiMetricSelector
: QWidget(parent)
{
filterEdit = new QLineEdit();
filterEdit->setClearButtonEnabled(true);
filterEdit->setPlaceholderText(tr("Filter..."));
availList = new QListWidget();