Additional line for entries in calendar day-view (#4709)

* Calendar Day-View now supports showing any field as tertiary line
  (good use: Notes)
* Additional fix: Rewriting metric name (secondary line); now
  BikeScore^TM is shown instead of BikeScore&8482;
This commit is contained in:
Joachim Kohlhammer
2025-09-28 08:21:29 +02:00
committed by GitHub
parent d0d5105bd3
commit 01ab7d1500
6 changed files with 169 additions and 21 deletions

View File

@@ -59,6 +59,7 @@ PlanningCalendarWindow::PlanningCalendarWindow(Context *context)
connect(context, &Context::homeFilterChanged, this, &PlanningCalendarWindow::updateActivities); connect(context, &Context::homeFilterChanged, this, &PlanningCalendarWindow::updateActivities);
connect(context, &Context::rideAdded, this, &PlanningCalendarWindow::updateActivitiesIfInRange); connect(context, &Context::rideAdded, this, &PlanningCalendarWindow::updateActivitiesIfInRange);
connect(context, &Context::rideDeleted, this, &PlanningCalendarWindow::updateActivitiesIfInRange); connect(context, &Context::rideDeleted, this, &PlanningCalendarWindow::updateActivitiesIfInRange);
connect(context, &Context::rideChanged, this, &PlanningCalendarWindow::updateActivitiesIfInRange);
connect(context, &Context::configChanged, this, &PlanningCalendarWindow::configChanged); connect(context, &Context::configChanged, this, &PlanningCalendarWindow::configChanged);
connect(calendar, &Calendar::showInTrainMode, [=](CalendarEntry activity) { connect(calendar, &Calendar::showInTrainMode, [=](CalendarEntry activity) {
for (RideItem *rideItem : context->athlete->rideCache->rides()) { for (RideItem *rideItem : context->athlete->rideCache->rides()) {
@@ -238,6 +239,22 @@ PlanningCalendarWindow::getSecondaryMetric
} }
QString
PlanningCalendarWindow::getTertiaryField
() const
{
return tertiaryCombo->currentText();
}
void
PlanningCalendarWindow::setTertiaryField
(const QString &name)
{
tertiaryCombo->setCurrentText(name);
}
QString QString
PlanningCalendarWindow::getSummaryMetrics PlanningCalendarWindow::getSummaryMetrics
() const () const
@@ -374,14 +391,17 @@ PlanningCalendarWindow::mkControls
primaryMainCombo = new QComboBox(); primaryMainCombo = new QComboBox();
primaryFallbackCombo = new QComboBox(); primaryFallbackCombo = new QComboBox();
secondaryCombo = new QComboBox(); secondaryCombo = new QComboBox();
tertiaryCombo = new QComboBox();
updatePrimaryConfigCombos(); updatePrimaryConfigCombos();
updateSecondaryConfigCombo(); updateSecondaryConfigCombo();
updateTertiaryConfigCombo();
primaryMainCombo->setCurrentText("Route"); primaryMainCombo->setCurrentText("Route");
primaryFallbackCombo->setCurrentText("Workout Code"); primaryFallbackCombo->setCurrentText("Workout Code");
int secondaryIndex = secondaryCombo->findData("workout_time"); int secondaryIndex = secondaryCombo->findData("workout_time");
if (secondaryIndex >= 0) { if (secondaryIndex >= 0) {
secondaryCombo->setCurrentIndex(secondaryIndex); secondaryCombo->setCurrentIndex(secondaryIndex);
} }
tertiaryCombo->setCurrentText("Notes");
QStringList summaryMetrics { "ride_count", "total_distance", "coggan_tss", "workout_time" }; QStringList summaryMetrics { "ride_count", "total_distance", "coggan_tss", "workout_time" };
multiMetricSelector = new MultiMetricSelector(tr("Available Metrics"), tr("Selected Metrics"), summaryMetrics); multiMetricSelector = new MultiMetricSelector(tr("Available Metrics"), tr("Selected Metrics"), summaryMetrics);
@@ -392,6 +412,7 @@ PlanningCalendarWindow::mkControls
formLayout->addRow(tr("Field for Primary Line"), primaryMainCombo); formLayout->addRow(tr("Field for Primary Line"), primaryMainCombo);
formLayout->addRow(tr("Fallback Field for Primary Line"), primaryFallbackCombo); formLayout->addRow(tr("Fallback Field for Primary Line"), primaryFallbackCombo);
formLayout->addRow(tr("Metric for Secondary Line"), secondaryCombo); formLayout->addRow(tr("Metric for Secondary Line"), secondaryCombo);
formLayout->addRow(tr("Field for Tertiary Line (day view only)"), tertiaryCombo);
formLayout->addRow(new QLabel(HLO + tr("Summary") + HLC)); formLayout->addRow(new QLabel(HLO + tr("Summary") + HLC));
QWidget *controlsWidget = new QWidget(); QWidget *controlsWidget = new QWidget();
@@ -406,11 +427,13 @@ PlanningCalendarWindow::mkControls
connect(primaryMainCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities); connect(primaryMainCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities);
connect(primaryFallbackCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities); connect(primaryFallbackCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities);
connect(secondaryCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities); connect(secondaryCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities);
connect(tertiaryCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities);
#else #else
connect(firstDayOfWeekCombo, &QComboBox::currentIndexChanged, [=](int idx) { setFirstDayOfWeek(idx + 1); }); connect(firstDayOfWeekCombo, &QComboBox::currentIndexChanged, [=](int idx) { setFirstDayOfWeek(idx + 1); });
connect(primaryMainCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities); connect(primaryMainCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities);
connect(primaryFallbackCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities); connect(primaryFallbackCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities);
connect(secondaryCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities); connect(secondaryCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities);
connect(tertiaryCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities);
#endif #endif
connect(summaryMonthCheck, &QCheckBox::toggled, this, &PlanningCalendarWindow::setSummaryVisibleMonth); connect(summaryMonthCheck, &QCheckBox::toggled, this, &PlanningCalendarWindow::setSummaryVisibleMonth);
connect(multiMetricSelector, &MultiMetricSelector::selectedChanged, this, &PlanningCalendarWindow::updateActivities); connect(multiMetricSelector, &MultiMetricSelector::selectedChanged, this, &PlanningCalendarWindow::updateActivities);
@@ -466,6 +489,26 @@ PlanningCalendarWindow::updateSecondaryConfigCombo
} }
void
PlanningCalendarWindow::updateTertiaryConfigCombo
()
{
QString field = getTertiaryField();
tertiaryCombo->blockSignals(true);
tertiaryCombo->clear();
QList<FieldDefinition> fieldsDefs = GlobalContext::context()->rideMetadata->getFields();
for (const FieldDefinition &fieldDef : fieldsDefs) {
if (fieldDef.isTextField()) {
tertiaryCombo->addItem(fieldDef.name);
}
}
tertiaryCombo->blockSignals(false);
setTertiaryField(field);
}
QHash<QDate, QList<CalendarEntry>> QHash<QDate, QList<CalendarEntry>>
PlanningCalendarWindow::getActivities PlanningCalendarWindow::getActivities
(const QDate &firstDay, const QDate &lastDay) const (const QDate &firstDay, const QDate &lastDay) const
@@ -519,6 +562,11 @@ PlanningCalendarWindow::getActivities
activity.secondary = ""; activity.secondary = "";
activity.secondaryMetric = ""; activity.secondaryMetric = "";
} }
activity.tertiary = rideItem->getText(getTertiaryField(), "").trimmed();
activity.primary = Utils::unprotect(activity.primary);
activity.secondary = Utils::unprotect(activity.secondary);
activity.secondaryMetric = Utils::unprotect(activity.secondaryMetric);
activity.tertiary = Utils::unprotect(activity.tertiary);
activity.iconFile = IconManager::instance().getFilepath(rideItem); activity.iconFile = IconManager::instance().getFilepath(rideItem);
if (rideItem->color.alpha() < 255 || rideItem->planned) { if (rideItem->color.alpha() < 255 || rideItem->planned) {

View File

@@ -43,6 +43,7 @@ class PlanningCalendarWindow : public GcChartWindow
Q_PROPERTY(QString primaryMainField READ getPrimaryMainField WRITE setPrimaryMainField 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 primaryFallbackField READ getPrimaryFallbackField WRITE setPrimaryFallbackField USER true)
Q_PROPERTY(QString secondaryMetric READ getSecondaryMetric WRITE setSecondaryMetric USER true) Q_PROPERTY(QString secondaryMetric READ getSecondaryMetric WRITE setSecondaryMetric USER true)
Q_PROPERTY(QString tertiaryField READ getTertiaryField WRITE setTertiaryField USER true)
Q_PROPERTY(QString summaryMetrics READ getSummaryMetrics WRITE setSummaryMetrics USER true) Q_PROPERTY(QString summaryMetrics READ getSummaryMetrics WRITE setSummaryMetrics USER true)
public: public:
@@ -56,6 +57,7 @@ class PlanningCalendarWindow : public GcChartWindow
QString getPrimaryMainField() const; QString getPrimaryMainField() const;
QString getPrimaryFallbackField() const; QString getPrimaryFallbackField() const;
QString getSecondaryMetric() const; QString getSecondaryMetric() const;
QString getTertiaryField() const;
QString getSummaryMetrics() const; QString getSummaryMetrics() const;
QStringList getSummaryMetricsList() const; QStringList getSummaryMetricsList() const;
@@ -65,6 +67,7 @@ class PlanningCalendarWindow : public GcChartWindow
void setPrimaryMainField(const QString &name); void setPrimaryMainField(const QString &name);
void setPrimaryFallbackField(const QString &name); void setPrimaryFallbackField(const QString &name);
void setSecondaryMetric(const QString &name); void setSecondaryMetric(const QString &name);
void setTertiaryField(const QString &name);
void setSummaryMetrics(const QString &summaryMetrics); void setSummaryMetrics(const QString &summaryMetrics);
void setSummaryMetrics(const QStringList &summaryMetrics); void setSummaryMetrics(const QStringList &summaryMetrics);
void configChanged(qint32); void configChanged(qint32);
@@ -78,12 +81,14 @@ class PlanningCalendarWindow : public GcChartWindow
QComboBox *primaryMainCombo; QComboBox *primaryMainCombo;
QComboBox *primaryFallbackCombo; QComboBox *primaryFallbackCombo;
QComboBox *secondaryCombo; QComboBox *secondaryCombo;
QComboBox *tertiaryCombo;
MultiMetricSelector *multiMetricSelector; MultiMetricSelector *multiMetricSelector;
Calendar *calendar; Calendar *calendar;
void mkControls(); void mkControls();
void updatePrimaryConfigCombos(); void updatePrimaryConfigCombos();
void updateSecondaryConfigCombo(); void updateSecondaryConfigCombo();
void updateTertiaryConfigCombo();
QHash<QDate, QList<CalendarEntry>> getActivities(const QDate &firstDay, const QDate &lastDay) const; QHash<QDate, QList<CalendarEntry>> getActivities(const QDate &firstDay, const QDate &lastDay) const;
QList<CalendarSummary> getSummaries(const QDate &firstDay, const QDate &lastDay, int timeBucketSize = 7) const; QList<CalendarSummary> getSummaries(const QDate &firstDay, const QDate &lastDay, int timeBucketSize = 7) const;
QHash<QDate, QList<CalendarEntry>> getPhasesEvents(const Season &season, const QDate &firstDay, const QDate &lastDay) const; QHash<QDate, QList<CalendarEntry>> getPhasesEvents(const Season &season, const QDate &firstDay, const QDate &lastDay) const;

View File

@@ -39,6 +39,7 @@ struct CalendarEntry {
QString primary; QString primary;
QString secondary; QString secondary;
QString secondaryMetric; QString secondaryMetric;
QString tertiary;
QString iconFile; QString iconFile;
QColor color; QColor color;
QString reference; QString reference;

View File

@@ -151,7 +151,7 @@ TimeScaleData::timeFromY
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
// CalendarDayViewDayDelegate // ColumnDelegatingItemDelegate
ColumnDelegatingItemDelegate::ColumnDelegatingItemDelegate ColumnDelegatingItemDelegate::ColumnDelegatingItemDelegate
(QList<QStyledItemDelegate*> delegates, QObject *parent) (QList<QStyledItemDelegate*> delegates, QObject *parent)
@@ -312,15 +312,15 @@ CalendarDayViewDayDelegate::paint(QPainter *painter, const QStyleOptionViewItem
painter->restore(); painter->restore();
// Activities // Activities
const int columnSpacing = 10 * dpiXFactor;
const int lineSpacing = 2 * dpiYFactor;
const int iconSpacing = 2 * dpiXFactor;
const int radius = 4 * dpiXFactor;
const int horPadding = 4 * dpiXFactor;
QFont entryFont = painter->font(); QFont entryFont = painter->font();
entryFont.setPointSize(entryFont.pointSize() * 0.95); entryFont.setPointSize(entryFont.pointSize() * 0.95);
QFontMetrics entryFM(entryFont); QFontMetrics entryFM(entryFont);
int lineHeight = entryFM.height(); const int columnSpacing = 10 * dpiXFactor;
const int horPadding = 4 * dpiXFactor;
const int radius = 4 * dpiXFactor;
const int iconSpacing = 2 * dpiXFactor;
const int priSecSpacing = 2 * dpiXFactor;
const int lineHeight = entryFM.height();
painter->setFont(entryFont); painter->setFont(entryFont);
int rectLeft = option.rect.left() + columnSpacing; int rectLeft = option.rect.left() + columnSpacing;
@@ -338,7 +338,7 @@ CalendarDayViewDayDelegate::paint(QPainter *painter, const QStyleOptionViewItem
int top = timeScaleData->minuteToY(startMinute, option.rect); int top = timeScaleData->minuteToY(startMinute, option.rect);
int bottom = timeScaleData->minuteToY(endMinute, option.rect); int bottom = timeScaleData->minuteToY(endMinute, option.rect);
int height = std::max(1, bottom - top); int height = std::max(1, bottom - top);
int numLines = (height + lineSpacing) / (lineHeight + lineSpacing); int numLines = (height + priSecSpacing) / (lineHeight + priSecSpacing);
painter->save(); painter->save();
QRect entryRect(left, top, columnWidth, height); QRect entryRect(left, top, columnWidth, height);
@@ -369,8 +369,8 @@ CalendarDayViewDayDelegate::paint(QPainter *painter, const QStyleOptionViewItem
if (height >= lineHeight && columnWidth >= lineHeight) { if (height >= lineHeight && columnWidth >= lineHeight) {
QColor pixmapColor(entry.color); QColor pixmapColor(entry.color);
int headlineOffset = 0; int headlineOffset = 0;
if (height >= 2 * lineHeight + lineSpacing && columnWidth >= 2 * lineHeight + lineSpacing) { if (height >= 2 * lineHeight + priSecSpacing && columnWidth >= 2 * lineHeight + priSecSpacing) {
iconWidth = 2 * lineHeight + lineSpacing; iconWidth = 2 * lineHeight + priSecSpacing;
} else { } else {
iconWidth = lineHeight; iconWidth = lineHeight;
} }
@@ -380,9 +380,9 @@ CalendarDayViewDayDelegate::paint(QPainter *painter, const QStyleOptionViewItem
if (pressed) { if (pressed) {
pixmapColor = GCColor::invertColor(entryBG); pixmapColor = GCColor::invertColor(entryBG);
} }
pixmap = svgAsColoredPixmap(entry.iconFile, pixmapSize, lineSpacing, pixmapColor); pixmap = svgAsColoredPixmap(entry.iconFile, pixmapSize, iconSpacing, pixmapColor);
} else { } else {
pixmap = svgOnBackground(entry.iconFile, pixmapSize, lineSpacing, pixmapColor, radius); pixmap = svgOnBackground(entry.iconFile, pixmapSize, iconSpacing, pixmapColor, radius);
} }
painter->drawPixmap(left, top, pixmap); painter->drawPixmap(left, top, pixmap);
} }
@@ -391,10 +391,25 @@ CalendarDayViewDayDelegate::paint(QPainter *painter, const QStyleOptionViewItem
QRect textRect(left + iconWidth + horPadding, top, columnWidth - iconWidth - 2 * horPadding, lineHeight); QRect textRect(left + iconWidth + horPadding, top, columnWidth - iconWidth - 2 * horPadding, lineHeight);
painter->setPen(GCColor::invertColor(entryBG)); painter->setPen(GCColor::invertColor(entryBG));
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop, entry.primary); painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop, entry.primary);
--numLines; if (--numLines > 0 && ! entry.secondary.isEmpty()) {
if (numLines > 0) { textRect.translate(0, lineHeight + priSecSpacing);
textRect.translate(0, lineHeight + lineSpacing); if (! entry.secondaryMetric.isEmpty()) {
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop, entry.secondary + " (" + entry.secondaryMetric + ")"); painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop, entry.secondary + " (" + entry.secondaryMetric + ")");
} else {
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop, entry.secondary);
}
}
if (--numLines > 0 && ! entry.tertiary.isEmpty()) {
QRect tertiaryRect(left + horPadding,
top + iconWidth + priSecSpacing,
columnWidth - 2 * horPadding,
entryRect.height() - iconWidth - 2 * priSecSpacing);
QFont tertiaryFont = painter->font();
tertiaryFont.setWeight(QFont::Light);
painter->save();
painter->setFont(tertiaryFont);
drawWrappingText(*painter, tertiaryRect, entry.tertiary);
painter->restore();
} }
painter->restore(); painter->restore();
} }
@@ -468,6 +483,82 @@ CalendarDayViewDayDelegate::hitTestEntry
} }
void
CalendarDayViewDayDelegate::drawWrappingText
(QPainter &painter, const QRect &rect, const QString &text) const
{
painter.save();
const QFont font = painter.font();
QFontMetrics fm(font);
const int maxWidth = rect.width();
const qreal maxY = rect.bottom() + 0.5;
qreal y = rect.top();
const QStringList paragraphs = text.split('\n');
QList<QPair<QPoint, QString>> linesToDraw;
QString lastLineText;
QPoint lastLinePos;
bool stopDrawing = false;
bool moreTextFollowing = false;
for (int p = 0; p < paragraphs.size() && ! stopDrawing; ++p) {
const QString &para = paragraphs[p];
QTextLayout layout(para, font);
layout.beginLayout();
while (! stopDrawing) {
QTextLine line = layout.createLine();
if (! line.isValid()) {
break;
}
line.setLineWidth(maxWidth);
const qreal ascent = line.ascent();
const qreal descent = line.descent();
const qreal lineHeight = ascent + descent;
const qreal lineBottom = y + lineHeight;
if (lineBottom > maxY) {
stopDrawing = true;
moreTextFollowing = true;
break;
}
const QString lineText = para.mid(line.textStart(), line.textLength());
if (line.naturalTextWidth() > maxWidth) {
linesToDraw.clear();
stopDrawing = true;
break;
}
QPoint pos(rect.left(), std::round(y + ascent));
linesToDraw.append({ pos, lineText });
lastLineText = lineText;
lastLinePos = pos;
y += lineHeight;
}
layout.endLayout();
if (stopDrawing) {
break;
}
if (p < paragraphs.size() - 1) {
qreal nextLineHeight = fm.ascent() + fm.descent();
if (y + nextLineHeight > maxY) {
stopDrawing = true;
moreTextFollowing = true;
break;
}
}
}
if (stopDrawing && ! linesToDraw.isEmpty() && moreTextFollowing) {
QString textToElide = lastLineText;
const QString ellipsis = QStringLiteral("");
while (! textToElide.isEmpty() && fm.horizontalAdvance(textToElide + ellipsis) > maxWidth - 1) {
textToElide.chop(1);
}
textToElide += ellipsis;
linesToDraw.last() = { lastLinePos, textToElide };
}
for (const auto &line : linesToDraw) {
painter.drawText(line.first, line.second);
}
painter.restore();
}
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
// CalendarTimeScaleDelegate // CalendarTimeScaleDelegate
@@ -1014,6 +1105,8 @@ QSize
CalendarSummaryDelegate::sizeHint CalendarSummaryDelegate::sizeHint
(const QStyleOptionViewItem &option, const QModelIndex &index) const (const QStyleOptionViewItem &option, const QModelIndex &index) const
{ {
Q_UNUSED(option)
const CalendarSummary summary = index.data(Qt::UserRole).value<CalendarSummary>(); const CalendarSummary summary = index.data(Qt::UserRole).value<CalendarSummary>();
QFont font; QFont font;
font.setWeight(QFont::DemiBold); font.setWeight(QFont::DemiBold);

View File

@@ -89,6 +89,7 @@ public:
private: private:
TimeScaleData const * const timeScaleData; TimeScaleData const * const timeScaleData;
mutable QHash<QModelIndex, QList<QRect>> entryRects; mutable QHash<QModelIndex, QList<QRect>> entryRects;
void drawWrappingText(QPainter &painter, const QRect &rect, const QString &text) const;
}; };

