mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 08:08:42 +00:00
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:
committed by
GitHub
parent
069e2942df
commit
f84f9a410e
@@ -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);
|
||||
|
||||
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) {
|
||||
@@ -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,11 +685,17 @@ void
|
||||
PlanningCalendarWindow::updateActivitiesIfInRange
|
||||
(RideItem *rideItem)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
1046
src/Gui/Calendar.cpp
1046
src/Gui/Calendar.cpp
File diff suppressed because it is too large
Load Diff
@@ -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
112
src/Gui/CalendarData.cpp
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
6
unittests/Gui/calendarData/calendarData.pro
Normal file
6
unittests/Gui/calendarData/calendarData.pro
Normal file
@@ -0,0 +1,6 @@
|
||||
QT += testlib core
|
||||
|
||||
SOURCES = testCalendarData.cpp
|
||||
GC_OBJS = CalendarData
|
||||
|
||||
include(../../unittests.pri)
|
||||
43
unittests/Gui/calendarData/testCalendarData.cpp
Normal file
43
unittests/Gui/calendarData/testCalendarData.cpp
Normal 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"
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user