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
701
src/Charts/PlanningCalendarWindow.cpp
Normal 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;
|
||||
}
|
||||
98
src/Charts/PlanningCalendarWindow.h
Normal 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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
440
src/Gui/CalendarItemDelegates.cpp
Normal 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> </td><td align='right'>%2</td></tr>").arg(p.first).arg(p.second);
|
||||
}
|
||||
tooltip += "</table>";
|
||||
QToolTip::showText(event->globalPos(), tooltip, view);
|
||||
return true;
|
||||
}
|
||||
36
src/Gui/CalendarItemDelegates.h
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -77,7 +77,8 @@ enum gcwinid {
|
||||
LiveMapWebPageWindow = 48,
|
||||
OverviewAnalysisBlank=49,
|
||||
OverviewTrendsBlank=50,
|
||||
ElevationChart=51
|
||||
ElevationChart=51,
|
||||
Calendar=52
|
||||
};
|
||||
};
|
||||
typedef enum GcWindowTypes::gcwinid GcWinID;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/*----------------------------------------------------------------------
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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>
|
||||
|
||||
5
src/Resources/images/breeze/dark/go-next-skip.svg
Normal 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 |
7
src/Resources/images/breeze/dark/go-next.svg
Normal 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 |
5
src/Resources/images/breeze/dark/go-previous-skip.svg
Normal 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 |
7
src/Resources/images/breeze/dark/go-previous.svg
Normal 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 |
13
src/Resources/images/breeze/games-highscores.svg
Normal 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 |
10
src/Resources/images/breeze/light/go-next-skip.svg
Normal 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 |
14
src/Resources/images/breeze/light/go-next.svg
Normal 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 |
10
src/Resources/images/breeze/light/go-previous-skip.svg
Normal 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 |
14
src/Resources/images/breeze/light/go-previous.svg
Normal 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 |
12
src/Resources/images/breeze/network-mobile-0.svg
Normal 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 |
12
src/Resources/images/breeze/network-mobile-100.svg
Normal 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 |
13
src/Resources/images/breeze/network-mobile-20.svg
Normal 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 |
13
src/Resources/images/breeze/network-mobile-40.svg
Normal 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 |
13
src/Resources/images/breeze/network-mobile-60.svg
Normal 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 |
13
src/Resources/images/breeze/network-mobile-80.svg
Normal 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 |
13
src/Resources/images/breeze/task-process-0.svg
Normal 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 |
13
src/Resources/images/breeze/task-process-1.svg
Normal 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 |
13
src/Resources/images/breeze/task-process-2.svg
Normal 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 |
13
src/Resources/images/breeze/task-process-3.svg
Normal 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 |
13
src/Resources/images/breeze/task-process-4.svg
Normal 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 |
@@ -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 |
@@ -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 |
10
src/src.pro
@@ -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 \
|
||||
|
||||