Calendar: Agenda View (#4727)

* Read-only view to show
  * missed planned activities (configurable number of days to look back)
  * todays planned activities, phases and events
  * upcoming planned activities, phases and events (configurable number
    of days to look ahead)
* Color of planned activities is now user configurable (coming from
  global color theme)
* Added a hint to show whether a filter is active (all calendar views)
* Configuration: Reorganized in tabs
* Refactoring: Created enums for all user roles used in the calendar
  (all views, all delegates) to improve readability; removed data that
  was only written but never read
This commit is contained in:
Joachim Kohlhammer
2025-11-10 16:08:27 +01:00
committed by GitHub
parent d12f14f4a4
commit 84d7272f6d
11 changed files with 1287 additions and 199 deletions

View File

@@ -44,6 +44,8 @@ PlanningCalendarWindow::PlanningCalendarWindow(Context *context)
setStartHour(8);
setEndHour(21);
setAgendaPastDays(7);
setAgendaFutureDays(7);
QVBoxLayout *mainLayout = new QVBoxLayout();
setChartLayout(mainLayout);
@@ -236,6 +238,46 @@ PlanningCalendarWindow::setEndHour
}
int
PlanningCalendarWindow::getAgendaPastDays
() const
{
return agendaPastDaysSpin->value();
}
void
PlanningCalendarWindow::setAgendaPastDays
(int days)
{
agendaPastDaysSpin->setValue(days);
if (calendar != nullptr) {
calendar->setAgendaPastDays(days);
updateActivities();
}
}
int
PlanningCalendarWindow::getAgendaFutureDays
() const
{
return agendaFutureDaysSpin->value();
}
void
PlanningCalendarWindow::setAgendaFutureDays
(int days)
{
agendaFutureDaysSpin->setValue(days);
if (calendar != nullptr) {
calendar->setAgendaFutureDays(days);
updateActivities();
}
}
bool
PlanningCalendarWindow::isSummaryVisibleDay
() const
@@ -473,9 +515,10 @@ PlanningCalendarWindow::mkControls
{
QLocale locale;
defaultViewCombo = new QComboBox();
defaultViewCombo->addItem(tr("Day View"));
defaultViewCombo->addItem(tr("Week View"));
defaultViewCombo->addItem(tr("Month View"));
defaultViewCombo->addItem(tr("Day"));
defaultViewCombo->addItem(tr("Week"));
defaultViewCombo->addItem(tr("Month"));
defaultViewCombo->addItem(tr("Agenda"));
defaultViewCombo->setCurrentIndex(static_cast<int>(CalendarView::Month));
firstDayOfWeekCombo = new QComboBox();
for (int i = Qt::Monday; i <= Qt::Sunday; ++i) {
@@ -488,6 +531,12 @@ PlanningCalendarWindow::mkControls
endHourSpin = new QSpinBox();
endHourSpin->setSuffix(":00");
endHourSpin->setMaximum(24);
agendaPastDaysSpin = new QSpinBox();
agendaPastDaysSpin->setMaximum(31);
agendaPastDaysSpin->setSuffix(" " + tr("day(s)"));
agendaFutureDaysSpin = new QSpinBox();
agendaFutureDaysSpin->setMaximum(31);
agendaFutureDaysSpin->setSuffix(" " + tr("day(s)"));
summaryDayCheck = new QCheckBox(tr("Day View"));
summaryDayCheck->setChecked(true);
summaryWeekCheck = new QCheckBox(tr("Week View"));
@@ -510,37 +559,38 @@ PlanningCalendarWindow::mkControls
tertiaryCombo->setCurrentText("Notes");
QStringList summaryMetrics { "ride_count", "total_distance", "coggan_tss", "workout_time" };
multiMetricSelector = new MultiMetricSelector(tr("Available Metrics"), tr("Selected Metrics"), summaryMetrics);
multiMetricSelector->setContentsMargins(10 * dpiXFactor, 10 * dpiYFactor, 10 * dpiXFactor, 10 * dpiYFactor);
multiMetricSelector->setMinimumHeight(300 * dpiYFactor);
QFormLayout *formLayout = newQFormLayout();
formLayout->addRow(tr("Default View on Startup"), defaultViewCombo);
formLayout->addRow(tr("First Day of Week"), firstDayOfWeekCombo);
formLayout->addRow(tr("Default Start Time"), startHourSpin);
formLayout->addRow(tr("Default End Time"), endHourSpin);
formLayout->addRow(tr("Show Summary In"), summaryDayCheck);
formLayout->addRow("", summaryWeekCheck);
formLayout->addRow("", summaryMonthCheck);
formLayout->addItem(new QSpacerItem(0, 10 * dpiYFactor, QSizePolicy::Minimum, QSizePolicy::Fixed));
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(tr("Field for Tertiary Line (Day and Week View)"), tertiaryCombo);
formLayout->addItem(new QSpacerItem(0, 10 * dpiYFactor, QSizePolicy::Minimum, QSizePolicy::Fixed));
formLayout->addRow(new QLabel(HLO + tr("Summary") + HLC));
QFormLayout *generalForm = newQFormLayout();
generalForm->setContentsMargins(0, 10 * dpiYFactor, 0, 10 * dpiYFactor);
generalForm->addRow(tr("Startup View"), defaultViewCombo);
generalForm->addRow(tr("First Day of Week"), firstDayOfWeekCombo);
generalForm->addRow(tr("Default Start Time"), startHourSpin);
generalForm->addRow(tr("Default End Time"), endHourSpin);
generalForm->addRow(tr("Agenda: Look Back"), agendaPastDaysSpin);
generalForm->addRow(tr("Agenda: Look Forward"), agendaFutureDaysSpin);
generalForm->addRow(tr("Show Summary In"), summaryDayCheck);
generalForm->addRow("", summaryWeekCheck);
generalForm->addRow("", summaryMonthCheck);
QWidget *controlsWidget = new QWidget();
QVBoxLayout *controlsLayout = new QVBoxLayout(controlsWidget);
controlsLayout->addWidget(centerLayoutInWidget(formLayout, false));
controlsLayout->addWidget(multiMetricSelector);
QFormLayout *entriesForm = newQFormLayout();
entriesForm->setContentsMargins(0, 10 * dpiYFactor, 0, 10 * dpiYFactor);
entriesForm->addRow(tr("Field for Primary Line"), primaryMainCombo);
entriesForm->addRow(tr("Fallback Field for Primary Line"), primaryFallbackCombo);
entriesForm->addRow(tr("Metric for Secondary Line"), secondaryCombo);
entriesForm->addRow(tr("Field for Tertiary Line (Day and Week View)"), tertiaryCombo);
QScrollArea *controlsScroller = new QScrollArea();
controlsScroller->setWidgetResizable(true);
controlsScroller->setWidget(controlsWidget);
QTabWidget *controlsTabs = new QTabWidget();
controlsTabs->addTab(centerLayoutInWidget(generalForm, false), tr("General"));
controlsTabs->addTab(centerLayoutInWidget(entriesForm, false), tr("Calendar Entries"));
controlsTabs->addTab(multiMetricSelector, tr("Summary"));
#if QT_VERSION < 0x060000
connect(startHourSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &PlanningCalendarWindow::setStartHour);
connect(endHourSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &PlanningCalendarWindow::setEndHour);
connect(agendaPastDaysSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &PlanningCalendarWindow::setAgendaPastDays);
connect(agendaFutureDaysSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &PlanningCalendarWindow::setAgendaFutureDays);
connect(defaultViewCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::setDefaultView);
connect(firstDayOfWeekCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), [=](int idx) { setFirstDayOfWeek(idx + 1); });
connect(primaryMainCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PlanningCalendarWindow::updateActivities);
@@ -550,6 +600,8 @@ PlanningCalendarWindow::mkControls
#else
connect(startHourSpin, &QSpinBox::valueChanged, this, &PlanningCalendarWindow::setStartHour);
connect(endHourSpin, &QSpinBox::valueChanged, this, &PlanningCalendarWindow::setEndHour);
connect(agendaPastDaysSpin, &QSpinBox::valueChanged, this, &PlanningCalendarWindow::setAgendaPastDays);
connect(agendaFutureDaysSpin, &QSpinBox::valueChanged, this, &PlanningCalendarWindow::setAgendaFutureDays);
connect(defaultViewCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::setDefaultView);
connect(firstDayOfWeekCombo, &QComboBox::currentIndexChanged, [=](int idx) { setFirstDayOfWeek(idx + 1); });
connect(primaryMainCombo, &QComboBox::currentIndexChanged, this, &PlanningCalendarWindow::updateActivities);
@@ -562,7 +614,7 @@ PlanningCalendarWindow::mkControls
connect(summaryMonthCheck, &QCheckBox::toggled, this, &PlanningCalendarWindow::setSummaryVisibleMonth);
connect(multiMetricSelector, &MultiMetricSelector::selectedChanged, this, &PlanningCalendarWindow::updateActivities);
setControls(controlsScroller);
setControls(controlsTabs);
}
@@ -694,7 +746,7 @@ PlanningCalendarWindow::getActivities
activity.iconFile = IconManager::instance().getFilepath(rideItem);
if (rideItem->color.alpha() < 255 || rideItem->planned) {
activity.color = QColor("#F79130");
activity.color = GColor(CCALPLANNED);
} else {
activity.color = rideItem->color;
}
@@ -770,33 +822,6 @@ 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) {
@@ -821,7 +846,7 @@ PlanningCalendarWindow::getPhasesEvents
} else {
entry.iconFile = ":images/breeze/task-process-0.svg";
}
entry.color = Qt::yellow;
entry.color = Qt::yellow; // TODO: Use color from GC-theme instead
entry.reference = event.id;
entry.start = QTime(0, 0, 0);
entry.durationSecs = 0;
@@ -831,6 +856,33 @@ PlanningCalendarWindow::getPhasesEvents
}
}
}
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; // TODO: Use color from GC-theme instead
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;
}
}
}
}
return phasesEvents;
}
@@ -854,7 +906,7 @@ PlanningCalendarWindow::updateActivities
} else {
summaries = getSummaries(calendar->firstVisibleDay(), calendar->lastVisibleDay(), 7);
}
calendar->fillEntries(activities, summaries, phasesEvents);
calendar->fillEntries(activities, summaries, phasesEvents, isFiltered());
}

