diff --git a/src/Charts/PlanningCalendarWindow.cpp b/src/Charts/PlanningCalendarWindow.cpp index 21f969b32..2d4e9fd1f 100644 --- a/src/Charts/PlanningCalendarWindow.cpp +++ b/src/Charts/PlanningCalendarWindow.cpp @@ -59,6 +59,7 @@ PlanningCalendarWindow::PlanningCalendarWindow(Context *context) connect(context, &Context::homeFilterChanged, this, &PlanningCalendarWindow::updateActivities); connect(context, &Context::rideAdded, 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(calendar, &Calendar::showInTrainMode, [=](CalendarEntry activity) { 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 PlanningCalendarWindow::getSummaryMetrics () const @@ -374,14 +391,17 @@ PlanningCalendarWindow::mkControls primaryMainCombo = new QComboBox(); primaryFallbackCombo = new QComboBox(); secondaryCombo = new QComboBox(); + tertiaryCombo = new QComboBox(); updatePrimaryConfigCombos(); updateSecondaryConfigCombo(); + updateTertiaryConfigCombo(); primaryMainCombo->setCurrentText("Route"); primaryFallbackCombo->setCurrentText("Workout Code"); int secondaryIndex = secondaryCombo->findData("workout_time"); if (secondaryIndex >= 0) { secondaryCombo->setCurrentIndex(secondaryIndex); } + tertiaryCombo->setCurrentText("Notes"); QStringList summaryMetrics { "ride_count", "total_distance", "coggan_tss", "workout_time" }; 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("Fallback Field for Primary Line"), primaryFallbackCombo); 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)); QWidget *controlsWidget = new QWidget(); @@ -406,11 +427,13 @@ PlanningCalendarWindow::mkControls connect(primaryMainCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities); connect(primaryFallbackCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities); connect(secondaryCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities); + connect(tertiaryCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities); #else connect(firstDayOfWeekCombo, &QComboBox::currentIndexChanged, [=](int idx) { setFirstDayOfWeek(idx + 1); }); connect(primaryMainCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities); connect(primaryFallbackCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities); connect(secondaryCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities); + connect(tertiaryCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities); #endif connect(summaryMonthCheck, &QCheckBox::toggled, this, &PlanningCalendarWindow::setSummaryVisibleMonth); 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 fieldsDefs = GlobalContext::context()->rideMetadata->getFields(); + for (const FieldDefinition &fieldDef : fieldsDefs) { + if (fieldDef.isTextField()) { + tertiaryCombo->addItem(fieldDef.name); + } + } + + tertiaryCombo->blockSignals(false); + setTertiaryField(field); +} + + QHash> PlanningCalendarWindow::getActivities (const QDate &firstDay, const QDate &lastDay) const @@ -519,6 +562,11 @@ PlanningCalendarWindow::getActivities activity.secondary = ""; 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); if (rideItem->color.alpha() < 255 || rideItem->planned) { diff --git a/src/Charts/PlanningCalendarWindow.h b/src/Charts/PlanningCalendarWindow.h index 9b7de65de..ac03eb465 100644 --- a/src/Charts/PlanningCalendarWindow.h +++ b/src/Charts/PlanningCalendarWindow.h @@ -43,6 +43,7 @@ class PlanningCalendarWindow : public GcChartWindow 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 tertiaryField READ getTertiaryField WRITE setTertiaryField USER true) Q_PROPERTY(QString summaryMetrics READ getSummaryMetrics WRITE setSummaryMetrics USER true) public: @@ -56,6 +57,7 @@ class PlanningCalendarWindow : public GcChartWindow QString getPrimaryMainField() const; QString getPrimaryFallbackField() const; QString getSecondaryMetric() const; + QString getTertiaryField() const; QString getSummaryMetrics() const; QStringList getSummaryMetricsList() const; @@ -65,6 +67,7 @@ class PlanningCalendarWindow : public GcChartWindow void setPrimaryMainField(const QString &name); void setPrimaryFallbackField(const QString &name); void setSecondaryMetric(const QString &name); + void setTertiaryField(const QString &name); void setSummaryMetrics(const QString &summaryMetrics); void setSummaryMetrics(const QStringList &summaryMetrics); void configChanged(qint32); @@ -78,12 +81,14 @@ class PlanningCalendarWindow : public GcChartWindow QComboBox *primaryMainCombo; QComboBox *primaryFallbackCombo; QComboBox *secondaryCombo; + QComboBox *tertiaryCombo; MultiMetricSelector *multiMetricSelector; Calendar *calendar; void mkControls(); void updatePrimaryConfigCombos(); void updateSecondaryConfigCombo(); + void updateTertiaryConfigCombo(); QHash> getActivities(const QDate &firstDay, const QDate &lastDay) const; QList getSummaries(const QDate &firstDay, const QDate &lastDay, int timeBucketSize = 7) const; QHash> getPhasesEvents(const Season &season, const QDate &firstDay, const QDate &lastDay) const; diff --git a/src/Gui/CalendarData.h b/src/Gui/CalendarData.h index 754023a5b..ed35ab195 100644 --- a/src/Gui/CalendarData.h +++ b/src/Gui/CalendarData.h @@ -39,6 +39,7 @@ struct CalendarEntry { QString primary; QString secondary; QString secondaryMetric; + QString tertiary; QString iconFile; QColor color; QString reference; diff --git a/src/Gui/CalendarItemDelegates.cpp b/src/Gui/CalendarItemDelegates.cpp index e9774b5c3..3a63c2bf1 100644 --- a/src/Gui/CalendarItemDelegates.cpp +++ b/src/Gui/CalendarItemDelegates.cpp @@ -151,7 +151,7 @@ TimeScaleData::timeFromY ////////////////////////////////////////////////////////////////////////////// -// CalendarDayViewDayDelegate +// ColumnDelegatingItemDelegate ColumnDelegatingItemDelegate::ColumnDelegatingItemDelegate (QList delegates, QObject *parent) @@ -312,15 +312,15 @@ CalendarDayViewDayDelegate::paint(QPainter *painter, const QStyleOptionViewItem painter->restore(); // 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(); entryFont.setPointSize(entryFont.pointSize() * 0.95); 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); int rectLeft = option.rect.left() + columnSpacing; @@ -338,7 +338,7 @@ CalendarDayViewDayDelegate::paint(QPainter *painter, const QStyleOptionViewItem int top = timeScaleData->minuteToY(startMinute, option.rect); int bottom = timeScaleData->minuteToY(endMinute, option.rect); int height = std::max(1, bottom - top); - int numLines = (height + lineSpacing) / (lineHeight + lineSpacing); + int numLines = (height + priSecSpacing) / (lineHeight + priSecSpacing); painter->save(); QRect entryRect(left, top, columnWidth, height); @@ -369,8 +369,8 @@ CalendarDayViewDayDelegate::paint(QPainter *painter, const QStyleOptionViewItem if (height >= lineHeight && columnWidth >= lineHeight) { QColor pixmapColor(entry.color); int headlineOffset = 0; - if (height >= 2 * lineHeight + lineSpacing && columnWidth >= 2 * lineHeight + lineSpacing) { - iconWidth = 2 * lineHeight + lineSpacing; + if (height >= 2 * lineHeight + priSecSpacing && columnWidth >= 2 * lineHeight + priSecSpacing) { + iconWidth = 2 * lineHeight + priSecSpacing; } else { iconWidth = lineHeight; } @@ -380,9 +380,9 @@ CalendarDayViewDayDelegate::paint(QPainter *painter, const QStyleOptionViewItem if (pressed) { pixmapColor = GCColor::invertColor(entryBG); } - pixmap = svgAsColoredPixmap(entry.iconFile, pixmapSize, lineSpacing, pixmapColor); + pixmap = svgAsColoredPixmap(entry.iconFile, pixmapSize, iconSpacing, pixmapColor); } else { - pixmap = svgOnBackground(entry.iconFile, pixmapSize, lineSpacing, pixmapColor, radius); + pixmap = svgOnBackground(entry.iconFile, pixmapSize, iconSpacing, pixmapColor, radius); } 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); painter->setPen(GCColor::invertColor(entryBG)); painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop, entry.primary); - --numLines; - if (numLines > 0) { - textRect.translate(0, lineHeight + lineSpacing); - painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop, entry.secondary + " (" + entry.secondaryMetric + ")"); + if (--numLines > 0 && ! entry.secondary.isEmpty()) { + textRect.translate(0, lineHeight + priSecSpacing); + if (! entry.secondaryMetric.isEmpty()) { + 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(); } @@ -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> linesToDraw; + QString lastLineText; + QPoint lastLinePos; + bool stopDrawing = false; + bool moreTextFollowing = false; + for (int p = 0; p < paragraphs.size() && ! stopDrawing; ++p) { + const QString ¶ = 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 @@ -1014,6 +1105,8 @@ QSize CalendarSummaryDelegate::sizeHint (const QStyleOptionViewItem &option, const QModelIndex &index) const { + Q_UNUSED(option) + const CalendarSummary summary = index.data(Qt::UserRole).value(); QFont font; font.setWeight(QFont::DemiBold); diff --git a/src/Gui/CalendarItemDelegates.h b/src/Gui/CalendarItemDelegates.h index c897fb286..61d9987e6 100644 --- a/src/Gui/CalendarItemDelegates.h +++ b/src/Gui/CalendarItemDelegates.h @@ -89,6 +89,7 @@ public: private: TimeScaleData const * const timeScaleData; mutable QHash> entryRects; + void drawWrappingText(QPainter &painter, const QRect &rect, const QString &text) const; }; diff --git a/unittests/Gui/calendarData/testCalendarData.cpp b/unittests/Gui/calendarData/testCalendarData.cpp index fa905bc4a..1384621a2 100644 --- a/unittests/Gui/calendarData/testCalendarData.cpp +++ b/unittests/Gui/calendarData/testCalendarData.cpp @@ -14,11 +14,11 @@ private slots: QSKIP("Skipping test with Qt5"); #else QList entries = { - { "", "", "", "", Qt::red, "", QTime(9, 0), 3600 }, - { "", "", "", "", Qt::red, "", QTime(9, 30), 3600 }, - { "", "", "", "", Qt::red, "", QTime(10, 30), 1800 }, - { "", "", "", "", Qt::red, "", QTime(11, 0), 1800 }, - { "", "", "", "", Qt::red, "", QTime(11, 15), 3600 }, + { "", "", "", "", "", Qt::red, "", QTime(9, 0), 3600 }, + { "", "", "", "", "", Qt::red, "", QTime(9, 30), 3600 }, + { "", "", "", "", "", Qt::red, "", QTime(10, 30), 1800 }, + { "", "", "", "", "", Qt::red, "", QTime(11, 0), 1800 }, + { "", "", "", "", "", Qt::red, "", QTime(11, 15), 3600 }, }; CalendarEntryLayouter layouter; QList layout = layouter.layout(entries);