New monthly calendar (#4679)

* Created new Trends-Chart "Planning Calendar"
* Added supporting tools to Colors
* Added notification about changed season
* Updated ManualActivityWizard to optionally take the date as parameter
* Added some new icons for the calendar
* Reading normalized sport from RideItem
* Showing all events from all seasons
* Added chart-setting to configure the first day of the week
* Added chart-setting to show / hide the summary column
* Updated the appearance of planned workouts (orange icon with no background)
* Setting a pixmap next to the cursor while dragging an activity
* Added a weekly summary
* Summary and entries can be configured in chart-settings
* Replaced some material icons (phases, events, generic sport) with
  ones from breeze (https://github.com/KDE/breeze-icons)
* Updated the calendar-navigation-header
* Minor visual updates (no orange icons on blue selection, ...)
* Always showing subsport when creating a completed / planned activity
* Added "Show in Train Mode..." to Calendar
This commit is contained in:
Joachim Kohlhammer
2025-08-18 23:53:34 +02:00
committed by GitHub
parent a1008db76a
commit 49cbe3db0c
42 changed files with 3047 additions and 38 deletions

View File

@@ -0,0 +1,701 @@
/*
* Copyright (c) 2025 Joachim Kohlhammer (joachim.kohlhammer@gmx.de)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "PlanningCalendarWindow.h"
#include <QComboBox>
#include "MainWindow.h"
#include "AthleteTab.h"
#include "Seasons.h"
#include "Athlete.h"
#include "RideMetadata.h"
#include "Colors.h"
#include "ManualActivityWizard.h"
#include "WorkoutFilter.h"
#define HLO "<h4>"
#define HLC "</h4>"
PlanningCalendarWindow::PlanningCalendarWindow(Context *context)
: GcChartWindow(context), context(context)
{
mkControls();
calendar = new Calendar(QDate::currentDate(), static_cast<Qt::DayOfWeek>(getFirstDayOfWeek()));
QVBoxLayout *mainLayout = new QVBoxLayout();
setChartLayout(mainLayout);
mainLayout->addWidget(calendar);
connect(context->athlete->seasons, &Seasons::seasonsChanged, [=]() {
updateSeason(context->currentSeason(), true);
});
connect(context, &Context::seasonSelected, [=](Season const *season, bool changed) {
if (changed || first) {
first = false;
updateSeason(season, false);
}
});
connect(context, &Context::filterChanged, this, &PlanningCalendarWindow::updateActivities);
connect(context, &Context::homeFilterChanged, this, &PlanningCalendarWindow::updateActivities);
connect(context, &Context::rideAdded, this, &PlanningCalendarWindow::updateActivitiesIfInRange);
connect(context, &Context::rideDeleted, this, &PlanningCalendarWindow::updateActivitiesIfInRange);
connect(context, &Context::configChanged, this, &PlanningCalendarWindow::configChanged);
connect(calendar, &Calendar::showInTrainMode, [=](CalendarEntry activity) {
for (RideItem *rideItem : context->athlete->rideCache->rides()) {
if (rideItem != nullptr && rideItem->fileName == activity.reference) {
QString filter = buildWorkoutFilter(rideItem);
if (! filter.isEmpty()) {
context->mainWindow->fillinWorkoutFilterBox(filter);
context->mainWindow->selectTrain();
context->notifySelectWorkout(0);
}
break;
}
}
});
connect(calendar, &Calendar::viewActivity, [=](CalendarEntry activity) {
for (RideItem *rideItem : context->athlete->rideCache->rides()) {
if (rideItem != nullptr && rideItem->fileName == activity.reference) {
context->notifyRideSelected(rideItem);
context->mainWindow->selectAnalysis();
break;
}
}
});
connect(calendar, &Calendar::addActivity, [=](bool plan, const QDate &day, const QTime &time) {
context->tab->setNoSwitch(true);
ManualActivityWizard wizard(context, plan, QDateTime(day, time));
wizard.exec();
context->tab->setNoSwitch(false);
});
connect(calendar, &Calendar::delActivity, [=](CalendarEntry activity) {
context->tab->setNoSwitch(true);
context->athlete->rideCache->removeRide(activity.reference);
context->tab->setNoSwitch(false);
// Context::rideDeleted is not always emitted, therefore forcing the update
updateActivities();
});
connect(calendar, &Calendar::moveActivity, [=](CalendarEntry activity, const QDate &srcDay, const QDate &destDay) {
Q_UNUSED(srcDay)
QApplication::setOverrideCursor(Qt::WaitCursor);
for (RideItem *rideItem : context->athlete->rideCache->rides()) {
if (rideItem != nullptr && rideItem->fileName == activity.reference) {
movePlannedActivity(rideItem, destDay);
break;
}
}
QApplication::restoreOverrideCursor();
});
connect(calendar, &Calendar::insertRestday, [=](const QDate &day) {
QApplication::setOverrideCursor(Qt::WaitCursor);
QList<RideItem*> plannedRides;
for (RideItem *rideItem : context->athlete->rideCache->rides()) {
if (rideItem != nullptr && rideItem->planned && rideItem->dateTime.date() >= day) {
plannedRides << rideItem;
}
}
for (int i = plannedRides.size() - 1; i >= 0; --i) {
QDate destDay = plannedRides[i]->dateTime.date().addDays(1);
movePlannedActivity(plannedRides[i], destDay);
}
updateActivities();
QApplication::restoreOverrideCursor();
});
connect(calendar, &Calendar::delRestday, [=](const QDate &day) {
QApplication::setOverrideCursor(Qt::WaitCursor);
QList<RideItem*> plannedRides;
for (RideItem *rideItem : context->athlete->rideCache->rides()) {
if (rideItem != nullptr && rideItem->planned && rideItem->dateTime.date() >= day) {
QDate destDay = rideItem->dateTime.date().addDays(-1);
movePlannedActivity(rideItem, destDay);
}
}
QApplication::restoreOverrideCursor();
});
connect(calendar, &Calendar::monthChanged, this, &PlanningCalendarWindow::updateActivities);
configChanged(CONFIG_APPEARANCE);
}
int
PlanningCalendarWindow::getFirstDayOfWeek
() const
{
return firstDayOfWeekCombo->currentIndex() + 1;
}
bool
PlanningCalendarWindow::isSummaryVisibleMonth
() const
{
return summaryMonthCheck->isChecked();
}
void
PlanningCalendarWindow::setFirstDayOfWeek
(int fdw)
{
firstDayOfWeekCombo->setCurrentIndex(std::min(static_cast<int>(Qt::Sunday), std::max(static_cast<int>(Qt::Monday), fdw)) - 1);
calendar->setFirstDayOfWeek(static_cast<Qt::DayOfWeek>(fdw));
}
void
PlanningCalendarWindow::setSummaryVisibleMonth
(bool svm)
{
summaryMonthCheck->setChecked(svm);
calendar->setSummaryMonthVisible(svm);
}
bool
PlanningCalendarWindow::isFiltered
() const
{
return (context->ishomefiltered || context->isfiltered);
}
QString
PlanningCalendarWindow::getPrimaryMainField
() const
{
return primaryMainCombo->currentText();
}
void
PlanningCalendarWindow::setPrimaryMainField
(const QString &name)
{
primaryMainCombo->setCurrentText(name);
}
QString
PlanningCalendarWindow::getPrimaryFallbackField
() const
{
return primaryFallbackCombo->currentText();
}
void
PlanningCalendarWindow::setPrimaryFallbackField
(const QString &name)
{
primaryFallbackCombo->setCurrentText(name);
}
QString
PlanningCalendarWindow::getSecondaryMetric
() const
{
return secondaryCombo->currentData(Qt::UserRole).toString();
}
QString
PlanningCalendarWindow::getSummaryMetrics
() const
{
return multiMetricSelector->getSymbols().join(',');
}
QStringList
PlanningCalendarWindow::getSummaryMetricsList
() const
{
return multiMetricSelector->getSymbols();
}
void
PlanningCalendarWindow::setSecondaryMetric
(const QString &name)
{
secondaryCombo->setCurrentIndex(std::max(0, secondaryCombo->findData(name)));
}
void
PlanningCalendarWindow::setSummaryMetrics
(const QString &summaryMetrics)
{
multiMetricSelector->setSymbols(summaryMetrics.split(',', Qt::SkipEmptyParts));
}
void
PlanningCalendarWindow::setSummaryMetrics
(const QStringList &summaryMetrics)
{
multiMetricSelector->setSymbols(summaryMetrics);
}
void
PlanningCalendarWindow::configChanged(qint32 what)
{
bool refreshActivities = false;
if ( (what & CONFIG_FIELDS)
|| (what & CONFIG_USERMETRICS)) {
updatePrimaryConfigCombos();
updateSecondaryConfigCombo();
multiMetricSelector->updateMetrics();
}
if (what & CONFIG_APPEARANCE) {
// change colors to reflect preferences
setProperty("color", GColor(CPLOTBACKGROUND));
QColor activeBg = GColor(CPLOTBACKGROUND);
QColor activeText = GCColor::invertColor(activeBg);
QColor activeHl = GColor(CCALCURRENT);
QColor activeHlText = GCColor::invertColor(activeHl);
QColor alternateBg = GCColor::inactiveColor(activeBg, 0.3);
QColor alternateText = GCColor::inactiveColor(activeText, 1.5);
QPalette palette;
palette.setColor(QPalette::Active, QPalette::Window, activeBg);
palette.setColor(QPalette::Active, QPalette::WindowText, activeText);
palette.setColor(QPalette::Active, QPalette::Base, activeBg);
palette.setColor(QPalette::Active, QPalette::Text, activeText);
palette.setColor(QPalette::Active, QPalette::Highlight, activeHl);
palette.setColor(QPalette::Active, QPalette::HighlightedText, activeHlText);
palette.setColor(QPalette::Active, QPalette::Button, activeBg);
palette.setColor(QPalette::Active, QPalette::ButtonText, activeText);
palette.setColor(QPalette::Disabled, QPalette::Window, alternateBg);
palette.setColor(QPalette::Disabled, QPalette::WindowText, alternateText);
palette.setColor(QPalette::Disabled, QPalette::Base, alternateBg);
palette.setColor(QPalette::Disabled, QPalette::Text, alternateText);
palette.setColor(QPalette::Disabled, QPalette::Highlight, activeHl);
palette.setColor(QPalette::Disabled, QPalette::HighlightedText, activeHlText);
palette.setColor(QPalette::Disabled, QPalette::Button, alternateBg);
palette.setColor(QPalette::Disabled, QPalette::ButtonText, alternateText);
PaletteApplier::setPaletteRecursively(this, palette, true);
calendar->applyNavIcons();
refreshActivities = true;
}
if (refreshActivities) {
updateActivities();
}
}
void
PlanningCalendarWindow::mkControls
()
{
QLocale locale;
firstDayOfWeekCombo = new QComboBox();
for (int i = Qt::Monday; i <= Qt::Sunday; ++i) {
firstDayOfWeekCombo->addItem(locale.dayName(i, QLocale::LongFormat));
}
firstDayOfWeekCombo->setCurrentIndex(locale.firstDayOfWeek() - 1);
summaryMonthCheck = new QCheckBox(tr("Show weekly summary on month view"));
summaryMonthCheck->setChecked(true);
primaryMainCombo = new QComboBox();
primaryFallbackCombo = new QComboBox();
secondaryCombo = new QComboBox();
updatePrimaryConfigCombos();
updateSecondaryConfigCombo();
primaryMainCombo->setCurrentText("Route");
primaryFallbackCombo->setCurrentText("Workout Code");
int secondaryIndex = secondaryCombo->findData("workout_time");
if (secondaryIndex >= 0) {
secondaryCombo->setCurrentIndex(secondaryIndex);
}
QStringList summaryMetrics { "ride_count", "total_distance", "coggan_tss", "workout_time" };
multiMetricSelector = new MultiMetricSelector(tr("Available Metrics"), tr("Selected Metrics"), summaryMetrics);
QFormLayout *formLayout = newQFormLayout();
formLayout->addRow(tr("First day of week"), firstDayOfWeekCombo);
formLayout->addRow("", summaryMonthCheck);
formLayout->addRow(new QLabel(HLO + tr("Calendar Entries") + HLC));
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(new QLabel(HLO + tr("Summary") + HLC));
QWidget *controlsWidget = new QWidget();
QVBoxLayout *controlsLayout = new QVBoxLayout(controlsWidget);
controlsLayout->addWidget(centerLayoutInWidget(formLayout, false));
controlsLayout->addWidget(multiMetricSelector, 2);
controlsLayout->addStretch(1);
#if QT_VERSION < 0x060000
connect(firstDayOfWeekCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), [=](int idx) { setFirstDayOfWeek(idx + 1); });
connect(primaryMainCombo, 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);
#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);
#endif
connect(summaryMonthCheck, &QCheckBox::toggled, this, &PlanningCalendarWindow::setSummaryVisibleMonth);
connect(multiMetricSelector, &MultiMetricSelector::selectedChanged, this, &PlanningCalendarWindow::updateActivities);
setControls(controlsWidget);
}
void
PlanningCalendarWindow::updatePrimaryConfigCombos
()
{
QString mainField = getPrimaryMainField();
QString fallbackField = getPrimaryFallbackField();
primaryMainCombo->blockSignals(true);
primaryFallbackCombo->blockSignals(true);
primaryMainCombo->clear();
primaryFallbackCombo->clear();
QList<FieldDefinition> fieldsDefs = GlobalContext::context()->rideMetadata->getFields();
for (const FieldDefinition &fieldDef : fieldsDefs) {
if ( fieldDef.type == FIELD_TEXT
|| fieldDef.type == FIELD_TEXTBOX
|| fieldDef.type == FIELD_SHORTTEXT) {
primaryMainCombo->addItem(fieldDef.name);
primaryFallbackCombo->addItem(fieldDef.name);
}
}
primaryMainCombo->blockSignals(false);
primaryFallbackCombo->blockSignals(false);
setPrimaryMainField(mainField);
setPrimaryFallbackField(fallbackField);
}
void
PlanningCalendarWindow::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);
}
QHash<QDate, QList<CalendarEntry>>
PlanningCalendarWindow::getActivities
(const QDate &firstDay, const QDate &lastDay) const
{
QHash<QDate, QList<CalendarEntry>> 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);
}
}
for (RideItem *rideItem : context->athlete->rideCache->rides()) {
if ( rideItem->dateTime.date() < firstDay
|| rideItem->dateTime.date() > lastDay
|| rideItem == nullptr) {
continue;
}
if (context->isfiltered && ! context->filters.contains(rideItem->fileName)) {
continue;
}
QString sport = rideItem->sport;
CalendarEntry activity;
QString primaryMain = rideItem->getText(getPrimaryMainField(), "").trimmed();
if (! primaryMain.isEmpty()) {
activity.primary = primaryMain;
} else {
QString primaryFallback = rideItem->getText(getPrimaryFallbackField(), "").trimmed();
if (! primaryFallback.isEmpty()) {
activity.primary = primaryFallback;
} else if (! sport.isEmpty()) {
activity.primary = tr("Unnamed %1").arg(sport);
} else {
activity.primary = tr("<unknown>");
}
}
if (rideMetric != nullptr && rideMetric->isRelevantForRide(rideItem)) {
activity.secondary = rideItem->getStringForSymbol(getSecondaryMetric(), GlobalContext::context()->useMetricUnits);
if (! rideMetricUnit.isEmpty()) {
activity.secondary += " " + rideMetricUnit;
}
activity.secondaryMetric = rideMetricName;
} else {
activity.secondary = "";
activity.secondaryMetric = "";
}
if (sport == "Bike") {
activity.iconFile = ":images/material/bike.svg";
} else if (sport == "Run") {
activity.iconFile = ":images/material/run.svg";
} else if (sport == "Swim") {
activity.iconFile = ":images/material/swim.svg";
} else if (sport == "Row") {
activity.iconFile = ":images/material/rowing.svg";
} else if (sport == "Ski") {
activity.iconFile = ":images/material/ski.svg";
} else if (sport == "Gym") {
activity.iconFile = ":images/material/weight-lifter.svg";
} else {
activity.iconFile = ":images/breeze/games-highscores.svg";
}
if (rideItem->color.alpha() < 255 || rideItem->planned) {
activity.color = QColor("#F79130");
} else {
activity.color = rideItem->color;
}
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();
activities[rideItem->dateTime.date()] << activity;
}
return activities;
}
QList<CalendarSummary>
PlanningCalendarWindow::getWeeklySummaries
(const QDate &firstDay, const QDate &lastDay) const
{
QStringList symbols = getSummaryMetricsList();
QList<CalendarSummary> summaries;
int numWeeks = firstDay.daysTo(lastDay) / 7 + 1;
bool useMetricUnits = GlobalContext::context()->useMetricUnits;
const RideMetricFactory &factory = RideMetricFactory::instance();
FilterSet filterSet(context->isfiltered, context->filters);
Specification spec;
spec.setFilterSet(filterSet);
for (int week = 0; week < numWeeks; ++week) {
QDate firstDayOfWeek = firstDay.addDays(week * 7);
QDate lastDayOfWeek = firstDayOfWeek.addDays(6);
spec.setDateRange(DateRange(firstDayOfWeek, lastDayOfWeek));
CalendarSummary summary;
summary.keyValues.clear();
for (const QString &symbol : symbols) {
const RideMetric *metric = factory.rideMetric(symbol);
if (metric == nullptr) {
continue;
}
QString value = context->athlete->rideCache->getAggregate(symbol, spec, useMetricUnits);
if (! metric->isDate() && ! metric->isTime()) {
if (value.contains('.')) {
while (value.endsWith('0')) {
value.chop(1);
}
if (value.endsWith('.')) {
value.chop(1);
}
}
if (! metric->units(useMetricUnits).isEmpty()) {
value += " " + metric->units(useMetricUnits);
}
}
summary.keyValues << std::make_pair(Utils::unprotect(metric->name()), value);
}
summaries << summary;
}
return summaries;
}
QHash<QDate, QList<CalendarEntry>>
PlanningCalendarWindow::getPhasesEvents
(const Season &season, const QDate &firstDay, const QDate &lastDay) const
{
QHash<QDate, QList<CalendarEntry>> phasesEvents;
for (const Phase &phase : season.phases) {
if (phase.getAbsoluteStart().isValid() && phase.getAbsoluteEnd().isValid()) {
int duration = std::max(qint64(1), phase.getAbsoluteStart().daysTo(phase.getAbsoluteEnd()));
for (QDate date = phase.getAbsoluteStart(); date <= phase.getAbsoluteEnd(); date = date.addDays(1)) {
if ( ( ( firstDay.isValid()
&& date >= firstDay)
|| ! firstDay.isValid())
&& ( ( lastDay.isValid()
&& date <= lastDay)
|| ! lastDay.isValid())) {
int progress = int(phase.getAbsoluteStart().daysTo(date) / double(duration) * 5.0) * 20;
CalendarEntry entry;
entry.primary = phase.getName();
entry.secondary = "";
entry.iconFile = QString(":images/breeze/network-mobile-%1.svg").arg(progress);
entry.color = Qt::red;
entry.reference = phase.id().toString();
entry.start = QTime(0, 0, 1);
entry.type = ENTRY_TYPE_PHASE;
entry.isRelocatable = false;
entry.spanStart = phase.getStart();
entry.spanEnd = phase.getEnd();
phasesEvents[date] << entry;
}
}
}
}
QList<Season> tmpSeasons = context->athlete->seasons->seasons;
std::sort(tmpSeasons.begin(),tmpSeasons.end(),Season::LessThanForStarts);
for (const Season &s : tmpSeasons) {
for (const SeasonEvent &event : s.events) {
if ( ( ( firstDay.isValid()
&& event.date >= firstDay)
|| ! firstDay.isValid())
&& ( ( lastDay.isValid()
&& event.date <= lastDay)
|| ! lastDay.isValid())) {
CalendarEntry entry;
entry.primary = event.name;
entry.secondary = "";
if (event.priority == 0 || event.priority == 1) {
entry.iconFile = ":images/breeze/task-process-4.svg";
} else if (event.priority == 2) {
entry.iconFile = ":images/breeze/task-process-3.svg";
} else if (event.priority == 3) {
entry.iconFile = ":images/breeze/task-process-2.svg";
} else if (event.priority == 4) {
entry.iconFile = ":images/breeze/task-process-1.svg";
} else {
entry.iconFile = ":images/breeze/task-process-0.svg";
}
entry.color = Qt::yellow;
entry.reference = event.id;
entry.start = QTime(0, 0, 0);
entry.durationSecs = 0;
entry.type = ENTRY_TYPE_EVENT;
entry.isRelocatable = false;
phasesEvents[event.date] << entry;
}
}
}
return phasesEvents;
}
void
PlanningCalendarWindow::updateActivities
()
{
Season const *season = context->currentSeason();
QHash<QDate, QList<CalendarEntry>> activities = getActivities(calendar->firstVisibleDay(), calendar->lastVisibleDay());
QList<CalendarSummary> summaries = getWeeklySummaries(calendar->firstVisibleDay(), calendar->lastVisibleDay());
QHash<QDate, QList<CalendarEntry>> phasesEvents = getPhasesEvents(*season, calendar->firstVisibleDay(), calendar->lastVisibleDay());
calendar->fillEntries(activities, summaries, phasesEvents);
}
void
PlanningCalendarWindow::updateActivitiesIfInRange
(RideItem *rideItem)
{
if ( rideItem->dateTime.date() >= calendar->firstVisibleDay()
&& rideItem->dateTime.date() <= calendar->lastVisibleDay()) {
updateActivities();
}
}
void
PlanningCalendarWindow::updateSeason
(Season const *season, bool allowKeepMonth)
{
if (season == nullptr) {
DateRange dr(QDate(), QDate(), "");
calendar->activateDateRange(dr, allowKeepMonth);
} else {
DateRange dr(DateRange(season->getStart(), season->getEnd(), season->getName()));
calendar->activateDateRange(dr, allowKeepMonth);
}
}
bool
PlanningCalendarWindow::movePlannedActivity
(RideItem *rideItem, const QDate &destDay, bool force)
{
bool ret = false;
RideFile *rideFile = rideItem->ride();
QDateTime rideDateTime(destDay, rideFile->startTime().time());
rideFile->setStartTime(rideDateTime);
QString basename = rideDateTime.toString("yyyy_MM_dd_HH_mm_ss");
QString filename;
if (rideItem->planned) {
filename = context->athlete->home->planned().canonicalPath() + "/" + basename + ".json";
} else {
filename = context->athlete->home->activities().canonicalPath() + "/" + basename + ".json";
}
QFile out(filename);
if ( ( force
|| (! force && ! out.exists()))
&& RideFileFactory::instance().writeRideFile(context, rideFile, out, "json")) {
context->tab->setNoSwitch(true);
context->athlete->rideCache->removeRide(rideItem->fileName);
context->athlete->addRide(basename + ".json", true, true, false, rideItem->planned);
context->tab->setNoSwitch(false);
ret = true;
} else {
QMessageBox oops(QMessageBox::Critical,
tr("Unable to save"),
tr("There is already an activity with the same start time or you do not have permissions to save a file."));
oops.exec();
}
return ret;
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (c) 2025 Joachim Kohlhammer (joachim.kohlhammer@gmx.de)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _GC_PlanningCalendarWindow_h
#define _GC_PlanningCalendarWindow_h
#include "GoldenCheetah.h"
#include <QtGui>
#include <QCheckBox>
#include <QHash>
#include <QList>
#include <QDate>
#include "MetricSelect.h"
#include "Context.h"
#include "Season.h"
#include "Calendar.h"
#include "CalendarData.h"
class PlanningCalendarWindow : public GcChartWindow
{
Q_OBJECT
Q_PROPERTY(int firstDayOfWeek READ getFirstDayOfWeek WRITE setFirstDayOfWeek USER true)
Q_PROPERTY(bool summaryVisibleMonth READ isSummaryVisibleMonth WRITE setSummaryVisibleMonth 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 summaryMetrics READ getSummaryMetrics WRITE setSummaryMetrics USER true)
public:
PlanningCalendarWindow(Context *context);
int getFirstDayOfWeek() const;
bool isSummaryVisibleMonth() const;
bool isFiltered() const;
QString getPrimaryMainField() const;
QString getPrimaryFallbackField() const;
QString getSecondaryMetric() const;
QString getSummaryMetrics() const;
QStringList getSummaryMetricsList() const;
public slots:
void setFirstDayOfWeek(int fdw);
void setSummaryVisibleMonth(bool svm);
void setPrimaryMainField(const QString &name);
void setPrimaryFallbackField(const QString &name);
void setSecondaryMetric(const QString &name);
void setSummaryMetrics(const QString &summaryMetrics);
void setSummaryMetrics(const QStringList &summaryMetrics);
void configChanged(qint32);
private:
Context *context;
bool first = true;
QComboBox *firstDayOfWeekCombo;
QCheckBox *summaryMonthCheck;
QComboBox *primaryMainCombo;
QComboBox *primaryFallbackCombo;
QComboBox *secondaryCombo;
MultiMetricSelector *multiMetricSelector;
Calendar *calendar;
void mkControls();
void updatePrimaryConfigCombos();
void updateSecondaryConfigCombo();
QHash<QDate, QList<CalendarEntry>> getActivities(const QDate &firstDay, const QDate &lastDay) const;
QList<CalendarSummary> getWeeklySummaries(const QDate &firstDay, const QDate &lastDay) const;
QHash<QDate, QList<CalendarEntry>> getPhasesEvents(const Season &season, const QDate &firstDay, const QDate &lastDay) const;
private slots:
void updateActivities();
void updateActivitiesIfInRange(RideItem *rideItem);
void updateSeason(Season const *season, bool allowKeepMonth = false);
bool movePlannedActivity(RideItem *rideItem, const QDate &destDay, bool force = false);
};
#endif

View File

@@ -24,6 +24,7 @@
#include "CompareInterval.h" // what intervals are being compared?
#include "CompareDateRange.h" // what intervals are being compared?
#include "RideFile.h"
#include "Season.h"
#ifdef GC_HAS_CLOUD_DB
#include "CloudDBChart.h"
@@ -126,6 +127,7 @@ class Context : public QObject
RideItem *rideItem() const { return ride; }
const RideItem *currentRideItem() { return ride; }
DateRange currentDateRange() { return dr_; }
Season const *currentSeason() { return season; }
// current selections and widgetry
MainWindow * const mainWindow;
@@ -134,6 +136,7 @@ class Context : public QObject
Athlete *athlete;
RideItem *ride; // the currently selected ride
DateRange dr_;
Season const *season = nullptr;
ErgFile *workout; // the currently selected workout file
VideoSyncFile *videosync; // the currently selected videosync file
QString videoFilename;
@@ -224,6 +227,12 @@ class Context : public QObject
void notifyWorkoutsChanged() { emit workoutsChanged(); }
void notifyVideoSyncChanged() { emit VideoSyncChanged(); }
void notifySeasonChanged(Season const *season) {
bool changed = this->season != season;
this->season = season;
emit seasonSelected(season, changed);
}
void notifyRideSelected(RideItem*x) { ride=x; rideSelected(x); }
void notifyRideAdded(RideItem *x) { ride=x; rideAdded(x); }
void notifyRideDeleted(RideItem *x) { ride=x; rideDeleted(x); }
@@ -307,6 +316,8 @@ class Context : public QObject
void dateRangeSelected(DateRange);
void rideSelected(RideItem*);
void seasonSelected(Season const *season, bool changed);
// we added/deleted/changed an item
void rideAdded(RideItem *);
void rideDeleted(RideItem *);

914
src/Gui/Calendar.cpp Normal file
View File

@@ -0,0 +1,914 @@
#include "Calendar.h"
#include <QHeaderView>
#include <QHBoxLayout>
#include <QEvent>
#include <QMouseEvent>
#include <QMenu>
#include <QHash>
#include <QDrag>
#include <QMimeData>
#include <QDebug>
#if QT_VERSION < 0x060000
#include <QAbstractItemDelegate>
#endif
#include "CalendarItemDelegates.h"
#include "Colors.h"
#include "Settings.h"
//////////////////////////////////////////////////////////////////////////////
// CalendarMonthTable
CalendarMonthTable::CalendarMonthTable
(Qt::DayOfWeek firstDayOfWeek, QWidget *parent)
: CalendarMonthTable(QDate::currentDate(), firstDayOfWeek, parent)
{
}
CalendarMonthTable::CalendarMonthTable
(const QDate &dateInMonth, Qt::DayOfWeek firstDayOfWeek, QWidget *parent)
: QTableWidget(parent)
{
dragTimer.setSingleShot(true);
setAcceptDrops(true);
setColumnCount(8);
setFrameShape(QFrame::NoFrame);
setItemDelegateForColumn(7, new CalendarSummaryDelegate(this));
for (int i = 0; i < 7; ++i) {
setItemDelegateForColumn(i, new CalendarDayDelegate(this));
}
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &CalendarMonthTable::customContextMenuRequested, this, &CalendarMonthTable::showContextMenu);
setFirstDayOfWeek(firstDayOfWeek);
setMonth(dateInMonth);
}
bool
CalendarMonthTable::selectDay
(const QDate &day)
{
if (day < startDate || day > endDate) {
return false;
}
int daysAfterFirst = startDate.daysTo(day);
int row = daysAfterFirst / 7;
int col = daysAfterFirst % 7;
setCurrentCell(row, col);
return true;
}
bool
CalendarMonthTable::setMonth
(const QDate &dateInMonth, bool allowKeepMonth)
{
if (! isInDateRange(dateInMonth) && ! allowKeepMonth) {
return false;
}
if (! (allowKeepMonth && dateInMonth >= startDate && dateInMonth <= endDate) || ! firstOfMonth.isValid()) {
firstOfMonth = QDate(dateInMonth.year(), dateInMonth.month(), 1);
}
clearContents();
int startDayOfWeek = firstOfMonth.dayOfWeek();
int offset = (startDayOfWeek - firstDayOfWeek + 7) % 7;
startDate = firstOfMonth.addDays(-offset);
int daysInMonth = dateInMonth.daysInMonth();
int totalDisplayedDays = offset + daysInMonth;
int totalRows = (totalDisplayedDays + 6) / 7;
endDate = startDate.addDays(totalRows * 7 - 1);
setRowCount(totalRows);
for (int i = 0; i < totalRows * 7; ++i) {
QDate date = startDate.addDays(i);
int row = i / 7;
int col = (date.dayOfWeek() - firstDayOfWeek + 7) % 7;
QTableWidgetItem *item = new QTableWidgetItem();
item->setData(Qt::UserRole, date);
bool isDimmed = ! dr.pass(date);
CalendarDay day;
day.date = date;
day.isDimmed = isDimmed;
item->setData(Qt::UserRole + 1, QVariant::fromValue(day));
setItem(row, col, item);
}
for (int i = 0; i < 7; ++i) {
horizontalHeader()->setSectionResizeMode(i, QHeaderView::Stretch);
}
horizontalHeader()->setSectionResizeMode(7, QHeaderView::Stretch);
verticalHeader()->setSectionResizeMode(QHeaderView::Stretch);
setSelectionMode(QAbstractItemView::SingleSelection);
setEditTriggers(QAbstractItemView::NoEditTriggers);
selectDay(dateInMonth);
emit monthChanged(firstOfMonth, startDate, endDate);
return true;
}
bool
CalendarMonthTable::addMonths
(int months)
{
return setMonth(fitDate(firstOfMonth.addMonths(months)));
}
bool
CalendarMonthTable::addYears
(int years)
{
return setMonth(fitDate(firstOfMonth.addYears(years)));
}
QDate
CalendarMonthTable::fitDate
(const QDate &date) const
{
QDate newDate(date);
QDate today = QDate::currentDate();
if ( newDate.year() == today.year()
&& newDate.month() == today.month()
&& isInDateRange(today)) {
newDate = today;
} else if (! isInDateRange(newDate)) {
if (newDate < dr.to) {
newDate = QDate(newDate.year(), newDate.month(), newDate.daysInMonth());
} else {
newDate = QDate(newDate.year(), newDate.month(), 1);
}
}
return isInDateRange(newDate) ? newDate : QDate();
}
bool
CalendarMonthTable::canAddMonths
(int months) const
{
QDate fom = firstOfMonth;
QDate lom(fom.year(), fom.month(), fom.daysInMonth());
fom = fom.addMonths(months);
lom = lom.addMonths(months);
return isInDateRange(fom) || isInDateRange(lom);
}
bool
CalendarMonthTable::canAddYears
(int years) const
{
QDate fom = firstOfMonth;
QDate lom(fom.year(), fom.month(), fom.daysInMonth());
fom = fom.addYears(years);
lom = lom.addYears(years);
return isInDateRange(fom) || isInDateRange(lom);
}
bool
CalendarMonthTable::isInDateRange
(const QDate &date) const
{
return date.isValid() && dr.pass(date);
}
void
CalendarMonthTable::fillEntries
(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries)
{
for (int i = 0; i < rowCount() * 7; ++i) {
QDate date = startDate.addDays(i);
int row = i / 7;
int col = (date.dayOfWeek() - firstDayOfWeek + 7) % 7;
QTableWidgetItem *item = this->item(row, col);
CalendarDay day = item->data(Qt::UserRole + 1).value<CalendarDay>();
if (activityEntries.contains(date)) {
day.entries = activityEntries[date];
} else {
day.entries.clear();
}
if (headlineEntries.contains(date)) {
day.headlineEntries = headlineEntries[date];
} else {
day.headlineEntries.clear();
}
item->setData(Qt::UserRole + 1, QVariant::fromValue(day));
}
for (int row = 0; row < rowCount() && row < summaries.count(); ++row) {
QTableWidgetItem *summaryItem = new QTableWidgetItem();
summaryItem->setData(Qt::UserRole, QVariant::fromValue(summaries[row]));
summaryItem->setFlags(Qt::ItemIsEnabled);
setItem(row, 7, summaryItem);
}
}
QDate
CalendarMonthTable::firstOfCurrentMonth
() const
{
return firstOfMonth;
}
QDate
CalendarMonthTable::firstVisibleDay
() const
{
return startDate;
}
QDate
CalendarMonthTable::lastVisibleDay
() const
{
return endDate;
}
QDate
CalendarMonthTable::selectedDate
() const
{
QTableWidgetItem *item = currentItem();
if (item != nullptr) {
return item->data(Qt::UserRole).toDate();
}
return firstOfMonth;
}
void
CalendarMonthTable::limitDateRange
(const DateRange &dr, bool allowKeepMonth)
{
if (dr.from.isValid() && dr.to.isValid() && dr.from > dr.to) {
return;
}
this->dr = dr;
if (currentItem() != nullptr && isInDateRange(currentItem()->data(Qt::UserRole).toDate())) {
setMonth(currentItem()->data(Qt::UserRole).toDate());
} else if (isInDateRange(QDate::currentDate())) {
setMonth(QDate::currentDate());
} else if (isInDateRange(firstOfMonth)) {
setMonth(firstOfMonth, allowKeepMonth);
} else if (dr.to.isValid() && dr.to < QDate::currentDate()) {
setMonth(dr.to);
} else if (dr.from.isValid() && dr.from > QDate::currentDate()) {
setMonth(dr.from);
} else if (dr.to.isValid()) {
setMonth(dr.to);
} else {
setMonth(dr.from);
}
}
void
CalendarMonthTable::setFirstDayOfWeek
(Qt::DayOfWeek firstDayOfWeek)
{
QDate selectedDate;
if (currentItem() != nullptr && isInDateRange(currentItem()->data(Qt::UserRole).toDate())) {
selectedDate = currentItem()->data(Qt::UserRole).toDate();
} else {
selectedDate = fitDate(firstOfMonth);
}
clear();
QLocale locale;
QStringList headers;
this->firstDayOfWeek = firstDayOfWeek;
for (int i = Qt::Monday - 1; i < Qt::Sunday; ++i) {
headers << locale.dayName((i + firstDayOfWeek - 1) % 7 + 1, QLocale::ShortFormat);
}
headers << tr("Summary");
setHorizontalHeaderLabels(headers);
verticalHeader()->setVisible(false);
if (selectedDate.isValid()) {
setMonth(selectedDate, true);
}
}
void
CalendarMonthTable::changeEvent
(QEvent *event)
{
if (event->type() == QEvent::PaletteChange) {
horizontalHeader()->setStyleSheet(QString("QHeaderView::section { border: none; background-color: %1; color: %2 }")
.arg(palette().color(QPalette::Active, QPalette::Window).name())
.arg(palette().color(QPalette::Active, QPalette::WindowText).name()));
}
QTableWidget::changeEvent(event);
}
void
CalendarMonthTable::mouseDoubleClickEvent
(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton) {
return;
}
QPoint pos = event->pos();
QModelIndex index = indexAt(pos);
int column = index.column();
if (column < 7) {
QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(index);
CalendarDayDelegate *delegate = static_cast<CalendarDayDelegate*>(abstractDelegate);
CalendarDay day = index.data(Qt::UserRole + 1).value<CalendarDay>();
if (delegate->hitTestMore(index, event->pos())) {
emit moreDblClicked(day);
} else {
int entryIdx = delegate->hitTestEntry(index, event->pos());
if (entryIdx >= 0) {
emit entryDblClicked(day, entryIdx);
} else {
emit dayDblClicked(day);
}
}
} else {
emit summaryDblClicked(index);
}
}
void
CalendarMonthTable::mousePressEvent
(QMouseEvent *event)
{
isDraggable = false;
pressedPos = event->pos();
pressedIndex = indexAt(pressedPos);
QTableWidget::mousePressEvent(event);
if (pressedIndex.column() < 7) {
QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(pressedIndex);
CalendarDayDelegate *delegate = static_cast<CalendarDayDelegate*>(abstractDelegate);
if (! delegate->hitTestMore(pressedIndex, pressedPos)) {
int entryIdx = delegate->hitTestEntry(pressedIndex, pressedPos);
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
item->setData(Qt::UserRole + 2, entryIdx);
}
CalendarDay day = pressedIndex.data(Qt::UserRole + 1).value<CalendarDay>();
if (entryIdx >= 0) {
CalendarEntry calEntry = day.entries[entryIdx];
isDraggable = day.entries[entryIdx].isRelocatable;
if (event->button() == Qt::LeftButton && isDraggable) {
dragTimer.start(QApplication::startDragTime());
}
}
}
}
}
void
CalendarMonthTable::mouseReleaseEvent
(QMouseEvent *event)
{
if (dragTimer.isActive()) {
dragTimer.stop();
}
if (pressedIndex.isValid()) {
QPoint pos = event->pos();
QModelIndex releasedIndex = indexAt(pos);
if (releasedIndex == pressedIndex) {
if (pressedIndex.column() < 7) {
CalendarDay day = pressedIndex.data(Qt::UserRole + 1).value<CalendarDay>();
QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(releasedIndex);
CalendarDayDelegate *delegate = static_cast<CalendarDayDelegate*>(abstractDelegate);
if (delegate->hitTestMore(releasedIndex, event->pos())) {
emit moreClicked(day);
} else {
int entryIdx = delegate->hitTestEntry(releasedIndex, event->pos());
if (event->button() == Qt::LeftButton) {
if (entryIdx >= 0) {
emit entryClicked(day, entryIdx);
} else {
emit dayClicked(day);
}
} else if (event->button() == Qt::RightButton) {
if (entryIdx >= 0) {
emit entryRightClicked(day, entryIdx);
} else {
emit dayRightClicked(day);
}
}
}
} else {
if (event->button() == Qt::LeftButton) {
emit summaryClicked(pressedIndex);
} else if (event->button() == Qt::RightButton) {
emit summaryRightClicked(pressedIndex);
}
}
}
selectDay(pressedIndex.data(Qt::UserRole).toDate());
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
item->setData(Qt::UserRole + 2, QVariant());
}
pressedPos = QPoint();
pressedIndex = QModelIndex();
}
QTableWidget::mousePressEvent(event);
}
void
CalendarMonthTable::mouseMoveEvent
(QMouseEvent *event)
{
if ( ! (event->buttons() & Qt::LeftButton)
|| ! isDraggable
|| dragTimer.isActive()
|| (event->pos() - pressedPos).manhattanLength() < QApplication::startDragDistance()) {
return;
}
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData();
QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(pressedIndex);
CalendarDayDelegate *delegate = static_cast<CalendarDayDelegate*>(abstractDelegate);
int entryIdx = delegate->hitTestEntry(pressedIndex, pressedPos);
CalendarEntry calEntry = pressedIndex.data(Qt::UserRole + 1).value<CalendarDay>().entries[entryIdx];
QPixmap pixmap = svgAsColoredPixmap(calEntry.iconFile, QSize(40 * dpiXFactor, 40 * dpiYFactor), 0, calEntry.color);
drag->setPixmap(pixmap);
QList<int> entryCoords = { pressedIndex.row(), pressedIndex.column(), entryIdx };
QString entryStr = QString::fromStdString(std::accumulate(entryCoords.begin(),
entryCoords.end(),
std::string(),
[](const std::string &a, int b) {
return a + (a.length() ? "," : "") + std::to_string(b);
}));
QByteArray entryBytes = entryStr.toUtf8();
mimeData->setData("application/x-month-grid-entry", entryBytes);
drag->setMimeData(mimeData);
drag->exec(Qt::MoveAction);
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
item->setData(Qt::UserRole + 2, QVariant());
}
pressedPos = QPoint();
pressedIndex = QModelIndex();
}
void
CalendarMonthTable::dragEnterEvent
(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("application/x-month-grid-entry") && event->source() == this) {
event->acceptProposedAction();
}
}
void
CalendarMonthTable::dragMoveEvent
(QDragMoveEvent *event)
{
#if QT_VERSION < 0x060000
QModelIndex hoverIndex = indexAt(event->pos());
#else
QModelIndex hoverIndex = indexAt(event->position().toPoint());
#endif
if ( hoverIndex.isValid()
&& hoverIndex.column() < 7
&& hoverIndex != pressedIndex) {
QTableWidgetItem *item = this->item(hoverIndex.row(), hoverIndex.column());
if (item != nullptr) {
setCurrentItem(item);
}
event->accept();
} else {
setCurrentItem(nullptr);
event->ignore();
}
}
void
CalendarMonthTable::dragLeaveEvent
(QDragLeaveEvent *event)
{
Q_UNUSED(event)
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
if (item != nullptr) {
setCurrentItem(item);
}
}
}
void
CalendarMonthTable::dropEvent
(QDropEvent *event)
{
if (event->mimeData()->hasFormat("application/x-month-grid-entry")) {
QByteArray entryBytes = event->mimeData()->data("application/x-month-grid-entry");
QString entryStr = QString::fromUtf8(entryBytes);
QStringList entryStrList = entryStr.split(',');
QList<int> entryCoords;
for (const QString &str : entryStrList) {
entryCoords.append(str.toInt());
}
if (entryCoords.size() != 3) {
return;
}
if ( entryCoords[0] >= 0
&& entryCoords[0] < rowCount()
&& entryCoords[1] >= 0
&& entryCoords[1] < 7) {
QModelIndex srcIndex = model()->index(entryCoords[0], entryCoords[1]);
int entryIdx = entryCoords[2];
if (srcIndex.isValid() && entryIdx >= 0) {
CalendarDay srcDay = srcIndex.data(Qt::UserRole + 1).value<CalendarDay>();
if (entryIdx < srcDay.entries.count()) {
#if QT_VERSION < 0x060000
QModelIndex destIndex = indexAt(event->pos());
#else
QModelIndex destIndex = indexAt(event->position().toPoint());
#endif
CalendarDay destDay = destIndex.data(Qt::UserRole + 1).value<CalendarDay>();
emit entryMoved(srcDay.entries[entryIdx], srcDay.date, destDay.date);
}
}
}
}
}
#if QT_VERSION < 0x060000
QAbstractItemDelegate*
CalendarMonthTable::itemDelegateForIndex
(const QModelIndex &index) const
{
return itemDelegateForColumn(index.column());
}
#endif
void
CalendarMonthTable::showContextMenu
(const QPoint &pos)
{
QModelIndex index = indexAt(pos);
if ( ! index.isValid()
|| index.column() > 6) {
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
item->setData(Qt::UserRole + 2, QVariant());
}
return;
}
QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(index);
CalendarDayDelegate *delegate = static_cast<CalendarDayDelegate*>(abstractDelegate);
if (delegate->hitTestMore(index, pos)) {
return;
}
int entryIdx = delegate->hitTestEntry(index, pos);
CalendarDay day = index.data(Qt::UserRole + 1).value<CalendarDay>();
QMenu contextMenu(this);
if (entryIdx >= 0) {
CalendarEntry calEntry = day.entries[entryIdx];
switch (calEntry.type) {
case ENTRY_TYPE_ACTIVITY:
contextMenu.addAction("View activity...", this, [=]() {
emit viewActivity(calEntry);
});
contextMenu.addAction("Delete activity", this, [=]() {
emit delActivity(calEntry);
});
break;
case ENTRY_TYPE_PLANNED_ACTIVITY:
if (calEntry.hasTrainMode) {
contextMenu.addAction("Show in Train Mode...", this, [=]() {
emit showInTrainMode(calEntry);
});
}
contextMenu.addAction("View planned activity...", this, [=]() {
emit viewActivity(calEntry);
});
contextMenu.addAction("Delete planned activity", this, [=]() {
emit delActivity(calEntry);
});
break;
case ENTRY_TYPE_OTHER:
default:
break;
}
} else {
if (day.date <= QDate::currentDate()) {
contextMenu.addAction("Add activity...", this, [=]() {
QTime time = QTime::currentTime();
if (day.date == QDate::currentDate()) {
time = time.addSecs(std::max(-4 * 3600, time.secsTo(QTime(0, 0))));
}
emit addActivity(false, day.date, time);
});
}
if (day.date >= QDate::currentDate()) {
contextMenu.addAction("Add planned activity...", this, [=]() {
QTime time = QTime::currentTime();
if (day.date == QDate::currentDate()) {
time = time.addSecs(std::min(4 * 3600, time.secsTo(QTime(23, 59, 59))));
}
emit addActivity(true, day.date, time);
});
bool hasPlannedActivity = false;
for (const CalendarEntry &calEntry : day.entries) {
if (calEntry.type == ENTRY_TYPE_PLANNED_ACTIVITY) {
hasPlannedActivity = true;
break;
}
}
if (hasPlannedActivity) {
contextMenu.addAction("Insert restday", this, [=]() {
emit insertRestday(day.date);
});
} else {
contextMenu.addAction("Delete restday", this, [=]() {
emit delRestday(day.date);
});
}
}
}
contextMenu.exec(viewport()->mapToGlobal(pos));
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
item->setData(Qt::UserRole + 2, QVariant());
}
}
int
CalendarMonthTable::findEntry
(const QModelIndex &index, const QPoint &pos) const
{
if (index.column() >= 7) {
return -1;
}
QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(index);
CalendarDayDelegate *delegate = static_cast<CalendarDayDelegate*>(abstractDelegate);
return delegate->hitTestEntry(index, pos);
}
//////////////////////////////////////////////////////////////////////////////
// Calendar
Calendar::Calendar
(Qt::DayOfWeek firstDayOfWeek, QWidget *parent)
: Calendar(QDate::currentDate(), firstDayOfWeek, parent)
{
}
Calendar::Calendar
(const QDate &dateInMonth, Qt::DayOfWeek firstDayOfWeek, QWidget *parent)
: QWidget(parent)
{
qRegisterMetaType<CalendarDay>("CalendarDay");
qRegisterMetaType<CalendarSummary>("CalendarSummary");
monthTable = new CalendarMonthTable(dateInMonth, firstDayOfWeek);
prevYButton = new QPushButton();
prevYButton->setFlat(true);
prevMButton = new QPushButton();
prevMButton->setFlat(true);
nextMButton = new QPushButton();
nextMButton->setFlat(true);
nextYButton = new QPushButton();
nextYButton->setFlat(true);
todayButton = new QPushButton("Today");
dateLabel = new QLabel();
dateLabel->setAlignment(Qt::AlignRight);
applyNavIcons();
connect(monthTable, &CalendarMonthTable::entryDblClicked, [=](const CalendarDay &day, int entryIdx) {
viewActivity(day.entries[entryIdx]);
});
connect(monthTable, &CalendarMonthTable::showInTrainMode, this, &Calendar::showInTrainMode);
connect(monthTable, &CalendarMonthTable::viewActivity, this, &Calendar::viewActivity);
connect(monthTable, &CalendarMonthTable::addActivity, this, &Calendar::addActivity);
connect(monthTable, &CalendarMonthTable::delActivity, this, &Calendar::delActivity);
connect(monthTable, &CalendarMonthTable::entryMoved, this, &Calendar::moveActivity);
connect(monthTable, &CalendarMonthTable::insertRestday, this, &Calendar::insertRestday);
connect(monthTable, &CalendarMonthTable::delRestday, this, &Calendar::delRestday);
connect(monthTable, &CalendarMonthTable::monthChanged, [=](const QDate &month, const QDate &firstVisible, const QDate &lastVisible) {
setNavButtonState();
QLocale locale;
dateLabel->setText(locale.toString(monthTable->firstOfCurrentMonth(), ("MMMM yyyy")));
emit monthChanged(month, firstVisible, lastVisible);
});
connect(prevYButton, &QPushButton::clicked, [=]() {
monthTable->addYears(-1);
});
connect(prevMButton, &QPushButton::clicked, [=]() {
monthTable->addMonths(-1);
});
connect(nextMButton, &QPushButton::clicked, [=]() {
monthTable->addMonths(1);
});
connect(nextYButton, &QPushButton::clicked, [=]() {
monthTable->addYears(1);
});
connect(todayButton, &QPushButton::clicked, [=]() {
setMonth(QDate::currentDate());
});
QHBoxLayout *navLayout = new QHBoxLayout();
navLayout->setSpacing(0);
navLayout->addWidget(prevYButton);
navLayout->addWidget(prevMButton);
navLayout->addWidget(nextMButton);
navLayout->addWidget(nextYButton);
navLayout->addWidget(todayButton);
navLayout->addSpacing(24 * dpiXFactor);
navLayout->addWidget(dateLabel);
navLayout->addStretch();
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(navLayout);
mainLayout->addWidget(monthTable);
setMonth(dateInMonth);
}
void
Calendar::setMonth
(const QDate &dateInMonth, bool allowKeepMonth)
{
if (monthTable->isInDateRange(dateInMonth)) {
monthTable->setMonth(dateInMonth, allowKeepMonth);
}
}
void
Calendar::fillEntries
(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries)
{
QHash<QDate, QList<CalendarEntry>> activities = activityEntries;
QDate firstVisible = monthTable->firstVisibleDay();
QDate lastVisible = monthTable->lastVisibleDay();
for (auto dayIt = activities.begin(); dayIt != activities.end(); ++dayIt) {
if (dayIt.key() >= firstVisible && dayIt.key() <= lastVisible) {
std::sort(dayIt.value().begin(), dayIt.value().end(), [](const CalendarEntry &a, const CalendarEntry &b) {
if (a.start == b.start) {
return a.primary < b.primary;
} else {
return a.start < b.start;
}
});
}
}
this->headlineEntries = headlineEntries;
monthTable->fillEntries(activities, summaries, headlineEntries);
}
QDate
Calendar::firstOfCurrentMonth
() const
{
return monthTable->firstOfCurrentMonth();
}
QDate
Calendar::firstVisibleDay
() const
{
return monthTable->firstVisibleDay();
}
QDate
Calendar::lastVisibleDay
() const
{
return monthTable->lastVisibleDay();
}
QDate
Calendar::selectedDate
() const
{
return monthTable->selectedDate();
}
void
Calendar::activateDateRange
(const DateRange &dr, bool allowKeepMonth)
{
headlineEntries.clear();
for (auto dayIt = headlineEntries.begin(); dayIt != headlineEntries.end(); ++dayIt) {
std::sort(dayIt.value().begin(), dayIt.value().end(), [](const CalendarEntry &a, const CalendarEntry &b) {
if (a.start == b.start) {
return a.primary < b.primary;
} else {
return a.start < b.start;
}
});
}
monthTable->limitDateRange(dr, allowKeepMonth);
setNavButtonState();
emit dateRangeActivated(dr.name);
}
void
Calendar::setFirstDayOfWeek
(Qt::DayOfWeek firstDayOfWeek)
{
monthTable->setFirstDayOfWeek(firstDayOfWeek);
}
void
Calendar::setSummaryMonthVisible
(bool visible)
{
monthTable->setColumnHidden(7, ! visible);
}
void
Calendar::setNavButtonState
()
{
QDate lom(monthTable->firstOfCurrentMonth().year(), monthTable->firstOfCurrentMonth().month(), monthTable->firstOfCurrentMonth().daysInMonth());
prevYButton->setEnabled(monthTable->isInDateRange(lom.addYears(-1)));
prevMButton->setEnabled(monthTable->isInDateRange(lom.addMonths(-1)));
nextMButton->setEnabled(monthTable->isInDateRange(monthTable->firstOfCurrentMonth().addMonths(1)));
nextYButton->setEnabled(monthTable->isInDateRange(monthTable->firstOfCurrentMonth().addYears(1)));
todayButton->setEnabled(monthTable->isInDateRange(QDate::currentDate()));
}
void
Calendar::applyNavIcons
()
{
double scale = appsettings->value(this, GC_FONT_SCALE, 1.0).toDouble();
QFont font;
font.setPointSize(font.pointSizeF() * scale * 1.3);
font.setWeight(QFont::Bold);
dateLabel->setFont(font);
int size = font.pointSize() * 1.5;
QSize iconSize(size * dpiXFactor, size * dpiYFactor);
QString buttonStyle = QString("padding: %1px;").arg(size / 6 * dpiXFactor);
prevYButton->setStyleSheet(buttonStyle);
prevYButton->setIcon(QIcon(QString(":images/breeze/%1/go-previous-skip.svg").arg(isDark() ? "dark" : "light")));
prevYButton->setIconSize(iconSize);
prevMButton->setStyleSheet(buttonStyle);
prevMButton->setIcon(QIcon(QString(":images/breeze/%1/go-previous.svg").arg(isDark() ? "dark" : "light")));
prevMButton->setIconSize(iconSize);
nextMButton->setStyleSheet(buttonStyle);
nextMButton->setIcon(QIcon(QString(":images/breeze/%1/go-next.svg").arg(isDark() ? "dark" : "light")));
nextMButton->setIconSize(iconSize);
nextYButton->setStyleSheet(buttonStyle);
nextYButton->setIcon(QIcon(QString(":images/breeze/%1/go-next-skip.svg").arg(isDark() ? "dark" : "light")));
nextYButton->setIconSize(iconSize);
}
bool
Calendar::isDark
() const
{
return palette().color(QPalette::Active, QPalette::Window).lightness() < 127;
}

142
src/Gui/Calendar.h Normal file
View File

@@ -0,0 +1,142 @@
#ifndef CALENDAR_H
#define CALENDAR_H
#include <QWidget>
#include <QTableWidget>
#include <QPushButton>
#include <QLabel>
#include <QComboBox>
#include <QDate>
#include <QStringList>
#include <QTimer>
#include <QApplication>
#include "CalendarData.h"
#include "TimeUtils.h"
class CalendarMonthTable : public QTableWidget {
Q_OBJECT
public:
explicit CalendarMonthTable(Qt::DayOfWeek firstDayOfWeek = Qt::Monday, QWidget *parent = nullptr);
explicit CalendarMonthTable(const QDate &dateInMonth, Qt::DayOfWeek firstDayOfWeek = Qt::Monday, QWidget *parent = nullptr);
bool selectDay(const QDate &day);
bool setMonth(const QDate &dateInMonth, bool allowKeepMonth = false);
bool addMonths(int months);
bool addYears(int years);
QDate fitDate(const QDate &date) const;
bool canAddMonths(int months) const;
bool canAddYears(int years) const;
bool isInDateRange(const QDate &date) const;
void fillEntries(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries);
QDate firstOfCurrentMonth() const;
QDate firstVisibleDay() const;
QDate lastVisibleDay() const;
QDate selectedDate() const;
void limitDateRange(const DateRange &dr, bool allowKeepMonth = false);
void setFirstDayOfWeek(Qt::DayOfWeek firstDayOfWeek);
signals:
void dayClicked(const CalendarDay &day);
void dayDblClicked(const CalendarDay &day);
void moreClicked(const CalendarDay &day);
void moreDblClicked(const CalendarDay &day);
void dayRightClicked(const CalendarDay &day);
void entryClicked(const CalendarDay &day, int entryIdx);
void entryDblClicked(const CalendarDay &day, int entryIdx);
void entryRightClicked(const CalendarDay &day, int entryIdx);
void entryMoved(const CalendarEntry &activity, const QDate &srcDay, const QDate &destDay);
void summaryClicked(const QModelIndex &index);
void summaryDblClicked(const QModelIndex &index);
void summaryRightClicked(const QModelIndex &index);
void monthChanged(const QDate &month, const QDate &firstVisible, const QDate &lastVisible);
void showInTrainMode(const CalendarEntry &activity);
void viewActivity(const CalendarEntry &activity);
void addActivity(bool plan, const QDate &day, const QTime &time);
void delActivity(const CalendarEntry &activity);
void insertRestday(const QDate &day);
void delRestday(const QDate &day);
protected:
void changeEvent(QEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override;
void dropEvent(QDropEvent *event) override;
#if QT_VERSION < 0x060000
QAbstractItemDelegate *itemDelegateForIndex(const QModelIndex &index) const;
#endif
private slots:
void showContextMenu(const QPoint &pos);
private:
int findEntry(const QModelIndex &index, const QPoint &pos) const;
Qt::DayOfWeek firstDayOfWeek = Qt::Monday;
QDate firstOfMonth;
QDate startDate; // first visible date
QDate endDate; // last visible date
DateRange dr;
QTimer dragTimer;
QPoint pressedPos;
QModelIndex pressedIndex;
bool isDraggable = false;
};
class Calendar : public QWidget {
Q_OBJECT
public:
explicit Calendar(Qt::DayOfWeek firstDayOfWeek = Qt::Monday, QWidget *parent = nullptr);
explicit Calendar(const QDate &dateInMonth, Qt::DayOfWeek firstDayOfWeek = Qt::Monday, QWidget *parent = nullptr);
void setMonth(const QDate &dateInMonth, bool allowKeepMonth = false);
void fillEntries(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries);
QDate firstOfCurrentMonth() const;
QDate firstVisibleDay() const;
QDate lastVisibleDay() const;
QDate selectedDate() const;
public slots:
void activateDateRange(const DateRange &dr, bool allowKeepMonth = false);
void setFirstDayOfWeek(Qt::DayOfWeek firstDayOfWeek);
void setSummaryMonthVisible(bool visible);
void applyNavIcons();
signals:
void dayClicked(const QDate &date);
void summaryClicked(const QDate &date);
void monthChanged(const QDate &month, const QDate &firstVisible, const QDate &lastVisible);
void dateRangeActivated(const QString &name);
void showInTrainMode(const CalendarEntry &activity);
void viewActivity(const CalendarEntry &activity);
void addActivity(bool plan, const QDate &day, const QTime &time);
void delActivity(const CalendarEntry &activity);
void moveActivity(const CalendarEntry &activity, const QDate &srcDay, const QDate &destDay);
void insertRestday(const QDate &day);
void delRestday(const QDate &day);
private:
QPushButton *prevYButton;
QPushButton *prevMButton;
QPushButton *nextMButton;
QPushButton *nextYButton;
QPushButton *todayButton;
QLabel *dateLabel;
CalendarMonthTable *monthTable;
QHash<QDate, QList<CalendarEntry>> headlineEntries;
void setNavButtonState();
bool isDark() const;
};
#endif

58
src/Gui/CalendarData.h Normal file
View File

@@ -0,0 +1,58 @@
#ifndef CALENDARDATA_H
#define CALENDARDATA_H
#include <QDate>
#include <QTime>
#include <QList>
#include <QHash>
#define ENTRY_TYPE_ACTIVITY 0
#define ENTRY_TYPE_PLANNED_ACTIVITY 1
#define ENTRY_TYPE_EVENT 10
#define ENTRY_TYPE_PHASE 11
#define ENTRY_TYPE_OTHER 99
struct CalendarEvent {
QString name;
QDate date;
};
struct CalendarPhase {
QString name;
QDate start;
QDate end;
};
struct CalendarEntry {
QString primary;
QString secondary;
QString secondaryMetric;
QString iconFile;
QColor color;
QString reference;
QTime start;
int durationSecs = 0;
int type = 0;
bool isRelocatable = false;
bool hasTrainMode = false;
QDate spanStart = QDate();
QDate spanEnd = QDate();
};
struct CalendarDay {
QDate date;
bool isDimmed;
QList<CalendarEntry> entries = QList<CalendarEntry>();
QList<CalendarEntry> headlineEntries = QList<CalendarEntry>();
};
struct CalendarSummary {
QList<std::pair<QString, QString>> keyValues;
};
Q_DECLARE_METATYPE(CalendarEntry)
Q_DECLARE_METATYPE(CalendarDay)
Q_DECLARE_METATYPE(CalendarSummary)
#endif

View File

@@ -0,0 +1,440 @@
#include "CalendarItemDelegates.h"
#include <QDate>
#include <QHelpEvent>
#include <QAbstractItemView>
#include <QPainter>
#include <QToolTip>
#include <QPixmap>
#include <QSvgRenderer>
#include <QPainterPath>
#include "CalendarData.h"
#include "Colors.h"
//////////////////////////////////////////////////////////////////////////////
// CalendarDayDelegate
CalendarDayDelegate::CalendarDayDelegate
(QObject *parent)
: QStyledItemDelegate(parent)
{
}
void
CalendarDayDelegate::paint
(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
painter->save();
painter->setRenderHints(painter->renderHints() | QPainter::Antialiasing | QPainter::TextAntialiasing);
entryRects[index].clear();
headlineEntryRects[index].clear();
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
QDate date = index.data(Qt::UserRole).toDate();
CalendarDay calendarDay = index.data(Qt::UserRole + 1).value<CalendarDay>();
bool isToday = (date == QDate::currentDate());
QColor bgColor;
QColor selColor = opt.palette.highlight().color();
QColor dayColor;
QColor entryColor;
bool ok;
int pressedEntryIdx = index.data(Qt::UserRole + 2).toInt(&ok);
if (! ok) {
pressedEntryIdx = -2;
}
if (pressedEntryIdx < 0 && opt.state & QStyle::State_Selected) {
bgColor = opt.palette.color(calendarDay.isDimmed ? QPalette::Disabled : QPalette::Active, QPalette::Highlight);
} else if (calendarDay.isDimmed) {
bgColor = opt.palette.color(QPalette::Disabled, QPalette::Base);
} else {
bgColor = opt.palette.base().color();
}
entryColor = GCColor::invertColor(bgColor);
painter->fillRect(opt.rect, bgColor);
int leftMargin = 4;
int rightMargin = 4;
int topMargin = 2;
int bottomMargin = 4;
int lineSpacing = 2;
int iconSpacing = 2;
int radius = 4;
// Day number / Headline
painter->save();
QFont dayFont = painter->font();
QFontMetrics dayFM(dayFont);
dayFont.setWeight(QFont::Bold);
int lineHeight = dayFM.height();
QRect dayRect(opt.rect.x() + leftMargin,
opt.rect.y() + topMargin,
std::max(dayFM.horizontalAdvance(QString::number(date.day())) + leftMargin, lineHeight) + leftMargin,
lineHeight);
int alignFlags = Qt::AlignLeft | Qt::AlignTop;
if (isToday) {
dayRect.setX(opt.rect.x() + 1);
dayRect.setWidth(dayRect.width() - 1);
painter->save();
painter->setPen(opt.palette.color(calendarDay.isDimmed ? QPalette::Disabled : QPalette::Active, QPalette::Base)),
painter->setBrush(opt.palette.color(calendarDay.isDimmed ? QPalette::Disabled : QPalette::Active, QPalette::Highlight)),
painter->drawRoundedRect(dayRect, 2 * radius, 2 * radius);
painter->restore();
dayColor = opt.palette.color(calendarDay.isDimmed ? QPalette::Disabled : QPalette::Active, QPalette::HighlightedText);
alignFlags = Qt::AlignHCenter | Qt::AlignTop;
} else if (pressedEntryIdx < 0 && opt.state & QStyle::State_Selected) {
dayColor = opt.palette.color(calendarDay.isDimmed ? QPalette::Disabled : QPalette::Active, QPalette::HighlightedText);
} else {
dayColor = opt.palette.color(calendarDay.isDimmed ? QPalette::Disabled : QPalette::Active, QPalette::Text);
}
painter->setFont(dayFont);
painter->setPen(dayColor);
painter->drawText(dayRect, alignFlags, QString::number(date.day()));
painter->restore();
for (int i = 0; i < calendarDay.headlineEntries.size(); ++i) {
CalendarEntry calEntry = calendarDay.headlineEntries[i];
int x = opt.rect.x() + opt.rect.width() - rightMargin - i * (lineHeight + lineSpacing) - lineHeight;
if (x < dayRect.x() + dayRect.width() + lineSpacing) {
break;
}
QPixmap pixmap = svgOnBackground(calEntry.iconFile, QSize(lineHeight, lineHeight), 0, calEntry.color, radius);
QRect headlineEntryRect(x, opt.rect.y() + topMargin, lineHeight, lineHeight);
painter->drawPixmap(x, opt.rect.y() + topMargin, pixmap);
headlineEntryRects[index].append(headlineEntryRect);
}
// Entries
QFont entryFont = painter->font();
dayFont.setWeight(QFont::Normal);
entryFont.setPointSize(entryFont.pointSize() * 0.95);
QFontMetrics entryFM(entryFont);
lineHeight = entryFM.height();
QSize pixmapSize(2 * lineHeight + lineSpacing, 2 * lineHeight + lineSpacing);
int entryHeight = pixmapSize.height();
int entrySpace = opt.rect.height() - dayRect.height() - topMargin - bottomMargin;
int maxLines = entrySpace / (entryHeight + lineSpacing);
bool multiLine = (static_cast<int>(calendarDay.entries.size()) <= maxLines);
if (! multiLine) {
pixmapSize = QSize(lineHeight, lineHeight);
entryHeight = pixmapSize.height();
maxLines = entrySpace / (entryHeight + lineSpacing);
}
painter->setPen(entryColor);
painter->setFont(entryFont);
int entryStartY = dayRect.y() + dayRect.height() + lineSpacing;
for (int i = 0; i < std::min(maxLines, static_cast<int>(calendarDay.entries.size())); ++i) {
CalendarEntry calEntry = calendarDay.entries[i];
QRect entryRect(opt.rect.x() + leftMargin,
entryStartY + i * (entryHeight + lineSpacing),
opt.rect.width() - leftMargin - rightMargin - 1,
entryHeight - 1);
QRect titleRect(opt.rect.x() + leftMargin + pixmapSize.width() + iconSpacing,
entryStartY + i * (entryHeight + lineSpacing),
opt.rect.width() - leftMargin - rightMargin - pixmapSize.width() - 2,
lineHeight);
painter->save();
if (i == pressedEntryIdx) {
painter->setBrush(selColor);
painter->setPen(selColor);
painter->drawRoundedRect(entryRect, radius, radius);
painter->setPen(GCColor::invertColor(selColor));
}
QPixmap pixmap;
if (calEntry.type == ENTRY_TYPE_PLANNED_ACTIVITY) {
QColor pixmapColor(calEntry.color);
if ( i == pressedEntryIdx
|| ( opt.state & QStyle::State_Selected
&& pressedEntryIdx < 0)) {
pixmapColor = GCColor::invertColor(selColor);
}
pixmap = svgAsColoredPixmap(calEntry.iconFile, pixmapSize, lineSpacing, pixmapColor);
} else {
pixmap = svgOnBackground(calEntry.iconFile, pixmapSize, lineSpacing, calEntry.color, radius);
}
painter->drawPixmap(opt.rect.x() + leftMargin,
entryStartY + i * (entryHeight + lineSpacing),
pixmap);
painter->drawText(titleRect, Qt::AlignLeft | Qt::AlignTop, calEntry.primary);
if (multiLine && ! calEntry.secondary.isEmpty()) {
QRect subRect(titleRect.x(),
titleRect.y() + lineSpacing + lineHeight,
titleRect.width(),
titleRect.height());
painter->drawText(subRect, Qt::AlignLeft | Qt::AlignTop, calEntry.secondary + " (" + calEntry.secondaryMetric + ")");
}
painter->restore();
entryRects[index].append(entryRect);
}
int overflow = static_cast<int>(calendarDay.entries.size()) - maxLines;
if (overflow > 0) {
QString overflowStr = tr("%1 more...").arg(overflow);
QFont overflowFont(entryFont);
overflowFont.setWeight(QFont::DemiBold);
QFontMetrics overflowFM(overflowFont);
int overflowWidth = overflowFM.horizontalAdvance(overflowStr) + 2 * rightMargin;
QRect overflowRect = QRect(opt.rect.x() + opt.rect.width() - overflowWidth,
opt.rect.y() + opt.rect.height() - lineHeight,
overflowWidth,
lineHeight);
QColor overflowBg(bgColor);
overflowBg.setAlpha(185);
painter->save();
painter->setPen(overflowBg);
painter->setBrush(overflowBg);
painter->drawRoundedRect(overflowRect, 2 * radius, 2 * radius);
painter->restore();
painter->save();
painter->setFont(overflowFont);
painter->drawText(overflowRect, Qt::AlignCenter, overflowStr);
painter->restore();
moreRects[index] = overflowRect;
} else {
moreRects[index] = QRect();
}
painter->restore();
}
bool
CalendarDayDelegate::helpEvent
(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index)
{
if (! event || ! view) {
return false;
}
CalendarDay day = index.data(Qt::UserRole + 1).value<CalendarDay>();
int entryIdx;
bool hoverMore = hitTestMore(index, event->pos());
if (! hoverMore && (entryIdx = hitTestEntry(index, event->pos())) >= 0 && entryIdx < day.entries.size()) {
CalendarEntry calEntry = day.entries[entryIdx];
QString tooltip = "<center><b>" + calEntry.primary + "</b>";
tooltip += "<br/>";
switch (calEntry.type) {
case ENTRY_TYPE_ACTIVITY:
tooltip += "[completed]";
break;
case ENTRY_TYPE_PLANNED_ACTIVITY:
tooltip += "[planned]";
break;
case ENTRY_TYPE_OTHER:
default:
tooltip += "[other]";
break;
}
tooltip += "<br/>";
if (! calEntry.secondary.isEmpty()) {
tooltip += calEntry.secondaryMetric + ": " + calEntry.secondary;
tooltip += "<br/>";
}
if (calEntry.start.isValid()) {
tooltip += calEntry.start.toString();
if (calEntry.durationSecs > 0) {
tooltip += " - " + calEntry.start.addSecs(calEntry.durationSecs).toString();
}
}
tooltip += "</center>";
QToolTip::showText(event->globalPos(), tooltip, view);
return true;
} else if (! hoverMore && (entryIdx = hitTestHeadlineEntry(index, event->pos())) >= 0 && entryIdx < day.headlineEntries.size()) {
CalendarEntry headlineEntry = day.headlineEntries[entryIdx];
QString tooltip = "<center><b>" + headlineEntry.primary + "</b></center>";
tooltip += "<center>";
switch (headlineEntry.type) {
case ENTRY_TYPE_PHASE:
tooltip += "[phase]";
break;
case ENTRY_TYPE_EVENT:
default:
tooltip += "[event]";
break;
}
tooltip += "</center>";
if (headlineEntry.spanStart.isValid() && headlineEntry.spanEnd.isValid() && headlineEntry.spanStart < headlineEntry.spanEnd) {
QLocale locale;
tooltip += QString("<center>%1 - %2</center>")
.arg(locale.toString(headlineEntry.spanStart, QLocale::ShortFormat))
.arg(locale.toString(headlineEntry.spanEnd, QLocale::ShortFormat));
}
QToolTip::showText(event->globalPos(), tooltip, view);
return true;
} else {
QStringList entries;
for (CalendarEntry entry : day.entries) {
entries << entry.primary;
}
if (! entries.isEmpty()) {
QString tooltip = entries.join("\n");
QToolTip::showText(event->globalPos(), tooltip, view);
return true;
}
}
return QStyledItemDelegate::helpEvent(event, view, option, index);
}
int
CalendarDayDelegate::hitTestEntry
(const QModelIndex &index, const QPoint &pos) const
{
if (! entryRects.contains(index)) {
return -1;
}
const QList<QRect> &rects = entryRects[index];
for (int i = 0; i < rects.size(); ++i) {
if (rects[i].contains(pos)) {
return i;
}
}
return -1;
}
int
CalendarDayDelegate::hitTestHeadlineEntry
(const QModelIndex &index, const QPoint &pos) const
{
if (! headlineEntryRects.contains(index)) {
return -1;
}
const QList<QRect> &rects = headlineEntryRects[index];
for (int i = 0; i < rects.size(); ++i) {
if (rects[i].contains(pos)) {
return i;
}
}
return -1;
}
bool
CalendarDayDelegate::hitTestMore
(const QModelIndex &index, const QPoint &pos) const
{
return moreRects.contains(index) && moreRects[index].contains(pos);
}
//////////////////////////////////////////////////////////////////////////////
// CalendarSummaryDelegate
CalendarSummaryDelegate::CalendarSummaryDelegate
(QObject *parent)
: QStyledItemDelegate(parent)
{
}
void
CalendarSummaryDelegate::paint
(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
bool hasToolTip = false;
const QColor bgColor = GCColor::inactiveColor(option.palette.color(QPalette::Active, QPalette::Base));
const QColor fgColor = option.palette.color(QPalette::Active, QPalette::Text);
const CalendarSummary summary = index.data(Qt::UserRole).value<CalendarSummary>();
QFont valueFont(painter->font());
valueFont.setWeight(QFont::Normal);
valueFont.setPointSize(valueFont.pointSize() * 0.95);
QFont keyFont(valueFont);
keyFont.setWeight(QFont::DemiBold);
const QFontMetrics valueFontMetrics(valueFont);
const QFontMetrics keyFontMetrics(keyFont);
const int lineSpacing = 2;
const int horMargin = 4;
const int vertMargin = 4;
const int lineHeight = keyFontMetrics.height();
const int cellWidth = option.rect.width();
const int cellHeight = option.rect.height();
const int leftX = option.rect.x();
const int rightX = option.rect.x() + cellWidth;
const int availableWidth = cellWidth - 2 * horMargin;
int lineY = option.rect.y() + vertMargin;
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
painter->fillRect(option.rect, bgColor);
painter->setPen(fgColor);
for (const std::pair<QString, QString> &p : summary.keyValues) {
if (lineY + lineHeight > option.rect.y() + cellHeight - vertMargin) {
hasToolTip = true;
break;
}
QString keyText = p.first;
QString valueText = p.second;
QRect keyRect = keyFontMetrics.boundingRect(keyText);
QRect valueRect = valueFontMetrics.boundingRect(valueText);
int availableForLeftLabel = availableWidth - valueRect.width() - horMargin;
if (keyRect.width() > availableForLeftLabel) {
valueText = valueText.split(' ')[0];
valueRect = valueFontMetrics.boundingRect(valueText);
availableForLeftLabel = availableWidth - valueRect.width() - horMargin;
if (keyRect.width() > availableForLeftLabel) {
keyText = keyFontMetrics.elidedText(keyText, Qt::ElideMiddle, availableForLeftLabel);
keyRect = valueFontMetrics.boundingRect(keyText);
}
hasToolTip = true;
}
keyRect.moveTo(leftX + horMargin, lineY);
valueRect.moveTo(rightX - horMargin - valueRect.width(), lineY);
painter->setFont(keyFont);
painter->drawText(keyRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip, keyText);
painter->setFont(valueFont);
painter->drawText(valueRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip, valueText);
int lineStartX = leftX + 2 * horMargin + keyRect.width();
int lineEndX = rightX - 2 * horMargin - valueRect.width();
if (lineEndX > lineStartX) {
QPen dottedPen(fgColor, 1 * dpiXFactor, Qt::DotLine);
painter->save();
painter->setPen(dottedPen);
int dotsY = lineY + keyFontMetrics.ascent() - 1;
painter->drawLine(lineStartX, dotsY, lineEndX, dotsY);
painter->restore();
}
lineY += lineHeight + lineSpacing;
}
painter->restore();
indexHasToolTip[index] = hasToolTip;
}
bool
CalendarSummaryDelegate::helpEvent
(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index)
{
Q_UNUSED(option)
if (! event || ! view || ! indexHasToolTip.value(index, true)) {
return false;
}
CalendarSummary summary = index.data(Qt::UserRole).value<CalendarSummary>();
QString tooltip = "<table>";
for (const std::pair<QString, QString> &p : summary.keyValues) {
tooltip += QString("<tr><td><b>%1</b></td><td>&nbsp;</td><td align='right'>%2</td></tr>").arg(p.first).arg(p.second);
}
tooltip += "</table>";
QToolTip::showText(event->globalPos(), tooltip, view);
return true;
}

View File

@@ -0,0 +1,36 @@
#ifndef CALENDARITEMDELEGATES_H
#define CALENDARITEMDELEGATES_H
#include <QStyledItemDelegate>
class CalendarDayDelegate : public QStyledItemDelegate {
public:
explicit CalendarDayDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override;
int hitTestEntry(const QModelIndex &index, const QPoint &pos) const;
int hitTestHeadlineEntry(const QModelIndex &index, const QPoint &pos) const;
bool hitTestMore(const QModelIndex &index, const QPoint &pos) const;
private:
mutable QHash<QModelIndex, QList<QRect>> entryRects;
mutable QHash<QModelIndex, QList<QRect>> headlineEntryRects;
mutable QHash<QModelIndex, QRect> moreRects;
};
class CalendarSummaryDelegate : public QStyledItemDelegate {
public:
explicit CalendarSummaryDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override;
private:
mutable QHash<QModelIndex, bool> indexHasToolTip;
};
#endif

View File

@@ -391,6 +391,16 @@ QColor GCColor::alternateColor(QColor bgColor)
return QColor(Qt::lightGray);
}
QColor GCColor::inactiveColor(QColor baseColor, double factor)
{
QColor gray = baseColor.lightness() < 128 ? QColor(128, 128, 128) : QColor(200, 200, 200);
return QColor::fromRgbF(
baseColor.redF() * (1 - factor) + gray.redF() * factor,
baseColor.greenF() * (1 - factor) + gray.greenF() * factor,
baseColor.blueF()* (1 - factor) + gray.blueF()* factor
);
}
QColor GCColor::selectedColor(QColor bgColor)
{
// if foreground is white then we're "dark" if it's
@@ -1080,6 +1090,48 @@ GCColor::applyTheme(int index)
}
//
// PaletteApplier - Helper to force a palette update on all children - use if palette propagation doesn't work
//
void
PaletteApplier::setPaletteRecursively
(QWidget *widget, const QPalette &palette, bool forceOnCustom)
{
widget->setPalette(palette);
for (QWidget *child : widget->findChildren<QWidget*>()) {
bool hasCustomPalette = child->testAttribute(Qt::WA_SetPalette);
if (! hasCustomPalette || forceOnCustom) {
child->setPalette(palette);
}
}
}
void
PaletteApplier::setPaletteOnList
(const QList<QWidget*> &widgets, const QPalette &palette)
{
for (QWidget *widget : widgets) {
widget->setPalette(palette);
}
}
void
PaletteApplier::setPaletteByType
(QWidget *root, const QPalette &palette, const QString &typeName)
{
QList<QWidget*> widgets = root->findChildren<QWidget*>();
for (QWidget *widget : widgets) {
if (widget->metaObject()->className() == typeName) {
widget->setPalette(palette);
}
}
}
//
// ColorLabel - just paints a swatch of the first 5 colors
//
@@ -1148,6 +1200,30 @@ svgAsColoredPixmap
}
QPixmap
svgOnBackground
(const QString& file, const QSize &size, int margin, const QColor &bg, int radius)
{
QColor fg(GCColor::invertColor(bg));
QPixmap svgPixmap = svgAsColoredPixmap(file, size, margin, fg);
QPixmap pixmap(svgPixmap.size());
pixmap.fill(Qt::transparent);
QPainterPath path;
path.addRoundedRect(QRectF(0, 0, size.width(), size.height()), radius, radius);
QPainter painter(&pixmap);
painter.setClipPath(path);
painter.fillRect(pixmap.rect(), bg);
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.drawPixmap(0, 0, svgPixmap);
painter.end();
return pixmap;
}
extern void
basicTreeWidgetStyle
(QTreeWidget *tree, bool editable)

View File

@@ -34,6 +34,7 @@
extern QIcon colouredIconFromPNG(QString filename, QColor color);
extern QPixmap colouredPixmapFromPNG(QString filename, QColor color);
extern QPixmap svgAsColoredPixmap(const QString &file, const QSize &size, int margin, const QColor &color);
extern QPixmap svgOnBackground(const QString& file, const QSize &size, int margin, const QColor &bg, int radius = 10);
// dialog scaling
extern double dpiXFactor, dpiYFactor;
@@ -137,6 +138,7 @@ class GCColor : public QObject
static double luminance(QColor color); // return the relative luminance
static QColor invertColor(QColor); // return the contrasting color
static QColor alternateColor(QColor); // return the alternate background
static QColor inactiveColor(QColor baseColor, double factor = 0.2); // return a dimmed variant
static QColor selectedColor(QColor); // return the on select background color
static QColor htmlCode(QColor x) { return x.name(); } // return the alternate background
static Themes &themes();
@@ -188,6 +190,13 @@ class ColorEngine : public QObject
GlobalContext *gc; // bootstrapping
};
class PaletteApplier {
public:
static void setPaletteRecursively(QWidget *widget, const QPalette &palette, bool forceOnCustom = false);
static void setPaletteOnList(const QList<QWidget*> &widgets, const QPalette &palette);
static void setPaletteByType(QWidget *root, const QPalette &palette, const QString &typeName);
};
// shorthand
#define GColor(x) GCColor::getColor(x)

View File

@@ -46,6 +46,7 @@
#include "WorkoutWindow.h"
#include "WebPageWindow.h"
#include "LiveMapWebPageWindow.h"
#include "PlanningCalendarWindow.h"
#ifdef GC_WANT_R
#include "RChart.h"
#endif
@@ -66,7 +67,7 @@ GcWindowRegistry* GcWindows;
void
GcWindowRegistry::initialize()
{
static GcWindowRegistry GcWindowsInit[35] = {
static GcWindowRegistry GcWindowsInit[36] = {
// name GcWinID
{ VIEW_TRENDS|VIEW_DIARY, tr("Season Overview"),GcWindowTypes::OverviewTrends },
{ VIEW_TRENDS|VIEW_DIARY, tr("Blank Overview "),GcWindowTypes::OverviewTrendsBlank },
@@ -110,6 +111,7 @@ GcWindowRegistry::initialize()
{ VIEW_TRAIN, tr("Live Map"),GcWindowTypes::LiveMapWebPageWindow },
{ VIEW_TRAIN, tr("Elevation Chart"),GcWindowTypes::ElevationChart },
{ VIEW_ANALYSIS|VIEW_TRENDS|VIEW_TRAIN, tr("Web page"),GcWindowTypes::WebPageWindow },
{ VIEW_TRENDS, tr("Planning Calendar"),GcWindowTypes::Calendar },
{ 0, "", GcWindowTypes::None }};
// initialize the global registry
GcWindows = GcWindowsInit;
@@ -254,6 +256,7 @@ GcWindowRegistry::newGcWindow(GcWinID id, Context *context)
case GcWindowTypes::SeasonPlan: returning = new PlanningWindow(context); break;
case GcWindowTypes::UserAnalysis: returning = new UserChartWindow(context, false); break;
case GcWindowTypes::UserTrends: returning = new UserChartWindow(context, true); break;
case GcWindowTypes::Calendar: returning = new PlanningCalendarWindow(context); break;
default: return NULL; break;
}
if (returning) returning->setProperty("type", QVariant::fromValue<GcWinID>(id));

View File

@@ -77,7 +77,8 @@ enum gcwinid {
LiveMapWebPageWindow = 48,
OverviewAnalysisBlank=49,
OverviewTrendsBlank=50,
ElevationChart=51
ElevationChart=51,
Calendar=52
};
};
typedef enum GcWindowTypes::gcwinid GcWinID;

View File

@@ -405,10 +405,16 @@ LTMSidebar::dateRangeTreeWidgetSelectionChanged()
}
// Let the view know its changed....
if (phase) emit dateRangeChanged(DateRange(phase->getStart(), phase->getEnd(), dateRange->getName() + "/" + phase->getName()));
else if (dateRange) emit dateRangeChanged(DateRange(dateRange->getStart(), dateRange->getEnd(), dateRange->getName()));
else emit dateRangeChanged(DateRange());
if (phase) {
emit dateRangeChanged(DateRange(phase->getStart(), phase->getEnd(), dateRange->getName() + "/" + phase->getName()));
context->notifySeasonChanged(phase);
} else if (dateRange) {
emit dateRangeChanged(DateRange(dateRange->getStart(), dateRange->getEnd(), dateRange->getName()));
context->notifySeasonChanged(dateRange);
} else {
emit dateRangeChanged(DateRange());
context->notifySeasonChanged(nullptr);
}
}
/*----------------------------------------------------------------------

View File

@@ -63,8 +63,9 @@ static QString activityFilename(const QDateTime &dt, bool plan, Context *context
////////////////////////////////////////////////////////////////////////////////
// ManualActivityWizard
ManualActivityWizard::ManualActivityWizard
(Context *context, bool plan, QWidget *parent)
(Context *context, bool plan, const QDateTime &when, QWidget *parent)
: QWizard(parent), context(context), plan(plan)
{
if (plan) {
@@ -80,9 +81,9 @@ ManualActivityWizard::ManualActivityWizard
#else
setWizardStyle(QWizard::ModernStyle);
#endif
setPixmap(ICON_TYPE, svgAsColoredPixmap(":images/material/summit.svg", QSize(ICON_SIZE * dpiXFactor, ICON_SIZE * dpiYFactor), ICON_MARGIN * dpiXFactor, ICON_COLOR));
setPixmap(ICON_TYPE, svgAsColoredPixmap(":images/breeze/games-highscores.svg", QSize(ICON_SIZE * dpiXFactor, ICON_SIZE * dpiYFactor), ICON_MARGIN * dpiXFactor, ICON_COLOR));
setPage(PageBasics, new ManualActivityPageBasics(context, plan));
setPage(PageBasics, new ManualActivityPageBasics(context, plan, when));
setPage(PageWorkout, new ManualActivityPageWorkout(context));
setPage(PageMetrics, new ManualActivityPageMetrics(context, plan));
setPage(PageSummary, new ManualActivityPageSummary(plan));
@@ -220,8 +221,8 @@ ManualActivityWizard::field2TagInt
// ManualActivityPageBasics
ManualActivityPageBasics::ManualActivityPageBasics
(Context *context, bool plan, QWidget *parent)
: QWizardPage(parent), context(context), plan(plan)
(Context *context, bool plan, const QDateTime &when, QWidget *parent)
: QWizardPage(parent), context(context), plan(plan), when(when)
{
setTitle(tr("General Information"));
if (plan) {
@@ -317,8 +318,6 @@ ManualActivityPageBasics::ManualActivityPageBasics
}
}
subSportLabel->setVisible(! plan);
subSportEdit->setVisible(! plan);
workoutCodeLabel->setVisible(! plan);
workoutCodeEdit->setVisible(! plan);
rpeLabel->setVisible(! plan);
@@ -373,11 +372,16 @@ void
ManualActivityPageBasics::initializePage
()
{
setField("activityDate", QDate::currentDate());
if (plan) {
setField("activityTime", QTime(16, 0, 0)); // Planned: 16:00 by default
if (when.isValid()) {
setField("activityDate", when);
setField("activityTime", when.time());
} else {
setField("activityTime", QTime::currentTime().addSecs(-4 * 3600)); // Completed: 4 hours ago by default
setField("activityDate", QDateTime::currentDateTime());
if (plan) {
setField("activityTime", QTime(16, 0, 0)); // Planned: 16:00 by default
} else {
setField("activityTime", QTime::currentTime().addSecs(-4 * 3600)); // Completed: 4 hours ago by default
}
}
if (plan) {
setField("woType", 0);
@@ -415,7 +419,7 @@ void
ManualActivityPageBasics::sportsChanged
()
{
QString path(":images/material/summit.svg");
QString path(":images/breeze/games-highscores.svg");
QString sport = RideFile::sportTag(field("sport").toString().trimmed());
if (sport == "Bike") {
path = ":images/material/bike.svg";
@@ -429,8 +433,6 @@ ManualActivityPageBasics::sportsChanged
path = ":images/material/ski.svg";
} else if (sport == "Gym") {
path = ":images/material/weight-lifter.svg";
} else if (! sport.isEmpty()) {
path = ":images/material/torch.svg";
}
wizard()->setPixmap(ICON_TYPE, svgAsColoredPixmap(path, QSize(ICON_SIZE * dpiXFactor, ICON_SIZE * dpiYFactor), ICON_MARGIN * dpiXFactor, ICON_COLOR));
}

View File

@@ -54,7 +54,7 @@ class ManualActivityWizard : public QWizard
PageBusyRejection
};
ManualActivityWizard(Context *context, bool plan = false, QWidget *parent = nullptr);
ManualActivityWizard(Context *context, bool plan = false, const QDateTime &when = QDateTime(), QWidget *parent = nullptr);
protected:
virtual void done(int result) override;
@@ -75,7 +75,7 @@ class ManualActivityPageBasics : public QWizardPage
Q_OBJECT
public:
ManualActivityPageBasics(Context *context, bool plan = false, QWidget *parent = nullptr);
ManualActivityPageBasics(Context *context, bool plan = false, const QDateTime &when = QDateTime(), QWidget *parent = nullptr);
virtual void initializePage() override;
virtual int nextId() const override;
@@ -88,6 +88,7 @@ class ManualActivityPageBasics : public QWizardPage
private:
Context *context;
bool plan = false;
QDateTime when;
QLabel *duplicateActivityLabel;
};

View File

@@ -19,6 +19,9 @@
#include "MetricSelect.h"
#include "RideFileCache.h"
#include "SpecialFields.h"
#include "ActionButtonBox.h"
#include "Colors.h"
MetricSelect::MetricSelect(QWidget *parent, Context *context, int scope)
: QLineEdit(parent), context(context), scope(scope), _metric(NULL)
@@ -186,3 +189,219 @@ SeriesSelect::series()
if (currentIndex() < 0) return RideFile::none;
return static_cast<RideFile::SeriesType>(itemData(currentIndex(), Qt::UserRole).toInt());
}
//////////////////////////////////////////////////////////////////////////////
// MultiMetricSelector
MultiMetricSelector::MultiMetricSelector
(const QString &leftLabel, const QString &rightLabel, const QStringList &selectedMetrics, QWidget *parent)
: QWidget(parent)
{
filterEdit = new QLineEdit();
filterEdit->setPlaceholderText(tr("Filter..."));
availList = new QListWidget();
availList->setSortingEnabled(true);
availList->setAlternatingRowColors(true);
availList->setSelectionMode(QAbstractItemView::SingleSelection);
QVBoxLayout *availLayout = new QVBoxLayout();
availLayout->addWidget(new QLabel(leftLabel));
availLayout->addWidget(filterEdit);
availLayout->addWidget(availList);
selectedList = new QListWidget();
selectedList->setAlternatingRowColors(true);
selectedList->setSelectionMode(QAbstractItemView::SingleSelection);
ActionButtonBox *actionButtons = new ActionButtonBox(ActionButtonBox::UpDownGroup);
actionButtons->defaultConnect(ActionButtonBox::UpDownGroup, selectedList);
QVBoxLayout *selectedLayout = new QVBoxLayout();
selectedLayout->addWidget(new QLabel(rightLabel));
selectedLayout->addWidget(selectedList);
selectedLayout->addWidget(actionButtons);
#ifndef Q_OS_MAC
unselectButton = new QToolButton(this);
unselectButton->setArrowType(Qt::LeftArrow);
unselectButton->setFixedSize(20 * dpiXFactor, 20 * dpiYFactor);
selectButton = new QToolButton(this);
selectButton->setArrowType(Qt::RightArrow);
selectButton->setFixedSize(20 * dpiXFactor, 20 * dpiYFactor);
#else
unselectButton = new QPushButton("<");
selectButton = new QPushButton(">");
#endif
unselectButton->setEnabled(false);
selectButton->setEnabled(false);
QHBoxLayout *inexcLayout = new QHBoxLayout();
inexcLayout->addStretch();
inexcLayout->addWidget(unselectButton);
inexcLayout->addWidget(selectButton);
inexcLayout->addStretch();
QVBoxLayout *buttonGrid = new QVBoxLayout();
buttonGrid->addStretch();
buttonGrid->addLayout(inexcLayout);
buttonGrid->addStretch();
QHBoxLayout *hlayout = new QHBoxLayout(this);;
hlayout->addLayout(availLayout, 2);
hlayout->addLayout(buttonGrid, 1);
hlayout->addLayout(selectedLayout, 2);
setSymbols(selectedMetrics);
connect(actionButtons, &ActionButtonBox::upRequested, this, &MultiMetricSelector::upClicked);
connect(actionButtons, &ActionButtonBox::downRequested, this, &MultiMetricSelector::downClicked);
connect(unselectButton, &QAbstractButton::clicked, this, &MultiMetricSelector::unselectClicked);
connect(selectButton, &QAbstractButton::clicked, this, &MultiMetricSelector::selectClicked);
connect(availList, &QListWidget::itemSelectionChanged, this, &MultiMetricSelector::updateSelectionButtons);
connect(selectedList, &QListWidget::itemSelectionChanged, this, &MultiMetricSelector::updateSelectionButtons);
connect(filterEdit, &QLineEdit::textChanged, this, &MultiMetricSelector::filterAvail);
}
void
MultiMetricSelector::setSymbols
(const QStringList &selectedMetrics)
{
availList->blockSignals(true);
selectedList->blockSignals(true);
availList->clear();
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i = 0; i < factory.metricCount(); ++i) {
QString symbol = factory.metricName(i);
if (selectedMetrics.contains(symbol) || symbol.startsWith("compatibility_")) {
continue;
}
QSharedPointer<RideMetric> m(factory.newMetric(symbol));
QListWidgetItem *item = new QListWidgetItem(Utils::unprotect(m->name()));
item->setData(Qt::UserRole, symbol);
item->setToolTip(m->description());
availList->addItem(item);
}
selectedList->clear();
for (const QString &symbol : selectedMetrics) {
if (! factory.haveMetric(symbol)) {
continue;
}
QSharedPointer<RideMetric> m(factory.newMetric(symbol));
QListWidgetItem *item = new QListWidgetItem(Utils::unprotect(m->name()));
item->setData(Qt::UserRole, symbol);
item->setToolTip(m->description());
selectedList->addItem(item);
}
availList->blockSignals(false);
selectedList->blockSignals(false);
updateSelectionButtons();
emit selectedChanged();
}
QStringList
MultiMetricSelector::getSymbols
() const
{
QStringList metrics;
for (int i = 0; i < selectedList->count(); ++i) {
metrics << selectedList->item(i)->data(Qt::UserRole).toString();
}
return metrics;
}
void
MultiMetricSelector::updateMetrics
()
{
setSymbols(getSymbols());
if (! filterEdit->text().isEmpty()) {
filterAvail(filterEdit->text());
}
}
void
MultiMetricSelector::filterAvail
(const QString &text)
{
for (int i = 0; i < availList->count(); ++i) {
QListWidgetItem *item = availList->item(i);
item->setHidden(! item->text().contains(text, Qt::CaseInsensitive));
}
}
void
MultiMetricSelector::upClicked
()
{
assert(!selectedList->selectedItems().isEmpty());
QListWidgetItem *item = selectedList->selectedItems().first();
int row = selectedList->row(item);
assert(row > 0);
selectedList->takeItem(row);
selectedList->insertItem(row - 1, item);
selectedList->setCurrentItem(item);
emit selectedChanged();
}
void
MultiMetricSelector::downClicked
()
{
assert(!selectedList->selectedItems().isEmpty());
QListWidgetItem *item = selectedList->selectedItems().first();
int row = selectedList->row(item);
assert(row < selectedList->count() - 1);
selectedList->takeItem(row);
selectedList->insertItem(row + 1, item);
selectedList->setCurrentItem(item);
emit selectedChanged();
}
void
MultiMetricSelector::unselectClicked
()
{
assert(!selectedList->selectedItems().isEmpty());
QListWidgetItem *item = selectedList->selectedItems().first();
selectedList->takeItem(selectedList->row(item));
availList->addItem(item);
updateSelectionButtons();
emit selectedChanged();
}
void
MultiMetricSelector::selectClicked
()
{
assert(!availList->selectedItems().isEmpty());
QListWidgetItem *item = availList->selectedItems().first();
availList->takeItem(availList->row(item));
selectedList->addItem(item);
updateSelectionButtons();
emit selectedChanged();
}
void
MultiMetricSelector::updateSelectionButtons
()
{
unselectButton->setEnabled(! selectedList->selectedItems().isEmpty());
selectButton->setEnabled(! availList->selectedItems().isEmpty());
}

View File

@@ -21,10 +21,11 @@
#include <QWidget>
#include <QLineEdit>
#include <RideMetric.h>
#include <QListWidget>
#include <QCompleter>
#include <QMap>
#include "RideMetric.h"
#include "RideMetadata.h"
#include "Context.h"
#include "Athlete.h"
@@ -84,4 +85,40 @@ class SeriesSelect : public QComboBox
int scope;
};
class MultiMetricSelector : public QWidget
{
Q_OBJECT
public:
MultiMetricSelector(const QString &leftLabel, const QString &rightLabel, const QStringList &selectedMetrics, QWidget *parent = nullptr);
void setSymbols(const QStringList &selectedMetrics);
QStringList getSymbols() const;
void updateMetrics();
signals:
void selectedChanged();
private:
QLineEdit *filterEdit;
QListWidget *availList;
QListWidget *selectedList;
#ifndef Q_OS_MAC
QToolButton *selectButton;
QToolButton *unselectButton;
#else
QPushButton *selectButton;
QPushButton *unselectButton;
#endif
private slots:
void filterAvail(const QString &filter);
void upClicked();
void downClicked();
void unselectClicked();
void selectClicked();
void updateSelectionButtons();
};
#endif

View File

@@ -81,6 +81,20 @@ private:
QVector<int> sourceRowToGroupRow;
QList<rankx> rankedRows;
int countResetInProgress = 0;
void myBeginResetModel() {
if (countResetInProgress++ == 0) {
beginResetModel();
}
}
void myEndResetModel() {
if (--countResetInProgress == 0) {
endResetModel();
}
}
void clearGroups() {
// Wipe current
QMapIterator<QString, QVector<int>*> i(groupToSourceRow);
@@ -511,7 +525,7 @@ public:
void setGroups() {
// let the views know we're doing this
beginResetModel();
myBeginResetModel();
// wipe whatever is there first
clearGroups();
@@ -579,7 +593,7 @@ public:
}
// all done. let the views know everything changed
endResetModel();
myEndResetModel();
}
public slots:
@@ -587,13 +601,13 @@ public slots:
void sourceModelChanged() {
// notify everyone we're changing
beginResetModel();
myBeginResetModel();
clearGroups();
setGroupBy(groupBy+2); // accommodate virtual columns
setIndexes();
endResetModel();// we're clean
myEndResetModel();// we're clean
// lets expand column 0 for the groupBy heading
for (int i=0; i < groupCount(); i++)

View File

@@ -124,6 +124,18 @@
<file>xml/home-perspectives.xml</file>
<file>ini/measures.ini</file>
<file>html/ltm-summary.html</file>
<file>images/breeze/games-highscores.svg</file>
<file>images/breeze/network-mobile-0.svg</file>
<file>images/breeze/network-mobile-20.svg</file>
<file>images/breeze/network-mobile-40.svg</file>
<file>images/breeze/network-mobile-60.svg</file>
<file>images/breeze/network-mobile-80.svg</file>
<file>images/breeze/network-mobile-100.svg</file>
<file>images/breeze/task-process-0.svg</file>
<file>images/breeze/task-process-1.svg</file>
<file>images/breeze/task-process-2.svg</file>
<file>images/breeze/task-process-3.svg</file>
<file>images/breeze/task-process-4.svg</file>
<file>images/breeze/light/unknown.svg</file>
<file>images/breeze/light/offline.svg</file>
<file>images/breeze/light/online.svg</file>
@@ -138,6 +150,10 @@
<file>images/breeze/light/up.svg</file>
<file>images/breeze/light/down.svg</file>
<file>images/breeze/light/cal.svg</file>
<file>images/breeze/light/go-next-skip.svg</file>
<file>images/breeze/light/go-next.svg</file>
<file>images/breeze/light/go-previous-skip.svg</file>
<file>images/breeze/light/go-previous.svg</file>
<file>images/breeze/dark/unknown.svg</file>
<file>images/breeze/dark/offline.svg</file>
<file>images/breeze/dark/online.svg</file>
@@ -152,6 +168,10 @@
<file>images/breeze/dark/up.svg</file>
<file>images/breeze/dark/down.svg</file>
<file>images/breeze/dark/cal.svg</file>
<file>images/breeze/dark/go-next-skip.svg</file>
<file>images/breeze/dark/go-next.svg</file>
<file>images/breeze/dark/go-previous-skip.svg</file>
<file>images/breeze/dark/go-previous.svg</file>
<file>images/toolbar/close-icon.png</file>
<file>images/toolbar/save.png</file>
<file>images/toolbar/saveas.png</file>
@@ -220,14 +240,12 @@
<file>images/services/polarflow.png</file>
<file>images/services/sporttracks.png</file>
<file>images/services/nolio.png</file>
<file>images/material/summit.svg</file>
<file>images/material/bike.svg</file>
<file>images/material/run.svg</file>
<file>images/material/swim.svg</file>
<file>images/material/rowing.svg</file>
<file>images/material/ski.svg</file>
<file>images/material/weight-lifter.svg</file>
<file>images/material/torch.svg</file>
<file>python/library.py</file>
<file>images/devices/imagic.png</file>
<file>data/powerprofile.csv</file>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
<path d="M4 3.707L4.707 3l8 8-8 8L4 18.293 11.293 11zm5 0L9.707 3l8 8-8 8L9 18.293 16.293 11z" class="ColorScheme-Text" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 353 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="m7.707031 3l-.707031.707031 6.125 6.125 1.167969 1.167969-1.167969 1.167969-6.125 6.125.707031.707031 6.125-6.125 1.875-1.875-1.875-1.875-6.125-6.125" class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
<path d="M18 3.707L17.293 3l-8 8 8 8 .707-.707L10.707 11zm-5 0L12.293 3l-8 8 8 8 .707-.707L5.707 11z" class="ColorScheme-Text" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="m14.292969 3l-6.125 6.125-1.875 1.875 1.875 1.875 6.125 6.125.707031-.707031-6.125-6.125-1.167969-1.167969 1.167969-1.167969 6.125-6.125-.707031-.707031" class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 5.0175781 2 C 5.0289131 2.4577976 5.0297601 2.7995517 5.0332031 3.2304688 C 3.9693109 3.1670214 2.9215824 3.0922409 2.0175781 3 C 2.0175781 6.64947 3.6879081 10.288801 7.0175781 10.900391 L 7.0175781 13 L 6.0175781 13 L 6.0175781 14 L 7.0175781 14 L 9.0175781 14 L 10.017578 14 L 10.017578 13 L 9.0175781 13 L 9.0175781 10.900391 C 12.151965 10.324668 13.796075 7.0632233 13.980469 3.6386719 C 13.998419 3.4231506 13.996928 3.2927952 14 3 C 12.963105 3.0980237 11.895422 3.1752174 10.994141 3.2226562 C 11.001842 2.8151362 11.010558 2.4075306 11.017578 2 C 9.2082283 2.45279 7.2624581 2.58158 5.0175781 2 z M 8.0175781 4 L 8.6367188 5.3164062 L 10.017578 5.5273438 L 9.0175781 6.5527344 L 9.2539062 8 L 8.0175781 7.3164062 L 6.78125 8 L 7.0175781 6.5527344 L 6.0175781 5.5273438 L 7.3984375 5.3164062 L 8.0175781 4 z M 12.964844 4.09375 C 12.776632 6.3665143 11.957626 8.8475713 9.7578125 9.6953125 C 10.538634 8.2533355 10.825859 5.9576496 10.9375 4.234375 C 11.591174 4.200215 12.271992 4.151513 12.964844 4.09375 z M 3.0722656 4.0996094 C 3.7597792 4.1576111 4.4419381 4.2085484 5.0976562 4.2421875 C 5.2096661 5.9646696 5.4976493 8.2553996 6.2773438 9.6953125 C 4.0794644 8.8483131 3.2614206 6.3707486 3.0722656 4.0996094 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,10 @@
<svg viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
<path d="M4 3.707L4.707 3l8 8-8 8L4 18.293 11.293 11zm5 0L9.707 3l8 8-8 8L9 18.293 16.293 11z" class="ColorScheme-Text" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 362 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path
style="fill:currentColor;fill-opacity:1;stroke:none"
d="m7.707031 3l-.707031.707031 6.125 6.125 1.167969 1.167969-1.167969 1.167969-6.125 6.125.707031.707031 6.125-6.125 1.875-1.875-1.875-1.875-6.125-6.125"
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 482 B

View File

@@ -0,0 +1,10 @@
<svg viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<style
type="text/css"
id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
<path d="M18 3.707L17.293 3l-8 8 8 8 .707-.707L10.707 11zm-5 0L12.293 3l-8 8 8 8 .707-.707L5.707 11z" class="ColorScheme-Text" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path
style="fill:currentColor;fill-opacity:1;stroke:none"
d="m14.292969 3l-6.125 6.125-1.875 1.875 1.875 1.875 6.125 6.125.707031-.707031-6.125-6.125-1.167969-1.167969 1.167969-1.167969 6.125-6.125-.707031-.707031"
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 485 B

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<defs id="defs4157">
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text {
color:#31363b;
}
</style>
</defs>
<g transform="translate(1,1)">
<path class="ColorScheme-Text" id="path4330" d="M 3,19 19,3 v 16 z" style="opacity:0.35;fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 522 B

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<defs id="defs4157">
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text {
color:#31363b;
}
</style>
</defs>
<g transform="translate(1,1)">
<path class="ColorScheme-Text" d="M 3,19 19,3 v 16 z" style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" id="path4317"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 509 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<defs id="defs4157">
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text {
color:#31363b;
}
</style>
</defs>
<g transform="translate(1,1)">
<path class="ColorScheme-Text" id="path4315" d="M 3,19 19,3 v 16 z" style="opacity:0.35;fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 10,12 3,19 h 7 z" id="path4317"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 743 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<defs id="defs4157">
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text {
color:#31363b;
}
</style>
</defs>
<g transform="translate(1,1)">
<path class="ColorScheme-Text" id="path4315" d="M 3,19 19,3 v 16 z" style="opacity:0.35;fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 13,9 3,19 h 10 z" id="path4317"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 743 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<defs id="defs4157">
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text {
color:#31363b;
}
</style>
</defs>
<g transform="translate(1,1)">
<path class="ColorScheme-Text" id="path4315" d="M 3,19 19,3 v 16 z" style="opacity:0.35;fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 15,7 3,19 h 12 z" id="path4317"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 743 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<defs id="defs4157">
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text {
color:#31363b;
}
</style>
</defs>
<g transform="translate(1,1)">
<path class="ColorScheme-Text" id="path4315" d="M 3,19 19,3 v 16 z" style="opacity:0.35;fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 17,5 3,19 h 14 z" id="path4317"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 743 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor"
d="M 8 2 A 6 6 0 0 0 2 8 A 6 6 0 0 0 8 14 A 6 6 0 0 0 14 8 A 6 6 0 0 0 8 2 z M 8 3 A 5 5 0 0 1 13 8 A 5 5 0 0 1 8 13 A 5 5 0 0 1 3 8 A 5 5 0 0 1 8 3 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 456 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor"
d="M 8 2 A 6 6 0 0 0 2 8 A 6 6 0 0 0 8 14 A 6 6 0 0 0 14 8 A 6 6 0 0 0 8 2 z M 8 3 L 8 8 L 13 8 A 5 5 0 0 1 8 13 A 5 5 0 0 1 3 8 A 5 5 0 0 1 8 3 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 452 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor"
d="M 8 2 C 4.7 2 2 4.7 2 8 C 2 11.3 4.7 14 8 14 C 11.3 14 14 11.3 14 8 C 14 4.7 11.3 2 8 2 z M 8 3 L 8 13 C 5.2 13 3 10.8 3 8 C 3 5.2 5.2 3 8 3 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 451 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor"
d="M 8 2 A 6 6 0 0 0 2 8 A 6 6 0 0 0 8 14 A 6 6 0 0 0 14 8 A 6 6 0 0 0 8 2 z M 8 3 L 8 8 L 3 8 A 5 5 0 0 1 8 3 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor"
d="M 8 2 C 4.7 2 2 4.7 2 8 C 2 11.3 4.7 14 8 14 C 11.3 14 14 11.3 14 8 C 14 4.7 11.3 2 8 2 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -1,4 +0,0 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-summit" viewBox="0 0 24 24">
<path d="M15,3H17L22,5L17,7V10.17L22,21H2L8,13L11.5,17.7L15,10.17V3Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 200 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-torch" viewBox="0 0 24 24"><path d="M8.6 9.6C9 10.2 9.5 10.7 10.2 11H14.2C14.5 10.9 14.7 10.7 14.9 10.5C15.9 9.5 16.3 8 15.8 6.7L15.7 6.5C15.6 6.2 15.4 6 15.2 5.8C15.1 5.6 14.9 5.5 14.8 5.3C14.4 5 14 4.7 13.6 4.3C12.7 3.4 12.6 2 13.1 1C12.6 1.1 12.1 1.4 11.7 1.8C10.2 3 9.6 5.1 10.3 7V7.2C10.3 7.3 10.2 7.4 10.1 7.5C10 7.6 9.8 7.5 9.7 7.4L9.6 7.3C9 6.5 8.9 5.3 9.3 4.3C8.4 5.1 7.9 6.4 8 7.7C8 8 8.1 8.3 8.2 8.6C8.2 8.9 8.4 9.3 8.6 9.6M12.3 8.1C12.4 7.6 12.2 7.2 12.1 6.8C12 6.4 12 6 12.2 5.6L12.5 6.2C12.9 6.8 13.6 7 13.8 7.8V8.1C13.8 8.6 13.6 9.1 13.3 9.4C13.1 9.5 12.9 9.7 12.7 9.7C12.1 9.9 11.4 9.6 11 9.2C11.8 9.2 12.2 8.6 12.3 8.1M15 12V14H14L13 22H11L10 14H9V12H15Z" /></svg>

Before

Width:  |  Height:  |  Size: 729 B

View File

@@ -606,7 +606,7 @@ HEADERS += Charts/Aerolab.h Charts/AerolabWindow.h Charts/AllPlot.h Charts/AllPl
Charts/MetadataWindow.h Charts/MUPlot.h Charts/MUPool.h Charts/MUWidget.h Charts/PfPvPlot.h Charts/PfPvWindow.h \
Charts/PowerHist.h Charts/ReferenceLineDialog.h Charts/RideEditor.h Charts/RideMapWindow.h \
Charts/ScatterPlot.h Charts/ScatterWindow.h Charts/SmallPlot.h Charts/TreeMapPlot.h \
Charts/TreeMapWindow.h Charts/ZoneScaleDraw.h
Charts/TreeMapWindow.h Charts/ZoneScaleDraw.h Charts/PlanningCalendarWindow.h
# cloud services
HEADERS += Cloud/CalendarDownload.h Cloud/CloudService.h \
@@ -650,7 +650,8 @@ HEADERS += Gui/AboutDialog.h Gui/AddIntervalDialog.h Gui/AnalysisSidebar.h Gui/C
Gui/MergeActivityWizard.h Gui/RideImportWizard.h Gui/SplitActivityWizard.h Gui/SolverDisplay.h Gui/MetricSelect.h \
Gui/AddTileWizard.h Gui/NavigationModel.h Gui/AthleteView.h Gui/AthleteConfigDialog.h Gui/AthletePages.h Gui/Perspective.h \
Gui/PerspectiveDialog.h Gui/SplashScreen.h Gui/StyledItemDelegates.h Gui/MetadataDialog.h Gui/ActionButtonBox.h \
Gui/MetricOverrideDialog.h
Gui/MetricOverrideDialog.h \
Gui/Calendar.h Gui/CalendarData.h Gui/CalendarItemDelegates.h
# metrics and models
HEADERS += Metrics/Banister.h Metrics/CPSolver.h Metrics/Estimator.h Metrics/ExtendedCriticalPower.h Metrics/HrZones.h Metrics/PaceZones.h \
@@ -714,7 +715,7 @@ SOURCES += Charts/Aerolab.cpp Charts/AerolabWindow.cpp Charts/AllPlot.cpp Charts
Charts/MetadataWindow.cpp Charts/MUPlot.cpp Charts/MUWidget.cpp Charts/PfPvPlot.cpp Charts/PfPvWindow.cpp \
Charts/PowerHist.cpp Charts/ReferenceLineDialog.cpp Charts/RideEditor.cpp Charts/RideMapWindow.cpp \
Charts/ScatterPlot.cpp Charts/ScatterWindow.cpp Charts/SmallPlot.cpp Charts/TreeMapPlot.cpp \
Charts/TreeMapWindow.cpp
Charts/TreeMapWindow.cpp Charts/PlanningCalendarWindow.cpp
## Cloud Services / Web resources
SOURCES += Cloud/CalendarDownload.cpp Cloud/CloudService.cpp \
@@ -761,7 +762,8 @@ SOURCES += Gui/AboutDialog.cpp Gui/AddIntervalDialog.cpp Gui/AnalysisSidebar.cpp
Gui/MergeActivityWizard.cpp Gui/RideImportWizard.cpp Gui/SplitActivityWizard.cpp Gui/SolverDisplay.cpp Gui/MetricSelect.cpp \
Gui/AddTileWizard.cpp Gui/NavigationModel.cpp Gui/AthleteView.cpp Gui/AthleteConfigDialog.cpp Gui/AthletePages.cpp Gui/Perspective.cpp \
Gui/PerspectiveDialog.cpp Gui/SplashScreen.cpp Gui/StyledItemDelegates.cpp Gui/MetadataDialog.cpp Gui/ActionButtonBox.cpp \
Gui/MetricOverrideDialog.cpp
Gui/MetricOverrideDialog.cpp \
Gui/Calendar.cpp Gui/CalendarItemDelegates.cpp
## Models and Metrics
SOURCES += Metrics/aBikeScore.cpp Metrics/aCoggan.cpp Metrics/AerobicDecoupling.cpp Metrics/Banister.cpp Metrics/BasicRideMetrics.cpp \