View File

@@ -42,6 +42,8 @@ class PlanningCalendarWindow : public GcChartWindow
Q_PROPERTY(int firstDayOfWeek READ getFirstDayOfWeek WRITE setFirstDayOfWeek USER true)
Q_PROPERTY(int startHour READ getStartHour WRITE setStartHour USER true)
Q_PROPERTY(int endHour READ getEndHour WRITE setEndHour USER true)
Q_PROPERTY(int agendaPastDays READ getAgendaPastDays WRITE setAgendaPastDays USER true)
Q_PROPERTY(int agendaFutureDays READ getAgendaFutureDays WRITE setAgendaFutureDays USER true)
Q_PROPERTY(bool summaryVisibleDay READ isSummaryVisibleDay WRITE setSummaryVisibleDay USER true)
Q_PROPERTY(bool summaryVisibleWeek READ isSummaryVisibleWeek WRITE setSummaryVisibleWeek USER true)
Q_PROPERTY(bool summaryVisibleMonth READ isSummaryVisibleMonth WRITE setSummaryVisibleMonth USER true)
@@ -58,6 +60,8 @@ class PlanningCalendarWindow : public GcChartWindow
int getFirstDayOfWeek() const;
int getStartHour() const;
int getEndHour() const;
int getAgendaPastDays() const;
int getAgendaFutureDays() const;
bool isSummaryVisibleDay() const;
bool isSummaryVisibleWeek() const;
bool isSummaryVisibleMonth() const;
@@ -76,6 +80,8 @@ class PlanningCalendarWindow : public GcChartWindow
void setFirstDayOfWeek(int fdw);
void setStartHour(int hour);
void setEndHour(int hour);
void setAgendaPastDays(int days);
void setAgendaFutureDays(int days);
void setSummaryVisibleDay(bool visible);
void setSummaryVisibleWeek(bool visible);
void setSummaryVisibleMonth(bool svm);
@@ -95,6 +101,8 @@ class PlanningCalendarWindow : public GcChartWindow
QComboBox *firstDayOfWeekCombo;
QSpinBox *startHourSpin;
QSpinBox *endHourSpin;
QSpinBox *agendaPastDaysSpin;
QSpinBox *agendaFutureDaysSpin;
QCheckBox *summaryDayCheck;
QCheckBox *summaryWeekCheck;
QCheckBox *summaryMonthCheck;

