From 24269f0dbf0cb8a19f7103eb9f0281aa7233da4c Mon Sep 17 00:00:00 2001 From: Joachim Kohlhammer Date: Tue, 6 Jan 2026 19:41:10 +0100 Subject: [PATCH] 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. --- src/Charts/AgendaWindow.cpp | 218 +++++++++++++++--------------- src/Charts/AgendaWindow.h | 23 ++-- src/Gui/Agenda.cpp | 53 ++++---- src/Gui/Agenda.h | 30 ++-- src/Gui/CalendarData.h | 17 +++ src/Gui/CalendarItemDelegates.cpp | 49 +++++-- src/Gui/CalendarItemDelegates.h | 12 +- src/Gui/MetricSelect.cpp | 1 + 8 files changed, 226 insertions(+), 177 deletions(-) diff --git a/src/Charts/AgendaWindow.cpp b/src/Charts/AgendaWindow.cpp index c91040b2e..0d4b5800e 100644 --- a/src/Charts/AgendaWindow.cpp +++ b/src/Charts/AgendaWindow.cpp @@ -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> +QHash> AgendaWindow::getActivities (const QDate &firstDay, const QDate &today, const QDate &lastDay) const { - QHash> activities; + QHash> 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 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(""); } } - 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> +std::pair, QList> AgendaWindow::getPhases (const Season &season, const QDate &firstDay) const { - QList ongoingPhases; - QList futurePhases; + QList ongoingPhases; + QList 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(phase.getType()) - static_cast(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> +QHash> AgendaWindow::getEvents (const QDate &firstDay) const { - QHash> events; + QHash> events; QList 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> activities = getActivities(agendaView->firstVisibleDay(), agendaView->selectedDate(), agendaView->lastVisibleDay()); - std::pair, QList> phases; - QHash> events; + QHash> activities = getActivities(agendaView->firstVisibleDay(), agendaView->selectedDate(), agendaView->lastVisibleDay()); + std::pair, QList> phases; + QHash> 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 diff --git a/src/Charts/AgendaWindow.h b/src/Charts/AgendaWindow.h index 79411eb0c..1d3edbfa3 100644 --- a/src/Charts/AgendaWindow.h +++ b/src/Charts/AgendaWindow.h @@ -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> getActivities(const QDate &firstDay, const QDate &today, const QDate &lastDay) const; - std::pair, QList> getPhases(const Season &season, const QDate &firstDay) const; - QHash> getEvents(const QDate &firstDay) const; + QHash> getActivities(const QDate &firstDay, const QDate &today, const QDate &lastDay) const; + std::pair, QList> getPhases(const Season &season, const QDate &firstDay) const; + QHash> 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 diff --git a/src/Gui/Agenda.cpp b/src/Gui/Agenda.cpp index 447944407..87bc58d27 100644 --- a/src/Gui/Agenda.cpp +++ b/src/Gui/Agenda.cpp @@ -377,7 +377,7 @@ ActivityTree::ActivityTree QVariant data = item->data(1, AgendaEntryDelegate::EntryRole); if (! data.isNull()) { - CalendarEntry entry = data.value(); + AgendaEntry entry = data.value(); if (entry.type == ENTRY_TYPE_PLANNED_ACTIVITY) { emit viewActivity(entry); } @@ -406,7 +406,7 @@ ActivityTree::setFutureDays void ActivityTree::fillEntries -(const QHash> &activities) +(const QHash> &activities) { QDate today = selectedDate(); QDate pastFirst = today.addDays(-pastDays); @@ -417,13 +417,13 @@ ActivityTree::fillEntries bool todayHasPlanned = false; QList missedDays; QList upcomingDays; - QHash> missedEntries; - QList todayEntries; - QHash> upcomingEntries; + QHash> missedEntries; + QList todayEntries; + QHash> upcomingEntries; for (QDate date = pastFirst; date <= futureLast; date = date.addDays(1)) { - QList dateEntries; + QList 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(); + AgendaEntry entry = entryData.value(); 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 &activities, QTreeWidgetItem *parent, const Fonts &fonts) +(const QDate &today, const QDate &date, const QList &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> &phases) +(const std::pair, QList> &phases) { QDate today = selectedDate(); if (! today.isValid()) { @@ -665,7 +666,7 @@ PhaseTree::createContextMenu if (entryData.isNull()) { return nullptr; } - CalendarEntry entry = entryData.value(); + AgendaEntry entry = entryData.value(); if (entry.type != ENTRY_TYPE_PHASE) { return nullptr; } @@ -679,9 +680,9 @@ PhaseTree::createContextMenu void PhaseTree::addEntries -(const QDate &today, const QList &phases, QTreeWidgetItem *parent, const Fonts &fonts) +(const QDate &today, const QList &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> &events) +(const QHash> &events) { QDate today = selectedDate(); if (! today.isValid()) { @@ -807,7 +808,7 @@ EventTree::createContextMenu if (entryData.isNull()) { return nullptr; } - CalendarEntry entry = entryData.value(); + AgendaEntry entry = entryData.value(); if (entry.type != ENTRY_TYPE_EVENT) { return nullptr; } @@ -821,10 +822,10 @@ EventTree::createContextMenu void EventTree::addEntries -(const QDate &today, const QList &events, QTreeWidgetItem *parent, const Fonts &fonts) +(const QDate &today, const QList &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> &activities, std::pair, QList> &phases , const QHash> &events, const QString &seasonName, bool isFiltered) +(const QHash> &activities, std::pair, QList> &phases , const QHash> &events, const QString &seasonName, bool isFiltered) { if (! seasonName.isNull()) { seasonLabel->setText(tr("Season: %1").arg(seasonName)); diff --git a/src/Gui/Agenda.h b/src/Gui/Agenda.h index 04225e493..0a2ace96b 100644 --- a/src/Gui/Agenda.h +++ b/src/Gui/Agenda.h @@ -92,13 +92,13 @@ public: void setPastDays(int days); void setFutureDays(int days); - void fillEntries(const QHash> &activities); + void fillEntries(const QHash> &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 &activities, QTreeWidgetItem *parent, const Fonts &fonts); + void addEntries(const QDate &today, const QDate &date, const QList &activities, QTreeWidgetItem *parent, const Fonts &fonts); }; @@ -115,16 +115,16 @@ class PhaseTree : public AgendaTree { Q_OBJECT public: - void fillEntries(const std::pair, QList> &phases); + void fillEntries(const std::pair, QList> &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 &phases, QTreeWidgetItem *parent, const Fonts &fonts); + void addEntries(const QDate &today, const QList &phases, QTreeWidgetItem *parent, const Fonts &fonts); }; @@ -132,16 +132,16 @@ class EventTree : public AgendaTree { Q_OBJECT public: - void fillEntries(const QHash> &events); + void fillEntries(const QHash> &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 &phases, QTreeWidgetItem *parent, const Fonts &fonts); + void addEntries(const QDate &today, const QList &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> &activities, std::pair, QList> &phases, const QHash> &events, const QString &seasonName, bool isFiltered); + void fillEntries(const QHash> &activities, std::pair, QList> &phases, const QHash> &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: diff --git a/src/Gui/CalendarData.h b/src/Gui/CalendarData.h index cd45da001..876291326 100644 --- a/src/Gui/CalendarData.h +++ b/src/Gui/CalendarData.h @@ -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> keyValues; }; +Q_DECLARE_METATYPE(AgendaEntry) Q_DECLARE_METATYPE(CalendarEntry) Q_DECLARE_METATYPE(CalendarEntryLayout) Q_DECLARE_METATYPE(CalendarDay) diff --git a/src/Gui/CalendarItemDelegates.cpp b/src/Gui/CalendarItemDelegates.cpp index 775f1f903..b6c305fbb 100644 --- a/src/Gui/CalendarItemDelegates.cpp +++ b/src/Gui/CalendarItemDelegates.cpp @@ -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(); + AgendaEntry entry = index.data(EntryRole).value(); 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(); + AgendaEntry entry = data.value(); 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(); + AgendaEntry entry = index.data(EntryRole).value(); 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; } diff --git a/src/Gui/CalendarItemDelegates.h b/src/Gui/CalendarItemDelegates.h index a102e6487..aa57d5c27 100644 --- a/src/Gui/CalendarItemDelegates.h +++ b/src/Gui/CalendarItemDelegates.h @@ -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 diff --git a/src/Gui/MetricSelect.cpp b/src/Gui/MetricSelect.cpp index cf98adc8d..2c0eab131 100644 --- a/src/Gui/MetricSelect.cpp +++ b/src/Gui/MetricSelect.cpp @@ -199,6 +199,7 @@ MultiMetricSelector::MultiMetricSelector : QWidget(parent) { filterEdit = new QLineEdit(); + filterEdit->setClearButtonEnabled(true); filterEdit->setPlaceholderText(tr("Filter...")); availList = new QListWidget();