Added a day view to PlanningCalendarWindow (#4704)

* Reusing the existing configuration
* Linked from days in month view to the more detailed view
* Fixed color setting after construction
This commit is contained in:
Joachim Kohlhammer
2025-09-16 03:08:35 +02:00
committed by GitHub
parent 069e2942df
commit f84f9a410e
15 changed files with 1991 additions and 254 deletions

View File

@@ -16,7 +16,6 @@
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "PlanningCalendarWindow.h"
#include <QComboBox>
@@ -109,13 +108,13 @@ PlanningCalendarWindow::PlanningCalendarWindow(Context *context)
updateActivities();
}
});
connect(calendar, &Calendar::moveActivity, [=](CalendarEntry activity, const QDate &srcDay, const QDate &destDay) {
connect(calendar, &Calendar::moveActivity, [=](CalendarEntry activity, const QDate &srcDay, const QDate &destDay, const QTime &destTime) {
Q_UNUSED(srcDay)
QApplication::setOverrideCursor(Qt::WaitCursor);
for (RideItem *rideItem : context->athlete->rideCache->rides()) {
if (rideItem != nullptr && rideItem->fileName == activity.reference) {
movePlannedActivity(rideItem, destDay);
movePlannedActivity(rideItem, destDay, destTime);
break;
}
}
@@ -147,9 +146,13 @@ PlanningCalendarWindow::PlanningCalendarWindow(Context *context)
}
QApplication::restoreOverrideCursor();
});
connect(calendar, &Calendar::dayChanged, this, &PlanningCalendarWindow::updateActivities);
connect(calendar, &Calendar::monthChanged, this, &PlanningCalendarWindow::updateActivities);
connect(calendar, &Calendar::viewChanged, this, &PlanningCalendarWindow::updateActivities);
configChanged(CONFIG_APPEARANCE);
QTimer::singleShot(0, this, [this]() {
configChanged(CONFIG_APPEARANCE);
});
}
@@ -276,7 +279,8 @@ PlanningCalendarWindow::setSummaryMetrics
void
PlanningCalendarWindow::configChanged(qint32 what)
PlanningCalendarWindow::configChanged
(qint32 what)
{
bool refreshActivities = false;
if ( (what & CONFIG_FIELDS)
@@ -299,9 +303,20 @@ PlanningCalendarWindow::configChanged(qint32 what)
QPalette palette;
palette.setColor(QPalette::Normal, QPalette::Window, activeBg);
palette.setColor(QPalette::Normal, QPalette::WindowText, activeText);
palette.setColor(QPalette::Normal, QPalette::Base, activeBg);
palette.setColor(QPalette::Normal, QPalette::AlternateBase, alternateBg);
palette.setColor(QPalette::Normal, QPalette::Text, activeText);
palette.setColor(QPalette::Normal, QPalette::Highlight, activeHl);
palette.setColor(QPalette::Normal, QPalette::HighlightedText, activeHlText);
palette.setColor(QPalette::Normal, QPalette::Button, activeBg);
palette.setColor(QPalette::Normal, QPalette::ButtonText, activeText);
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::AlternateBase, alternateBg);
palette.setColor(QPalette::Active, QPalette::Text, activeText);
palette.setColor(QPalette::Active, QPalette::Highlight, activeHl);
palette.setColor(QPalette::Active, QPalette::HighlightedText, activeHlText);
@@ -311,12 +326,23 @@ PlanningCalendarWindow::configChanged(qint32 what)
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::AlternateBase, 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);
palette.setColor(QPalette::Inactive, QPalette::Window, activeBg);
palette.setColor(QPalette::Inactive, QPalette::WindowText, activeText);
palette.setColor(QPalette::Inactive, QPalette::Base, activeBg);
palette.setColor(QPalette::Inactive, QPalette::AlternateBase, alternateBg);
palette.setColor(QPalette::Inactive, QPalette::Text, activeText);
palette.setColor(QPalette::Inactive, QPalette::Highlight, activeHl);
palette.setColor(QPalette::Inactive, QPalette::HighlightedText, activeHlText);
palette.setColor(QPalette::Inactive, QPalette::Button, activeBg);
palette.setColor(QPalette::Inactive, QPalette::ButtonText, activeText);
PaletteApplier::setPaletteRecursively(this, palette, true);
calendar->applyNavIcons();
@@ -505,27 +531,36 @@ PlanningCalendarWindow::getActivities
activity.hasTrainMode = rideItem->planned && sport == "Bike" && ! buildWorkoutFilter(rideItem).isEmpty();
activities[rideItem->dateTime.date()] << activity;
}
for (auto dayIt = activities.begin(); dayIt != activities.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;
}
});
}
return activities;
}
QList<CalendarSummary>
PlanningCalendarWindow::getWeeklySummaries
(const QDate &firstDay, const QDate &lastDay) const
PlanningCalendarWindow::getSummaries
(const QDate &firstDay, const QDate &lastDay, int timeBucketSize) const
{
QStringList symbols = getSummaryMetricsList();
QList<CalendarSummary> summaries;
int numWeeks = firstDay.daysTo(lastDay) / 7 + 1;
int numTimeBuckets = firstDay.daysTo(lastDay) / timeBucketSize + 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));
for (int timeBucket = 0; timeBucket < numTimeBuckets; ++timeBucket) {
QDate firstDayOfTimeBucket = firstDay.addDays(timeBucket * timeBucketSize);
QDate lastDayOfTimeBucket = firstDayOfTimeBucket.addDays(timeBucketSize - 1);
spec.setDateRange(DateRange(firstDayOfTimeBucket, lastDayOfTimeBucket));
CalendarSummary summary;
summary.keyValues.clear();
for (const QString &symbol : symbols) {
@@ -588,7 +623,7 @@ PlanningCalendarWindow::getPhasesEvents
}
}
QList<Season> tmpSeasons = context->athlete->seasons->seasons;
std::sort(tmpSeasons.begin(),tmpSeasons.end(),Season::LessThanForStarts);
std::sort(tmpSeasons.begin(), tmpSeasons.end(), Season::LessThanForStarts);
for (const Season &s : tmpSeasons) {
for (const SeasonEvent &event : s.events) {
if ( ( ( firstDay.isValid()
@@ -633,9 +668,15 @@ PlanningCalendarWindow::updateActivities
Season const *season = context->currentSeason();
if (!season) return; // avoid crash if no season selected
QList<CalendarSummary> summaries;
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());
if (calendar->currentView() == CalendarView::Day) {
QDate dayViewDate = calendar->selectedDate();
summaries = getSummaries(dayViewDate, dayViewDate, 1);
} else {
summaries = getSummaries(calendar->firstVisibleDay(), calendar->lastVisibleDay(), 7);
}
calendar->fillEntries(activities, summaries, phasesEvents);
}
@@ -644,9 +685,15 @@ void
PlanningCalendarWindow::updateActivitiesIfInRange
(RideItem *rideItem)
{
if ( rideItem->dateTime.date() >= calendar->firstVisibleDay()
&& rideItem->dateTime.date() <= calendar->lastVisibleDay()) {
updateActivities();
if (calendar->currentView() == CalendarView::Day) {
if (rideItem->dateTime.date() == calendar->selectedDate()) {
updateActivities();
}
} else {
if ( rideItem->dateTime.date() >= calendar->firstVisibleDay()
&& rideItem->dateTime.date() <= calendar->lastVisibleDay()) {
updateActivities();
}
}
}
@@ -667,12 +714,12 @@ PlanningCalendarWindow::updateSeason
bool
PlanningCalendarWindow::movePlannedActivity
(RideItem *rideItem, const QDate &destDay, bool force)
(RideItem *rideItem, const QDate &destDay, const QTime &destTime)
{
bool ret = false;
RideFile *rideFile = rideItem->ride();
QDateTime rideDateTime(destDay, rideFile->startTime().time());
QDateTime rideDateTime(destDay, destTime);
rideFile->setStartTime(rideDateTime);
QString basename = rideDateTime.toString("yyyy_MM_dd_HH_mm_ss");
@@ -683,8 +730,7 @@ PlanningCalendarWindow::movePlannedActivity
filename = context->athlete->home->activities().canonicalPath() + "/" + basename + ".json";
}
QFile out(filename);
if ( ( force
|| (! force && ! out.exists()))
if ( ! out.exists()
&& RideFileFactory::instance().writeRideFile(context, rideFile, out, "json")) {
context->tab->setNoSwitch(true);
context->athlete->rideCache->removeRide(rideItem->fileName);

View File

@@ -85,14 +85,14 @@ class PlanningCalendarWindow : public GcChartWindow
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;
QList<CalendarSummary> getSummaries(const QDate &firstDay, const QDate &lastDay, int timeBucketSize = 7) const;
QHash<QDate, QList<CalendarEntry>> getPhasesEvents(const Season &season, const QDate &firstDay, const QDate &lastDay) const;
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);
bool movePlannedActivity(RideItem *rideItem, const QDate &destDay, const QTime &destTime = QTime());
};
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,9 @@
#define CALENDAR_H
#include <QWidget>
#include <QStackedWidget>
#include <QTableWidget>
#include <QCalendarWidget>
#include <QPushButton>
#include <QLabel>
#include <QComboBox>
@@ -29,10 +31,92 @@
#include <QTimer>
#include <QApplication>
#include "CalendarItemDelegates.h"
#include "CalendarData.h"
#include "TimeUtils.h"
class CalendarOverview : public QCalendarWidget {
Q_OBJECT
public:
explicit CalendarOverview(QWidget *parent = nullptr);
QDate firstVisibleDay() const;
QDate lastVisibleDay() const;
void limitDateRange(const DateRange &dr);
void fillEntries(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries);
protected:
#if QT_VERSION < 0x060000
void paintCell(QPainter *painter, const QRect &rect, const QDate &date) const override;
#else
void paintCell(QPainter *painter, const QRect &rect, QDate date) const override;
#endif
private:
QHash<QDate, QList<CalendarEntry>> activityEntries;
QHash<QDate, QList<CalendarEntry>> headlineEntries;
void drawEntries(QPainter *painter, const QList<CalendarEntry> &entries, QPolygon polygon, int maxEntries, int shiftX) const;
};
class CalendarDayTable : public QTableWidget {
Q_OBJECT
public:
explicit CalendarDayTable(const QDate &date, QWidget *parent = nullptr);
bool setDay(const QDate &date);
QDate selectedDate() const;
bool isInDateRange(const QDate &date) const;
void fillEntries(const QList<CalendarEntry> &activityEntries, const CalendarSummary &summary, const QList<CalendarEntry> &headlineEntries);
void limitDateRange(const DateRange &dr);
signals:
void dayClicked(const CalendarDay &day, const QTime &time);
void dayRightClicked(const CalendarDay &day, const QTime &time);
void entryClicked(const CalendarDay &day, int entryIdx);
void entryRightClicked(const CalendarDay &day, int entryIdx);
void entryMoved(const CalendarEntry &activity, const QDate &srcDay, const QDate &destDay, const QTime &destTime);
void dayChanged(const QDate &date);
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);
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:
QDate date;
DateRange dr;
QTimer dragTimer;
QPoint pressedPos;
QModelIndex pressedIndex;
bool isDraggable = false;
TimeScaleData timeScaleData;
void setDropIndicator(int y, BlockIndicator block);
};
class CalendarMonthTable : public QTableWidget {
Q_OBJECT
@@ -42,11 +126,6 @@ public:
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;
@@ -57,6 +136,7 @@ public:
void setFirstDayOfWeek(Qt::DayOfWeek firstDayOfWeek);
signals:
void daySelected(const CalendarDay &day);
void dayClicked(const CalendarDay &day);
void dayDblClicked(const CalendarDay &day);
void moreClicked(const CalendarDay &day);
@@ -65,7 +145,7 @@ signals:
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 entryMoved(const CalendarEntry &activity, const QDate &srcDay, const QDate &destDay, const QTime &destTime);
void summaryClicked(const QModelIndex &index);
void summaryDblClicked(const QModelIndex &index);
void summaryRightClicked(const QModelIndex &index);
@@ -96,12 +176,11 @@ 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
QDate currentDate; // currently selected date
DateRange dr;
QTimer dragTimer;
@@ -111,6 +190,12 @@ private:
};
enum class CalendarView {
Day = 0,
Month = 1
};
class Calendar : public QWidget {
Q_OBJECT
@@ -118,22 +203,34 @@ 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 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);
QDate firstOfCurrentMonth() const;
QDate firstVisibleDay() const;
QDate lastVisibleDay() const;
QDate selectedDate() const;
CalendarView currentView() const;
bool addMonths(int months);
bool addYears(int years);
QDate fitDate(const QDate &date, bool preferToday = true) const;
bool canAddMonths(int months) const;
bool canAddYears(int years) const;
bool isInDateRange(const QDate &date) const;
public slots:
void setView(CalendarView view);
void activateDateRange(const DateRange &dr, bool allowKeepMonth = false);
void setFirstDayOfWeek(Qt::DayOfWeek firstDayOfWeek);
void setSummaryMonthVisible(bool visible);
void applyNavIcons();
signals:
void viewChanged(CalendarView newView, CalendarView oldView);
void daySelected(const QDate &date);
void dayClicked(const QDate &date);
void summaryClicked(const QDate &date);
void dayChanged(const QDate &date);
void monthChanged(const QDate &month, const QDate &firstVisible, const QDate &lastVisible);
void dateRangeActivated(const QString &name);
void showInTrainMode(const CalendarEntry &activity);
@@ -141,22 +238,29 @@ signals:
void addActivity(bool plan, const QDate &day, const QTime &time);
void delActivity(const CalendarEntry &activity);
void repeatSchedule(const QDate &day);
void moveActivity(const CalendarEntry &activity, const QDate &srcDay, const QDate &destDay);
void moveActivity(const CalendarEntry &activity, const QDate &srcDay, const QDate &destDay, const QTime &destTime);
void insertRestday(const QDate &day);
void delRestday(const QDate &day);
private:
QPushButton *prevYButton;
QPushButton *prevMButton;
QPushButton *nextMButton;
QPushButton *nextYButton;
QPushButton *todayButton;
QAction *prevYAction;
QAction *prevMAction;
QAction *nextMAction;
QAction *nextYAction;
QAction *todayAction;
QAction *dayAction;
QAction *monthAction;
QLabel *dateLabel;
QStackedWidget *tableStack;
QWidget *dayCalendar;
CalendarOverview *dayDateSelector;
QLabel *dayPhaseLabel;
QLabel *dayEventLabel;
CalendarDayTable *dayTable;
CalendarMonthTable *monthTable;
QHash<QDate, QList<CalendarEntry>> headlineEntries;
DateRange dateRange;
void setNavButtonState();
bool isDark() const;
};
#endif

112
src/Gui/CalendarData.cpp Normal file
View File

@@ -0,0 +1,112 @@
/*
* 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 <CalendarData.h>
CalendarEntryLayouter::CalendarEntryLayouter
()
{
}
QList<CalendarEntryLayout>
CalendarEntryLayouter::layout
(const QList<CalendarEntry> &entries)
{
QList<QList<int>> clusters = groupOverlapping(entries);
QList<CalendarEntryLayout> layouted;
for (const auto &cluster : clusters) {
layouted += assignColumns(cluster, entries);
}
return layouted;
}
QList<QList<int>>
CalendarEntryLayouter::groupOverlapping
(const QList<CalendarEntry> &entries)
{
QList<QList<int>> clusters;
QList<int> currentCluster;
QTime clusterEnd;
for (int entryIdx = 0; entryIdx < entries.count(); ++entryIdx) {
const CalendarEntry &entry = entries[entryIdx];
QTime end = entry.start.addSecs(entry.durationSecs);
if (currentCluster.isEmpty()) {
currentCluster.append(entryIdx);
clusterEnd = end;
} else {
if (entry.start < clusterEnd) {
currentCluster.append(entryIdx);
if (end > clusterEnd) {
clusterEnd = end;
}
} else {
clusters.append(currentCluster);
currentCluster = { entryIdx };
clusterEnd = end;
}
}
}
if (! currentCluster.isEmpty()) {
clusters.append(currentCluster);
}
return clusters;
}
QList<CalendarEntryLayout>
CalendarEntryLayouter::assignColumns
(const QList<int> &cluster, const QList<CalendarEntry> &entries)
{
QList<QTime> columns;
QMap<int, int> entryColumns;
for (int entryIdx : cluster) {
const CalendarEntry &entry = entries[entryIdx];
QTime start = entry.start;
QTime end = start.addSecs(entry.durationSecs);
bool found = false;
for (int colIdx = 0; colIdx < columns.count(); ++colIdx) {
if (columns[colIdx] <= start) {
columns[colIdx] = end;
entryColumns[entryIdx] = colIdx;
found = true;
break;
}
}
if (! found) {
columns << end;
entryColumns[entryIdx] = columns.count() - 1;
}
}
QList<CalendarEntryLayout> result;
for (int entryIdx : cluster) {
result << CalendarEntryLayout({
entryIdx,
entryColumns[entryIdx],
static_cast<int>(columns.count())
});
}
return result;
}

View File

@@ -1,10 +1,32 @@
/*
* 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 CALENDARDATA_H
#define CALENDARDATA_H
#include <QMetaType>
#include <QDate>
#include <QTime>
#include <QList>
#include <QHash>
#include <QMap>
#include <QColor>
#include <utility>
#define ENTRY_TYPE_ACTIVITY 0
#define ENTRY_TYPE_PLANNED_ACTIVITY 1
@@ -13,17 +35,6 @@
#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;
@@ -40,6 +51,12 @@ struct CalendarEntry {
QDate spanEnd = QDate();
};
struct CalendarEntryLayout {
int entryIdx;
int columnIndex;
int columnCount;
};
struct CalendarDay {
QDate date;
bool isDimmed;
@@ -52,7 +69,20 @@ struct CalendarSummary {
};
Q_DECLARE_METATYPE(CalendarEntry)
Q_DECLARE_METATYPE(CalendarEntryLayout)
Q_DECLARE_METATYPE(CalendarDay)
Q_DECLARE_METATYPE(CalendarSummary)
class CalendarEntryLayouter {
public:
explicit CalendarEntryLayouter();
QList<CalendarEntryLayout> layout(const QList<CalendarEntry> &entries);
private:
QList<QList<int>> groupOverlapping(const QList<CalendarEntry> &entries);
QList<CalendarEntryLayout> assignColumns(const QList<int> &cluster, const QList<CalendarEntry> &entries);
};
#endif

View File

@@ -1,3 +1,21 @@
/*
* 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 "CalendarItemDelegates.h"
#include <QDate>
@@ -13,6 +31,561 @@
#include "Colors.h"
//////////////////////////////////////////////////////////////////////////////
// TimeScaleData
TimeScaleData::TimeScaleData
()
{
}
void
TimeScaleData::setFirstMinute
(int minute)
{
_firstMinute = minute;
}
int
TimeScaleData::firstMinute
() const
{
return _firstMinute;
}
void
TimeScaleData::setLastMinute
(int minute)
{
_lastMinute = minute;
}
int
TimeScaleData::lastMinute
() const
{
return _lastMinute;
}
void
TimeScaleData::setStepSize
(int step)
{
_stepSize = step;
}
int
TimeScaleData::stepSize
() const
{
return _stepSize;
}
double
TimeScaleData::pixelsPerMinute
(int availableHeight) const
{
int totalMinutes = _lastMinute - _firstMinute;
if (totalMinutes <= 0) {
return 0.0;
}
return static_cast<double>(availableHeight) / totalMinutes;
}
double
TimeScaleData::pixelsPerMinute
(const QRect& rect) const
{
return pixelsPerMinute(rect.height());
}
int
TimeScaleData::minuteToY
(int minute, int rectTop, int rectHeight) const
{
if (minute < _firstMinute) {
return rectTop;
}
if (minute > _lastMinute) {
return rectTop + rectHeight;
}
int totalMinutes = _lastMinute - _firstMinute;
double pixelsPerMinute = static_cast<double>(rectHeight) / totalMinutes;
return std::min(rectHeight, rectTop + static_cast<int>((minute - _firstMinute) * pixelsPerMinute));
}
int
TimeScaleData::minuteToY
(int minute, const QRect &rect) const
{
return minuteToY(minute, rect.top(), rect.height());
}
int
TimeScaleData::minuteToY
(const QTime &time, const QRect& rect) const
{
return minuteToY(time.hour() * 60 + time.minute(), rect.top(), rect.height());
}
QTime
TimeScaleData::timeFromY
(int y, const QRect &rect, int snap) const
{
int s = std::max(1, snap);
int minute = trunc((_firstMinute + y / pixelsPerMinute(rect)) / s) * s;
return QTime(0, 0).addSecs(60 * minute);
}
//////////////////////////////////////////////////////////////////////////////
// CalendarDayViewDayDelegate
ColumnDelegatingItemDelegate::ColumnDelegatingItemDelegate
(QList<QStyledItemDelegate*> delegates, QObject *parent)
: QStyledItemDelegate(parent), delegates(delegates)
{
}
QStyledItemDelegate*
ColumnDelegatingItemDelegate::getDelegate
(int idx) const
{
return delegates.value(idx, nullptr);
}
QWidget*
ColumnDelegatingItemDelegate::createEditor
(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyledItemDelegate *delegate = delegates.value(index.column(), nullptr);
if (delegate != nullptr) {
return delegate->createEditor(parent, option, index);
} else {
return QStyledItemDelegate::createEditor(parent, option, index);
}
}
void
ColumnDelegatingItemDelegate::paint
(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyledItemDelegate *delegate = delegates.value(index.column(), nullptr);
if (delegate != nullptr) {
delegate->paint(painter, option, index);
} else {
QStyledItemDelegate::paint(painter, option, index);
}
}
bool
ColumnDelegatingItemDelegate::helpEvent
(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index)
{
QStyledItemDelegate *delegate = delegates.value(index.column(), nullptr);
if (delegate != nullptr) {
return delegate->helpEvent(event, view, option, index);
} else {
return QStyledItemDelegate::helpEvent(event, view, option, index);
}
}
void
ColumnDelegatingItemDelegate::setEditorData
(QWidget *editor, const QModelIndex &index) const
{
QStyledItemDelegate *delegate = delegates.value(index.column(), nullptr);
if (delegate != nullptr) {
return delegate->setEditorData(editor, index);
} else {
return QStyledItemDelegate::setEditorData(editor, index);
}
}
void
ColumnDelegatingItemDelegate::setModelData
(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
QStyledItemDelegate *delegate = delegates.value(index.column(), nullptr);
if (delegate != nullptr) {
delegate->setModelData(editor, model, index);
} else {
QStyledItemDelegate::setModelData(editor, model, index);
}
}
QSize
ColumnDelegatingItemDelegate::sizeHint
(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyledItemDelegate *delegate = delegates.value(index.column(), nullptr);
if (delegate != nullptr) {
return delegate->sizeHint(option, index);
} else {
return QStyledItemDelegate::sizeHint(option, index);
}
}
void
ColumnDelegatingItemDelegate::updateEditorGeometry
(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyledItemDelegate *delegate = delegates.value(index.column(), nullptr);
if (delegate != nullptr) {
delegate->updateEditorGeometry(editor, option, index);
} else {
QStyledItemDelegate::updateEditorGeometry(editor, option, index);
}
}
//////////////////////////////////////////////////////////////////////////////
// CalendarDayViewDayDelegate
CalendarDayViewDayDelegate::CalendarDayViewDayDelegate
(TimeScaleData const * const timeScaleData, QObject *parent)
: QStyledItemDelegate(parent), timeScaleData(timeScaleData)
{
}
void
CalendarDayViewDayDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (index.row() != 0) {
QStyledItemDelegate::paint(painter, option, index);
return;
}
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
bool ok;
int pressedEntryIdx = index.data(Qt::UserRole + 2).toInt(&ok);
if (! ok) {
pressedEntryIdx = -2;
}
QDate date = index.data(Qt::UserRole).toDate();
CalendarDay calendarDay = index.data(Qt::UserRole + 1).value<CalendarDay>();
QList<CalendarEntryLayout> layout = index.data(Qt::UserRole + 3).value<QList<CalendarEntryLayout>>();
#if QT_VERSION < 0x060000
entryRects[index].clear();
for (int i = 0; i < layout.count(); ++i) {
entryRects[index] << QRect();
}
#else
entryRects[index].resize(layout.count());
#endif
bool isToday = (date == QDate::currentDate());
// Horizontal Lines
painter->save();
QPen linePen(Qt::lightGray, 1, Qt::DotLine);
painter->setPen(linePen);
for (int minute = timeScaleData->firstMinute(); minute <= timeScaleData->lastMinute(); minute += timeScaleData->stepSize()) {
int y = timeScaleData->minuteToY(minute, option.rect);
painter->drawLine(option.rect.left(), y, option.rect.right(), y);
}
painter->restore();
// Activities
const int columnSpacing = 10 * dpiXFactor;
const int lineSpacing = 2 * dpiYFactor;
const int iconSpacing = 2 * dpiXFactor;
const int radius = 4 * dpiXFactor;
const int horPadding = 4 * dpiXFactor;
QFont entryFont = painter->font();
entryFont.setPointSize(entryFont.pointSize() * 0.95);
QFontMetrics entryFM(entryFont);
int lineHeight = entryFM.height();
painter->setFont(entryFont);
int rectLeft = option.rect.left() + columnSpacing;
int rectWidth = option.rect.width() - 2 * columnSpacing;
for (const CalendarEntryLayout &l : layout) {
if (calendarDay.entries.count() < l.entryIdx) {
continue;
}
bool pressed = (l.entryIdx == pressedEntryIdx);
const CalendarEntry &entry = calendarDay.entries[l.entryIdx];
int startMinute = entry.start.hour() * 60 + entry.start.minute();
int endMinute = startMinute + entry.durationSecs / 60;
int columnWidth = (rectWidth - (l.columnCount - 1) * columnSpacing) / std::max(1, l.columnCount);
int left = rectLeft + l.columnIndex * (columnWidth + columnSpacing);
int top = timeScaleData->minuteToY(startMinute, option.rect);
int bottom = timeScaleData->minuteToY(endMinute, option.rect);
int height = std::max(1, bottom - top);
int numLines = (height + lineSpacing) / (lineHeight + lineSpacing);
painter->save();
QRect entryRect(left, top, columnWidth, height);
entryRects[index][l.entryIdx] = entryRect;
QColor entryBG = option.palette.base().color();
QColor entryFrame = entry.color;
if (pressed) {
entryBG = option.palette.highlight().color();
entryFrame = entryBG;
} else if (entry.type == ENTRY_TYPE_ACTIVITY) {
QColor overlayBG = entry.color;
overlayBG = entry.color;
if (GCColor::isPaletteDark(option.palette)) {
overlayBG.setAlpha(150);
} else {
overlayBG.setAlpha(60);
}
entryBG = GCColor::blendedColor(overlayBG, entryBG);
entryFrame = entryBG;
}
painter->save();
painter->setPen(entryFrame);
painter->setBrush(entryBG);
painter->drawRoundedRect(entryRect, radius, radius);
painter->restore();
int iconWidth = 0;
if (height >= lineHeight && columnWidth >= lineHeight) {
QColor pixmapColor(entry.color);
int headlineOffset = 0;
if (height >= 2 * lineHeight + lineSpacing && columnWidth >= 2 * lineHeight + lineSpacing) {
iconWidth = 2 * lineHeight + lineSpacing;
} else {
iconWidth = lineHeight;
}
QSize pixmapSize(iconWidth, iconWidth);
QPixmap pixmap;
if (entry.type == ENTRY_TYPE_PLANNED_ACTIVITY) {
if (pressed) {
pixmapColor = GCColor::invertColor(entryBG);
}
pixmap = svgAsColoredPixmap(entry.iconFile, pixmapSize, lineSpacing, pixmapColor);
} else {
pixmap = svgOnBackground(entry.iconFile, pixmapSize, lineSpacing, pixmapColor, radius);
}
painter->drawPixmap(left, top, pixmap);
}
if (numLines > 0) {
painter->save();
QRect textRect(left + iconWidth + horPadding, top, columnWidth - iconWidth - 2 * horPadding, lineHeight);
painter->setPen(GCColor::invertColor(entryBG));
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop, entry.primary);
--numLines;
if (numLines > 0) {
textRect.translate(0, lineHeight + lineSpacing);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop, entry.secondary + " (" + entry.secondaryMetric + ")");
}
painter->restore();
}
painter->restore();
}
painter->restore();
}
bool
CalendarDayViewDayDelegate::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 = hitTestEntry(index, event->pos());
if (entryIdx >= 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;
}
return QStyledItemDelegate::helpEvent(event, view, option, index);
}
int
CalendarDayViewDayDelegate::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;
}
//////////////////////////////////////////////////////////////////////////////
// CalendarTimeScaleDelegate
CalendarTimeScaleDelegate::CalendarTimeScaleDelegate
(TimeScaleData *timeScaleData, QObject *parent)
: QStyledItemDelegate(parent), timeScaleData(timeScaleData)
{
}
void
CalendarTimeScaleDelegate::paint
(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (index.row() != 0) {
QStyledItemDelegate::paint(painter, option, index);
return;
}
const int totalRangeMinutes = timeScaleData->lastMinute() - timeScaleData->firstMinute();
const int cellHeight = option.rect.height();
const double pixelsPerMinute = static_cast<double>(cellHeight) / totalRangeMinutes;
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
QPen linePen(Qt::lightGray, 1, Qt::DotLine);
QPen textPen(Qt::darkGray);
painter->setPen(textPen);
QFont font = painter->font();
font.setPointSizeF(font.pointSizeF() * 0.9);
painter->setFont(font);
QFontMetrics fm(font);
int labelHeight = fm.height();
int minPixelSpacing = labelHeight + 4;
int calculatedMinInterval = std::ceil(minPixelSpacing / pixelsPerMinute);
int effectiveInterval = selectNiceInterval(calculatedMinInterval);
timeScaleData->setStepSize(effectiveInterval);
int previousYBottom = -labelHeight;
for (int minute = timeScaleData->firstMinute(); minute <= timeScaleData->lastMinute(); minute += timeScaleData->stepSize()) {
int y = timeScaleData->minuteToY(minute, option.rect);
QString timeLabel = QTime(0, 0).addSecs(minute * 60).toString("hh:mm");
QRect textRect;
if (minute == timeScaleData->firstMinute()) {
textRect = QRect(option.rect.left() + 4, y, option.rect.width() - 8, labelHeight);
} else if (minute + effectiveInterval > timeScaleData->lastMinute()) {
int bottomY = option.rect.bottom();
textRect = QRect(option.rect.left() + 4, bottomY - labelHeight, option.rect.width() - 8, labelHeight);
y = bottomY;
} else {
textRect = QRect(option.rect.left() + 4, y - labelHeight / 2, option.rect.width() - 8, labelHeight);
}
if (textRect.top() < previousYBottom + 2) {
continue;
}
painter->setPen(linePen);
painter->drawLine(textRect.right(), y, option.rect.right(), y);
painter->setPen(textPen);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, timeLabel);
previousYBottom = textRect.bottom();
}
int blockY = -1;
QModelIndex scaleIndex = index.sibling(0, 0);
BlockIndicator blockIndicator = static_cast<BlockIndicator>(scaleIndex.data(Qt::UserRole + 1).toInt());
if (blockIndicator == BlockIndicator::AllBlock) {
blockY = option.rect.height();
} else if (blockIndicator == BlockIndicator::BlockBeforeNow) {
blockY = timeScaleData->minuteToY(QTime::currentTime(), option.rect);
}
if (blockY >= 0) {
QRect rect = option.rect;
rect.setHeight(blockY);
QColor blockColor = option.palette.color(QPalette::Disabled, QPalette::Base);
blockColor.setAlpha(180);
painter->fillRect(rect, blockColor);
}
int currentY = scaleIndex.data(Qt::UserRole).toInt();
if (currentY > blockY) {
painter->save();
painter->setPen(textPen);
painter->drawLine(option.rect.left(), currentY, option.rect.width(), currentY);
painter->restore();
}
painter->restore();
}
QSize
CalendarTimeScaleDelegate::sizeHint
(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
return QStyledItemDelegate::sizeHint(option, index);
}
int
CalendarTimeScaleDelegate::selectNiceInterval
(int minInterval) const
{
const QList<int> allowed = { 60, 120, 180, 240, 360, 720 };
for (int interval : allowed) {
if (interval >= minInterval) {
return interval;
}
}
return 1440;
}
//////////////////////////////////////////////////////////////////////////////
// CalendarDayDelegate
@@ -61,13 +634,13 @@ CalendarDayDelegate::paint
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;
const int leftMargin = 4 * dpiXFactor;
const int rightMargin = 4 * dpiXFactor;
const int topMargin = 2 * dpiYFactor;
const int bottomMargin = 4 * dpiYFactor;
const int lineSpacing = 2 * dpiYFactor;
const int iconSpacing = 2 * dpiXFactor;
const int radius = 4 * dpiXFactor;
// Day number / Headline
painter->save();
@@ -336,8 +909,8 @@ CalendarDayDelegate::hitTestMore
// CalendarSummaryDelegate
CalendarSummaryDelegate::CalendarSummaryDelegate
(QObject *parent)
: QStyledItemDelegate(parent)
(int horMargin, QObject *parent)
: QStyledItemDelegate(parent), horMargin(horMargin)
{
}
@@ -357,9 +930,6 @@ CalendarSummaryDelegate::paint
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();
@@ -438,3 +1008,17 @@ CalendarSummaryDelegate::helpEvent
QToolTip::showText(event->globalPos(), tooltip, view);
return true;
}
QSize
CalendarSummaryDelegate::sizeHint
(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
const CalendarSummary summary = index.data(Qt::UserRole).value<CalendarSummary>();
QFont font;
font.setWeight(QFont::DemiBold);
const QFontMetrics fontMetrics(font);
const int lineHeight = fontMetrics.height();
return QSize(10, vertMargin + (lineHeight + lineSpacing) * summary.keyValues.count() - lineSpacing);
}

View File

@@ -1,9 +1,111 @@
/*
* 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 CALENDARITEMDELEGATES_H
#define CALENDARITEMDELEGATES_H
#include <QStyledItemDelegate>
enum class BlockIndicator {
NoBlock = 0,
AllBlock = 1,
BlockBeforeNow = 2
};
class TimeScaleData {
public:
explicit TimeScaleData();
void setFirstMinute(int minute);
int firstMinute() const;
void setLastMinute(int minute);
int lastMinute() const;
void setStepSize(int step);
int stepSize() const;
double pixelsPerMinute(int availableHeight) const;
double pixelsPerMinute(const QRect& rect) const;
int minuteToY(int minute, int rectTop, int rectHeight) const;
int minuteToY(int minute, const QRect& rect) const;
int minuteToY(const QTime &time, const QRect& rect) const;
QTime timeFromY(int y, const QRect &rect, int snap = 15) const;
private:
int _firstMinute = 8 * 60;
int _lastMinute = 21 * 60;
int _stepSize = 60;
};
// Workaround for Qt5 not having QAbstractItemView::itemDelegateForIndex (Qt6 only)
class ColumnDelegatingItemDelegate : public QStyledItemDelegate {
public:
explicit ColumnDelegatingItemDelegate(QList<QStyledItemDelegate*> delegates, QObject *parent = nullptr);
QStyledItemDelegate *getDelegate(int idx) const;
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
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;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
private:
QList<QStyledItemDelegate*> delegates;
};
class CalendarDayViewDayDelegate : public QStyledItemDelegate {
public:
explicit CalendarDayViewDayDelegate(TimeScaleData const * const timeScaleData, 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;
private:
TimeScaleData const * const timeScaleData;
mutable QHash<QModelIndex, QList<QRect>> entryRects;
};
class CalendarTimeScaleDelegate : public QStyledItemDelegate {
public:
explicit CalendarTimeScaleDelegate(TimeScaleData *timeScaleData, 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;
private:
TimeScaleData *timeScaleData;
int selectNiceInterval(int minInterval) const;
};
class CalendarDayDelegate : public QStyledItemDelegate {
public:
explicit CalendarDayDelegate(QObject *parent = nullptr);
@@ -24,13 +126,17 @@ private:
class CalendarSummaryDelegate : public QStyledItemDelegate {
public:
explicit CalendarSummaryDelegate(QObject *parent = nullptr);
explicit CalendarSummaryDelegate(int horMargin = 4, 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;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
private:
mutable QHash<QModelIndex, bool> indexHasToolTip;
const int horMargin;
const int vertMargin = 4;
const int lineSpacing = 2;
};
#endif

View File

@@ -24,6 +24,7 @@
#include <QByteArray>
#include <QDir>
#include <QSvgRenderer>
#include <cmath>
#include "Settings.h"
#ifdef Q_OS_WIN
@@ -417,6 +418,25 @@ QColor GCColor::selectedColor(QColor bgColor)
return bg_select;
}
QColor GCColor::blendedColor(const QColor &fg, const QColor &bg)
{
double alpha = fg.alphaF();
int r = static_cast<int>(std::round(fg.red() * alpha + bg.red() * (1.0 - alpha)));
int g = static_cast<int>(std::round(fg.green() * alpha + bg.green() * (1.0 - alpha)));
int b = static_cast<int>(std::round(fg.blue() * alpha + bg.blue() * (1.0 - alpha)));
return QColor(r, g, b);
}
bool GCColor::isDark(const QColor &color)
{
return color.lightness() < 127;
}
bool GCColor::isPaletteDark(const QPalette &palette)
{
return isDark(palette.color(QPalette::Active, QPalette::Base));
}
const Colors * GCColor::colorSet()
{
return ColorList;

View File

@@ -140,6 +140,9 @@ class GCColor : public QObject
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 blendedColor(const QColor &fg, const QColor &bg); // return a color with alpha blended on a background
static bool isDark(const QColor &color);
static bool isPaletteDark(const QPalette &palette);
static QColor htmlCode(QColor x) { return x.name(); } // return the alternate background
static Themes &themes();
static void applyTheme(int index);

View File

@@ -72,7 +72,7 @@ ManualActivityWizard::ManualActivityWizard
if (plan) {
setWindowTitle(tr("Add a Planned Activity"));
} else {
setWindowTitle(tr("Create a Completed Activity"));
setWindowTitle(tr("Add a Completed Activity"));
}
setMinimumSize(800 * dpiXFactor, 650 * dpiYFactor);
setModal(true);

View File

@@ -764,7 +764,7 @@ SOURCES += Gui/AboutDialog.cpp Gui/AddIntervalDialog.cpp Gui/AnalysisSidebar.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/RepeatScheduleWizard.cpp \
Gui/Calendar.cpp Gui/CalendarItemDelegates.cpp \
Gui/Calendar.cpp Gui/CalendarData.cpp Gui/CalendarItemDelegates.cpp \
Gui/IconManager.cpp
## Models and Metrics

View File

@@ -0,0 +1,6 @@
QT += testlib core
SOURCES = testCalendarData.cpp
GC_OBJS = CalendarData
include(../../unittests.pri)

View File

@@ -0,0 +1,43 @@
#include "Gui/CalendarData.h"
#include <QTest>
class TestCalendarData: public QObject
{
Q_OBJECT
private slots:
void layoutCalendarEntry() {
#if QT_VERSION < 0x060000
QSKIP("Skipping test with Qt5");
#else
QList<CalendarEntry> entries = {
{ "", "", "", "", Qt::red, "", QTime(9, 0), 3600 },
{ "", "", "", "", Qt::red, "", QTime(9, 30), 3600 },
{ "", "", "", "", Qt::red, "", QTime(10, 30), 1800 },
{ "", "", "", "", Qt::red, "", QTime(11, 0), 1800 },
{ "", "", "", "", Qt::red, "", QTime(11, 15), 3600 },
};
CalendarEntryLayouter layouter;
QList<CalendarEntryLayout> layout = layouter.layout(entries);
QCOMPARE(layout.count(), entries.count());
QCOMPARE(layout[0].columnIndex, 0);
QCOMPARE(layout[0].columnCount, 2);
QCOMPARE(layout[1].columnIndex, 1);
QCOMPARE(layout[1].columnCount, 2);
QCOMPARE(layout[2].columnIndex, 0);
QCOMPARE(layout[2].columnCount, 1);
QCOMPARE(layout[3].columnIndex, 0);
QCOMPARE(layout[3].columnCount, 2);
QCOMPARE(layout[4].columnIndex, 1);
QCOMPARE(layout[4].columnCount, 2);
#endif
}
};
QTEST_MAIN(TestCalendarData)
#include "testCalendarData.moc"

View File

@@ -8,7 +8,8 @@ equals(GC_UNITTESTS, active) {
SUBDIRS += Core/seasonOffset \
Core/season \
Core/seasonParser \
Core/units
Core/units \
Gui/calendarData
CONFIG += ordered
} else {
message("Unittests are disabled; to enable copy unittests/unittests.pri.in to unittests/unittests.pri")