View File

@@ -52,6 +52,7 @@
#include "RemoteControl.h"
#include "Measures.h"
#include "StyledItemDelegates.h"
#include "Qt5Compatibility.h"
class MeasuresPage : public QWidget
{
@@ -245,31 +246,6 @@ class SchemePage : public QWidget
};
// Compatibility helper for Qt5
// exposes methods that turned public in Qt6 from protected in Qt5
#if QT_VERSION < 0x060000
class TreeWidget6 : public QTreeWidget
{
Q_OBJECT
public:
TreeWidget6(QWidget *parent = nullptr): QTreeWidget(parent) {
}
QModelIndex indexFromItem(const QTreeWidgetItem *item, int column = 0) const {
return QTreeWidget::indexFromItem(item, column);
}
QTreeWidgetItem* itemFromIndex(const QModelIndex &index) const {
return QTreeWidget::itemFromIndex(index);
}
};
#else
typedef QTreeWidget TreeWidget6;
#endif
class CPPage : public QWidget
{
Q_OBJECT

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
#include <QStackedWidget>
#include <QTableWidget>
#include <QCalendarWidget>
#include <QPushButton>
#include <QToolBar>
#include <QMenu>
#include <QLabel>
#include <QComboBox>
@@ -36,6 +36,7 @@
#include "CalendarData.h"
#include "TimeUtils.h"
#include "Measures.h"
#include "Qt5Compatibility.h"
class CalendarOverview : public QCalendarWidget {
@@ -140,6 +141,10 @@ class CalendarMonthTable : public QTableWidget {
Q_OBJECT
public:
enum CalendarDayTableRoles {
DateRole = Qt::UserRole + 1000 // [QDate] Date of cell
};
explicit CalendarMonthTable(Qt::DayOfWeek firstDayOfWeek = Qt::Monday, QWidget *parent = nullptr);
explicit CalendarMonthTable(const QDate &dateInMonth, Qt::DayOfWeek firstDayOfWeek = Qt::Monday, QWidget *parent = nullptr);
@@ -212,7 +217,8 @@ private:
enum class CalendarView {
Day = 0,
Week = 1,
Month = 2
Month = 2,
Agenda = 3
};
@@ -285,6 +291,65 @@ private:
};
struct CalendarAgendaStyles {
QFont defaultFont;
QFont relativeFont;
QFont hoverFont;
QFont headlineDefaultFont;
QFont headlineTodayFont;
QFont headlineEmptyFont;
QFont headlineSmallEmptyFont;
int sectionSpacerHeight;
int sectionEntrySpacerHeight;
int entrySpacerHeight;
int daySpacerHeight;
};
class CalendarAgendaView : public QWidget {
Q_OBJECT
public:
explicit CalendarAgendaView(QWidget *parent = nullptr);
void updateDate();
void setDateRange(const DateRange &dateRange);
void setPastDays(int days);
void setFutureDays(int days);
void fillEntries(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries);
QDate firstVisibleDay() const;
QDate lastVisibleDay() const;
QDate selectedDate() const;
signals:
void showInTrainMode(const CalendarEntry &activity);
void showInMonthView(const QDate &date);
void viewActivity(const CalendarEntry &activity);
void dayChanged(const QDate &date);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
void changeEvent(QEvent *event) override;
private:
QDate currentDate;
DateRange dateRange;
int pastDays = 7;
int futureDays = 7;
TreeWidget6 *agendaTree;
QTreeWidgetItem *lastHoveredItem = nullptr;
int lastHoveredColumn = -1;
void clearHover();
void addEntries(const QDate &today, const QDate &date, const QList<CalendarEntry> &entries, QTreeWidgetItem *parent, const CalendarAgendaStyles &cas);
void addSpacer(QTreeWidgetItem *parent, int height);
void addSeparator(QTreeWidgetItem *parent, int top, int bottom);
void fillStyles(CalendarAgendaStyles &cas) const;
private slots:
void showContextMenu(const QPoint &pos);
};
class Calendar : public QWidget {
Q_OBJECT
@@ -292,7 +357,7 @@ public:
explicit Calendar(const QDate &dateInMonth, Qt::DayOfWeek firstDayOfWeek = Qt::Monday, Measures * const athleteMeasures = nullptr, QWidget *parent = nullptr);
void setDate(const QDate &dateInMonth, bool allowKeepMonth = false);
void fillEntries(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries);
void fillEntries(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries, bool isFiltered);
QDate firstOfCurrentMonth() const;
QDate firstVisibleDay() const;
QDate lastVisibleDay() const;
@@ -311,6 +376,8 @@ public slots:
void setFirstDayOfWeek(Qt::DayOfWeek firstDayOfWeek);
void setStartHour(int hour);
void setEndHour(int hour);
void setAgendaPastDays(int days);
void setAgendaFutureDays(int days);
void setSummaryDayVisible(bool visible);
void setSummaryWeekVisible(bool visible);
void setSummaryMonthVisible(bool visible);
@@ -335,23 +402,34 @@ signals:
void delRestday(const QDate &day);
private:
QToolBar *toolbar;
QAction *prevAction;
QAction *nextAction;
QAction *todayAction;
QAction *separator;
QAction *dayAction;
QAction *weekAction;
QAction *monthAction;
QPushButton *dateNavigator;
QAction *agendaAction;
QToolButton *dateNavigator;
QAction *dateNavigatorAction;
QMenu *dateMenu;
QLabel *seasonLabel;
QAction *seasonLabelAction;
QAction *filterSpacerAction;
QLabel *filterLabel;
QAction *filterLabelAction;
QStackedWidget *viewStack;
CalendarDayView *dayView;
CalendarWeekView *weekView;
CalendarMonthTable *monthView;
CalendarAgendaView *agendaView;
DateRange dateRange;
Qt::DayOfWeek firstDayOfWeek = Qt::Monday;
QDate lastNonAgendaDate;
void setNavButtonState();
void updateDateLabel();
void updateHeader();
private slots:
void populateDateMenu();

View File

@@ -351,12 +351,12 @@ CalendarDetailedDayDelegate::paint(QPainter *painter, const QStyleOptionViewItem
initStyleOption(&opt, index);
bool ok;
int pressedEntryIdx = index.data(Qt::UserRole + 2).toInt(&ok);
int pressedEntryIdx = index.data(PressedEntryRole).toInt(&ok);
if (! ok) {
pressedEntryIdx = -2;
}
CalendarDay calendarDay = index.data(Qt::UserRole + 1).value<CalendarDay>();
QList<CalendarEntryLayout> layout = index.data(Qt::UserRole + 3).value<QList<CalendarEntryLayout>>();
CalendarDay calendarDay = index.data(DayRole).value<CalendarDay>();
QList<CalendarEntryLayout> layout = index.data(LayoutRole).value<QList<CalendarEntryLayout>>();
entryTester.resize(index, layout.count());
// Background
@@ -494,7 +494,7 @@ CalendarDetailedDayDelegate::helpEvent
if (! event || ! view) {
return false;
}
CalendarDay day = index.data(Qt::UserRole + 1).value<CalendarDay>();
CalendarDay day = index.data(DayRole).value<CalendarDay>();
if (toolTipDayEntry(event->globalPos(), view, day, entryTester.hitTest(index, event->pos()))) {
return true;
}
@@ -622,7 +622,7 @@ CalendarHeadlineDelegate::helpEvent
if (! event || ! view) {
return false;
}
CalendarDay day = index.data(Qt::UserRole + 1).value<CalendarDay>();
CalendarDay day = index.data(DayRole).value<CalendarDay>();
if (toolTipHeadlineEntry(event->globalPos(), view, day, headlineTester.hitTest(index, event->pos()))) {
return true;
}
@@ -704,7 +704,7 @@ CalendarTimeScaleDelegate::paint
}
int blockY = -1;
QModelIndex scaleIndex = index.siblingAtColumn(0);
BlockIndicator blockIndicator = static_cast<BlockIndicator>(scaleIndex.data(Qt::UserRole + 1).toInt());
BlockIndicator blockIndicator = static_cast<BlockIndicator>(scaleIndex.data(BlockRole).toInt());
if (blockIndicator == BlockIndicator::AllBlock) {
blockY = option.rect.height();
} else if (blockIndicator == BlockIndicator::BlockBeforeNow) {
@@ -717,7 +717,7 @@ CalendarTimeScaleDelegate::paint
blockColor.setAlpha(180);
painter->fillRect(rect, blockColor);
}
int currentY = scaleIndex.data(Qt::UserRole).toInt();
int currentY = scaleIndex.data(CurrentYRole).toInt();
if (currentY - option.rect.y() > blockY) {
painter->save();
painter->setPen(textPen);
@@ -779,14 +779,14 @@ CalendarCompactDayDelegate::paint
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
CalendarDay calendarDay = index.data(Qt::UserRole + 1).value<CalendarDay>();
CalendarDay calendarDay = index.data(DayRole).value<CalendarDay>();
QColor bgColor;
QColor selColor = opt.palette.highlight().color();
QColor dayColor;
QColor entryColor;
bool ok;
int pressedEntryIdx = index.data(Qt::UserRole + 2).toInt(&ok);
int pressedEntryIdx = index.data(PressedEntryRole).toInt(&ok);
if (! ok) {
pressedEntryIdx = -2;
}
@@ -914,7 +914,7 @@ CalendarCompactDayDelegate::helpEvent
if (! event || ! view) {
return false;
}
CalendarDay day = index.data(Qt::UserRole + 1).value<CalendarDay>();
CalendarDay day = index.data(DayRole).value<CalendarDay>();
if ( (moreTester.hitTest(index, event->pos()) != -1 && toolTipMore(event->globalPos(), view, day))
|| (toolTipDayEntry(event->globalPos(), view, day, entryTester.hitTest(index, event->pos())))
|| (toolTipHeadlineEntry(event->globalPos(), view, day, headlineTester.hitTest(index, event->pos())))) {
@@ -941,7 +941,7 @@ CalendarSummaryDelegate::paint
bool hasToolTip = false;
const QColor bgColor = option.palette.color(QPalette::Active, QPalette::AlternateBase);
const QColor fgColor = option.palette.color(QPalette::Active, QPalette::Text);
const CalendarSummary summary = index.data(Qt::UserRole).value<CalendarSummary>();
const CalendarSummary summary = index.data(SummaryRole).value<CalendarSummary>();
QFont valueFont(painter->font());
valueFont.setWeight(QFont::Normal);
valueFont.setPointSize(valueFont.pointSize() * 0.95);
@@ -1018,7 +1018,7 @@ CalendarSummaryDelegate::helpEvent
return false;
}
CalendarSummary summary = index.data(Qt::UserRole).value<CalendarSummary>();
CalendarSummary summary = index.data(SummaryRole).value<CalendarSummary>();
QString tooltip = "<table>";
for (const std::pair<QString, QString> &p : summary.keyValues) {
tooltip += QString("<tr><td><b>%1</b></td><td>&nbsp;</td><td align='right'>%2</td></tr>").arg(p.first).arg(p.second);
@@ -1035,7 +1035,7 @@ CalendarSummaryDelegate::sizeHint
{
Q_UNUSED(option)
const CalendarSummary summary = index.data(Qt::UserRole).value<CalendarSummary>();
const CalendarSummary summary = index.data(SummaryRole).value<CalendarSummary>();
QFont font;
font.setWeight(QFont::DemiBold);
const QFontMetrics fontMetrics(font);
@@ -1045,6 +1045,228 @@ CalendarSummaryDelegate::sizeHint
}
//////////////////////////////////////////////////////////////////////////////
// AgendaMultiDelegate
AgendaMultiDelegate::AgendaMultiDelegate
(QObject *parent)
: QStyledItemDelegate(parent)
{
}
void
AgendaMultiDelegate::paint
(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
opt.state &= ~QStyle::State_Selected;
opt.state &= ~QStyle::State_HasFocus;
opt.state &= ~QStyle::State_MouseOver;
int type = index.data(TypeRole).toInt();
if (type == 0) {
opt.displayAlignment = Qt::AlignLeft | Qt::AlignTop;
const bool hovered = index.data(HoverFlagRole).toBool();
const QString hoverText = index.data(HoverTextRole).toString();
const QFont hoverFont = index.data(HoverFontRole).value<QFont>();
if (hovered) {
if (! hoverText.isEmpty()) {
opt.text = hoverText;
}
if (hoverFont != QFont()) {
opt.font = hoverFont;
}
}
const QWidget *widget = opt.widget;
QStyle *style = widget ? widget->style() : QApplication::style();
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);
} else if (type == 1) {
const QWidget *widget = opt.widget;
QStyle *style = widget ? widget->style() : QApplication::style();
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);
} else if (type == 2) {
const int horPadding = 4 * dpiXFactor;
const QWidget *widget = option.widget;
QStyle *style = widget ? widget->style() : QApplication::style();
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);
int topMargin = index.data(MarginTopRole).toInt();
QStyleOption sepOpt;
sepOpt.rect = QRect(opt.rect.x() + horPadding + 1, opt.rect.y() + topMargin + 1, opt.rect.width() - 2 * horPadding - 2, 1);
sepOpt.palette = opt.palette;
sepOpt.state = QStyle::State_Enabled;
style->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &sepOpt, painter, widget);
}
}
QSize
AgendaMultiDelegate::sizeHint
(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
int type = index.data(TypeRole).toInt();
if (type == 0) {
QString text = index.data(Qt::DisplayRole).toString();
QFont normalFont = index.data(Qt::FontRole).value<QFont>();
if (! normalFont.family().isEmpty()) {
opt.font = normalFont;
}
QFontMetrics normalFM(opt.font);
QSize normalSize = normalFM.size(Qt::TextSingleLine, text);
QString hoverText = index.data(HoverTextRole).toString();
QFont hoverFont = index.data(HoverFontRole).value<QFont>();
QSize hoverSize = QSize(0, 0);
if (! hoverText.isEmpty() || hoverFont != QFont()) {
QFontMetrics hoverFM(hoverFont != QFont() ? hoverFont : opt.font);
hoverSize = hoverFM.size(Qt::TextSingleLine, hoverText.isEmpty() ? text : hoverText);
}
QSize maxSize(std::max(normalSize.width(), hoverSize.width()), std::max(normalSize.height(), hoverSize.height()));
maxSize += QSize(8, 4);
return maxSize;
} else if (type == 1) {
return QSize(opt.rect.width(), index.data(MarginTopRole).toInt());
} else if (type == 2) {
int topMargin = index.data(MarginTopRole).toInt();
int bottomMargin = index.data(MarginBottomRole).toInt();
const QWidget *widget = opt.widget;
QStyle *style = widget ? widget->style() : QApplication::style();
int sepHeight = style->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, widget);
return QSize(option.rect.width(), sepHeight + topMargin + bottomMargin);
}
return QStyledItemDelegate::sizeHint(option, index);
}
//////////////////////////////////////////////////////////////////////////////
// CalendarSingleActivityDelegate
CalendarSingleActivityDelegate::CalendarSingleActivityDelegate
(QObject *parent)
{
Q_UNUSED(parent)
}
void
CalendarSingleActivityDelegate::paint
(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
CalendarEntry entry = index.data(EntryRole).value<CalendarEntry>();
bool hover = index.data(HoverFlagRole).toBool();
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
opt.state &= ~QStyle::State_Selected;
opt.state &= ~QStyle::State_HasFocus;
opt.state &= ~QStyle::State_MouseOver;
opt.text.clear();
const QWidget *widget = opt.widget;
QStyle *style = widget ? widget->style() : QApplication::style();
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);
QFontMetrics entryFM(painter->font());
const int horPadding = 4 * dpiXFactor;
const int lineSpacing = 2 * dpiYFactor;
const int lineHeight = entryFM.height();
const int radius = 4 * dpiXFactor;
const int iconSpacing = 4 * dpiXFactor;
const int iconWidth = 2 * lineHeight + lineSpacing;
const QSize pixmapSize(iconWidth, iconWidth);
QColor textColor = opt.palette.color(QPalette::Active, QPalette::Text);
QColor iconColor = entry.color;
if (hover) {
painter->save();
QRect hoverRect;
if (entry.type == ENTRY_TYPE_PLANNED_ACTIVITY) {
hoverRect = QRect(opt.rect.x() + horPadding + 1, opt.rect.y() + 1, opt.rect.width() - 2 * horPadding - 2, iconWidth - 2);
} else {
hoverRect = QRect(opt.rect.x() + horPadding + 1, opt.rect.y() + 1, opt.rect.width() - 2 * horPadding - 2, lineHeight - 2);
}
QColor bgColor = opt.palette.color(QPalette::Active, QPalette::Highlight);
bgColor.setAlphaF(0.2);
QColor bgBorder = bgColor;
bgBorder.setAlphaF(0.6);
iconColor = GCColor::invertColor(GCColor::blendedColor(bgColor, opt.palette.color(QPalette::Active, QPalette::AlternateBase)));
textColor = iconColor;
painter->setPen(bgBorder);
painter->setBrush(bgColor);
painter->drawRoundedRect(hoverRect, radius, radius);
painter->restore();
}
if (entry.type == ENTRY_TYPE_PLANNED_ACTIVITY) {
QPixmap pixmap = svgAsColoredPixmap(entry.iconFile, pixmapSize, iconSpacing, iconColor);
painter->drawPixmap(opt.rect.x() + horPadding, opt.rect.y(), pixmap);
} else {
QPixmap pixmap = svgOnBackground(entry.iconFile, QSize(lineHeight, lineHeight), iconSpacing, entry.color, radius);
painter->drawPixmap(opt.rect.x() + horPadding + lineHeight / 2, opt.rect.y(), pixmap);
}
painter->setPen(textColor);
QRect textRect(opt.rect.x() + iconWidth + iconSpacing + horPadding, opt.rect.y(), opt.rect.width() - iconWidth - iconSpacing - 2 * horPadding, lineHeight);
const QFontMetrics fm(painter->font());
QString primary(entry.primary);
QRect boundingRect = fm.boundingRect(primary);
if (boundingRect.width() > textRect.width()) {
primary = fm.elidedText(primary, Qt::ElideRight, textRect.width());
}
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, primary);
if (! entry.secondary.isEmpty()) {
textRect.translate(0, lineHeight + lineSpacing);
QString secondary = entry.secondary;
if (! entry.secondaryMetric.isEmpty()) {
secondary += " (" + entry.secondaryMetric + ")";
}
if (fm.boundingRect(secondary).width() > textRect.width()) {
secondary = fm.elidedText(secondary, Qt::ElideRight, textRect.width());
}
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, secondary);
}
painter->restore();
}
QSize
CalendarSingleActivityDelegate::sizeHint
(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QVariant data = index.data(EntryRole);
if (! data.isNull()) {
QFontMetrics entryFM(option.font);
const int lineSpacing = 2 * dpiYFactor;
const int lineHeight = entryFM.height();
CalendarEntry entry = data.value<CalendarEntry>();
if (entry.type == ENTRY_TYPE_PLANNED_ACTIVITY) {
return QSize(100, 2 * lineHeight + lineSpacing);
} else {
return QSize(100, lineHeight);
}
} else {
return QSize(0, 0);
}
}
//////////////////////////////////////////////////////////////////////////////
// Local helpers
@@ -1138,7 +1360,7 @@ static QRect
paintHeadline
(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index, HitTester &headlineTester, const QString &dateFormat, int pressedEntryIdx, int leftMargin, int rightMargin, int topMargin, int lineSpacing, int radius)
{
CalendarDay calendarDay = index.data(Qt::UserRole + 1).value<CalendarDay>();
CalendarDay calendarDay = index.data(CalendarHeadlineDelegate::DayRole).value<CalendarDay>();
QColor dayColor;
bool isToday = (calendarDay.date == QDate::currentDate());
QLocale locale;

View File

@@ -94,6 +94,12 @@ private:
class CalendarDetailedDayDelegate : public QStyledItemDelegate {
public:
enum Roles {
DayRole = Qt::UserRole + 1, // [CalendarDay] Calendar day
PressedEntryRole, // [int] Index of the currently pressed CalendarDay (see DayRole >> entries)
LayoutRole // [QList<CalendarEntryLayout>] Layout of the activities in one column
};
explicit CalendarDetailedDayDelegate(TimeScaleData const * const timeScaleData, QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
@@ -109,6 +115,10 @@ private:
class CalendarHeadlineDelegate : public QStyledItemDelegate {
public:
enum Roles {
DayRole = Qt::UserRole + 1 // [CalendarDay] Calendar day
};
explicit CalendarHeadlineDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
@@ -124,6 +134,11 @@ private:
class CalendarTimeScaleDelegate : public QStyledItemDelegate {
public:
enum Roles {
CurrentYRole = Qt::UserRole, // [int] Current Y value of the mousepointer
BlockRole // [int / BlockIndicator] How the timescale should be marked as blocked
};
explicit CalendarTimeScaleDelegate(TimeScaleData *timeScaleData, QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
@@ -138,6 +153,11 @@ private:
class CalendarCompactDayDelegate : public QStyledItemDelegate {
public:
enum Roles {
DayRole = Qt::UserRole + 1, // [CalendarDay] Calendar day
PressedEntryRole // [int] Index of the currently pressed CalendarDay (see DayRole >> entries)
};
explicit CalendarCompactDayDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
@@ -151,6 +171,10 @@ public:
class CalendarSummaryDelegate : public QStyledItemDelegate {
public:
enum Roles {
SummaryRole = Qt::UserRole // [CalendarSummary] Summary data
};
explicit CalendarSummaryDelegate(int horMargin = 4, QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
@@ -164,4 +188,41 @@ private:
const int lineSpacing = 2;
};
class AgendaMultiDelegate : public QStyledItemDelegate {
public:
enum Roles {
// Qt::DisplayRole // [QString] The default text to display
// Qt::FontRole // [QFont] The default font to use for text
HoverFlagRole = Qt::UserRole, // [bool] Hover flag. True if the item is hovered. If not set or invalid, treated as false
HoverTextRole, // [QString] Hover text to display when the hover flag is true. If empty, the normal DisplayRole text is used
HoverFontRole, // [QFont] Hover font to use when the hover flag is true. If default-constructed, the normal font is used
TypeRole, // [int] 0: Text
// 1: Spacer (Qt::UserRole + 4: Height)
// 2: Separator
MarginTopRole, // [int] Margin above (Type Spacer + Separator only)
MarginBottomRole // [int] Margin below (Type Separator only)
};
explicit AgendaMultiDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};
class CalendarSingleActivityDelegate : public QStyledItemDelegate {
public:
enum Roles {
EntryRole = Qt::UserRole, // [CalendarEntry] Entry to be displayed
HoverFlagRole, // [bool] Hover flag. True if the item is hovered. If not set or invalid, treated as false
EntryDateRole // [bool] Date of the CalendarEntry
};
explicit CalendarSingleActivityDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};
#endif

View File

@@ -1281,6 +1281,21 @@ newQFormLayout
}
extern QLayout*
centerWidgetInLayout
(QWidget *widget, bool margins)
{
QHBoxLayout *centerLayout = new QHBoxLayout();
if (! margins) {
centerLayout->setContentsMargins(0, 0, 0, 0);
}
centerLayout->addStretch(1);
centerLayout->addWidget(widget, 3);
centerLayout->addStretch(1);
return centerLayout;
}
extern QLayout*
centerLayout
(QLayout *layout, bool margins)

View File

@@ -43,6 +43,7 @@ extern QFont baseFont;
// layout and widget styling
extern void basicTreeWidgetStyle(QTreeWidget *tree, bool editable = true);
extern QFormLayout *newQFormLayout(QWidget *parent = nullptr);
extern QLayout *centerWidgetInLayout(QWidget *widget, bool margins = true);
extern QLayout *centerLayout(QLayout *layout, bool margins = true);
extern QWidget *centerLayoutInWidget(QLayout *layout, bool margins = true);

View File

@@ -0,0 +1,50 @@
/*
* 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 QT5COMPATIBILITY__H
#define QT5COMPATIBILITY__H
#include <QTreeWidget>
// Compatibility helper for Qt5
// exposes methods that turned public in Qt6 from protected in Qt5
#if QT_VERSION < 0x060000
class TreeWidget6 : public QTreeWidget
{
Q_OBJECT
public:
TreeWidget6(QWidget *parent = nullptr): QTreeWidget(parent) {
}
QModelIndex indexFromItem(const QTreeWidgetItem *item, int column = 0) const {
return QTreeWidget::indexFromItem(item, column);
}
QTreeWidgetItem* itemFromIndex(const QModelIndex &index) const {
return QTreeWidget::itemFromIndex(index);
}
};
#else
typedef QTreeWidget TreeWidget6;
#endif
#endif

View File

@@ -652,7 +652,7 @@ HEADERS += Gui/AboutDialog.h Gui/AddIntervalDialog.h Gui/AnalysisSidebar.h Gui/C
Gui/PerspectiveDialog.h Gui/SplashScreen.h Gui/StyledItemDelegates.h Gui/MetadataDialog.h Gui/ActionButtonBox.h \
Gui/MetricOverrideDialog.h Gui/RepeatScheduleWizard.h \
Gui/Calendar.h Gui/CalendarData.h Gui/CalendarItemDelegates.h \
Gui/IconManager.h
Gui/IconManager.h Gui/Qt5Compatibility.h
# metrics and models
HEADERS += Metrics/Banister.h Metrics/CPSolver.h Metrics/Estimator.h Metrics/ExtendedCriticalPower.h Metrics/HrZones.h Metrics/PaceZones.h \