View File

@@ -14,11 +14,11 @@ private slots:
QSKIP("Skipping test with Qt5"); QSKIP("Skipping test with Qt5");
#else #else
QList<CalendarEntry> entries = { QList<CalendarEntry> entries = {
{ "", "", "", "", Qt::red, "", QTime(9, 0), 3600 }, { "", "", "", "", "", Qt::red, "", QTime(9, 0), 3600 },
{ "", "", "", "", Qt::red, "", QTime(9, 30), 3600 }, { "", "", "", "", "", Qt::red, "", QTime(9, 30), 3600 },
{ "", "", "", "", Qt::red, "", QTime(10, 30), 1800 }, { "", "", "", "", "", Qt::red, "", QTime(10, 30), 1800 },
{ "", "", "", "", Qt::red, "", QTime(11, 0), 1800 }, { "", "", "", "", "", Qt::red, "", QTime(11, 0), 1800 },
{ "", "", "", "", Qt::red, "", QTime(11, 15), 3600 }, { "", "", "", "", "", Qt::red, "", QTime(11, 15), 3600 },
}; };
CalendarEntryLayouter layouter; CalendarEntryLayouter layouter;
QList<CalendarEntryLayout> layout = layouter.layout(entries); QList<CalendarEntryLayout> layout = layouter.layout(entries);