Files
GoldenCheetah/src/Gui/Calendar.cpp
Joachim Kohlhammer a00e27638f Dialog to build filter queries for similar activities (#4805)
* list all (non-zero) fields and metrics
* filter list by field / metric name
* select an operator per field (ignore, equals, contains, larger than, ...)
* available in the context menus of activity- and calendar view
2026-01-17 14:36:34 -03:00

2571 lines
88 KiB
C++

/*
* 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 "Calendar.h"
#include <QHeaderView>
#include <QHBoxLayout>
#include <QTextEdit>
#include <QScrollArea>
#include <QEvent>
#include <QMouseEvent>
#include <QToolBar>
#include <QDialog>
#include <QDialogButtonBox>
#include <QActionGroup>
#include <QHash>
#include <QDrag>
#include <QMimeData>
#include <QDebug>
#include "Colors.h"
#include "Settings.h"
#include "Context.h"
//////////////////////////////////////////////////////////////////////////////
// CalendarOverview
CalendarOverview::CalendarOverview
(QWidget *parent)
: QCalendarWidget(parent)
{
setGridVisible(false);
setNavigationBarVisible(false);
setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader);
}
QDate
CalendarOverview::firstVisibleDay
() const
{
int year = yearShown();
int month = monthShown();
QDate firstOfMonth(year, month, 1);
int dayOfWeek = firstOfMonth.dayOfWeek();
int offset = (dayOfWeek - firstDayOfWeek() + 7) % 7 + 7;
QDate firstVisibleDate = firstOfMonth.addDays(-offset);
return firstVisibleDate;
}
QDate
CalendarOverview::lastVisibleDay
() const
{
return firstVisibleDay().addDays(48);
}
void
CalendarOverview::limitDateRange
(const DateRange &dr)
{
setMinimumDate(dr.from);
setMaximumDate(dr.to);
}
void
CalendarOverview::fillEntries
(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries)
{
this->activityEntries = activityEntries;
this->headlineEntries = headlineEntries;
}
void
CalendarOverview::paintCell
(QPainter *painter, const QRect &rect, QDate date) const
{
QCalendarWidget::paintCell(painter, rect, date);
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
int w = 10 * dpiXFactor;
int r = 2 * dpiXFactor;
int maxEntries = (rect.width() - 2 * r) / (w + r);
QPolygon triangle;
triangle << QPoint(w / 2, w / 1.4)
<< QPoint(0, 0)
<< QPoint(w, 0);
triangle.translate(rect.x() + r, rect.y() + r);
drawEntries(painter, headlineEntries.value(date), triangle, maxEntries, w + r);
triangle.clear();
triangle << QPoint(w / 2, 0)
<< QPoint(0, w / 1.4)
<< QPoint(w, w / 1.4);
triangle.translate(rect.x() + r, rect.y() + rect.height() - w);
drawEntries(painter, activityEntries.value(date), triangle, maxEntries, w + r);
painter->restore();
}
void
CalendarOverview::drawEntries
(QPainter *painter, const QList<CalendarEntry> &entries, QPolygon polygon, int maxEntries, int shiftX) const
{
painter->save();
int i = 0;
for (const CalendarEntry &entry : entries) {
painter->setPen(entry.color);
if (entry.type == ENTRY_TYPE_PLANNED_ACTIVITY) {
painter->setBrush(Qt::transparent);
} else {
painter->setBrush(entry.color);
}
painter->drawPolygon(polygon);
polygon.translate(shiftX, 0);
if (++i >= maxEntries) {
break;
}
}
painter->restore();
}
//////////////////////////////////////////////////////////////////////////////
// CalendarDayTable
CalendarBaseTable::CalendarBaseTable
(QWidget *parent)
: QTableWidget(parent)
{
}
QMenu*
CalendarBaseTable::buildContextMenu
(const CalendarDay &day, CalendarEntry const * const entryPtr, const QTime &time, bool canHavePhasesEvents)
{
QMenu *contextMenu = new QMenu(this);
if (entryPtr != nullptr) {
CalendarEntry entry = *entryPtr; // Prevent dereferencing of dangling pointer in lambdas
switch (entry.type) {
case ENTRY_TYPE_ACTIVITY:
if (entry.dirty) {
contextMenu->addAction(tr("Save changes"), this, [this, entry]() { emit saveChanges(entry); });
contextMenu->addAction(tr("Discard changes"), this, [this, entry]() { emit discardChanges(entry); });
contextMenu->addSeparator();
}
if (! entry.linkedReference.isEmpty()) {
contextMenu->addAction(tr("View planned activity..."), this, [this, entry]() {
emit viewLinkedActivity(entry);
});
}
contextMenu->addAction(tr("View completed activity..."), this, [this, entry]() { emit viewActivity(entry); });
contextMenu->addSeparator();
if (entry.linkedReference.isEmpty()) {
contextMenu->addAction(tr("Link to planned activity"), this, [this, entry]() { emit linkActivity(entry, true); });
contextMenu->addAction(tr("Link to planned activity..."), this, [this, entry]() { emit linkActivity(entry, false); });
} else {
contextMenu->addAction(tr("Unlink from planned activity"), this, [this, entry]() { emit unlinkActivity(entry); });
}
contextMenu->addSeparator();
contextMenu->addAction(tr("Filter similar activities..."), this, [this, entry]() { emit filterSimilar(entry); });
contextMenu->addSeparator();
contextMenu->addAction(tr("Delete completed activity"), this, [this, entry]() { emit delActivity(entry); });
break;
case ENTRY_TYPE_PLANNED_ACTIVITY:
if (entry.dirty) {
contextMenu->addAction(tr("Save changes"), this, [this, entry]() { emit saveChanges(entry); });
contextMenu->addAction(tr("Discard changes"), this, [this, entry]() { emit discardChanges(entry); });
contextMenu->addSeparator();
}
if (! entry.linkedReference.isEmpty()) {
contextMenu->addAction(tr("View completed activity..."), this, [this, entry]() { emit viewLinkedActivity(entry); });
}
contextMenu->addAction(tr("View planned activity..."), this, [this, entry]() { emit viewActivity(entry); });
contextMenu->addSeparator();
if (entry.linkedReference.isEmpty()) {
contextMenu->addAction(tr("Mark as completed"), this, [this, entry]() { emit linkActivity(entry, true); });
contextMenu->addAction(tr("Mark as completed..."), this, [this, entry]() { emit linkActivity(entry, false); });
} else {
contextMenu->addAction(tr("Mark as incomplete"), this, [this, entry]() { emit unlinkActivity(entry); });
}
contextMenu->addSeparator();
if (entry.hasTrainMode) {
contextMenu->addAction(tr("Show in train mode..."), this, [this, entry]() { emit showInTrainMode(entry); });
}
contextMenu->addAction(tr("Filter similar activities..."), this, [this, entry]() { emit filterSimilar(entry); });
contextMenu->addSeparator();
contextMenu->addAction(tr("Delete planned activity"), this, [this, entry]() { emit delActivity(entry); });
break;
case ENTRY_TYPE_EVENT:
if (canHavePhasesEvents) {
contextMenu->addAction(tr("Edit event..."), this, [this, entry]() { emit editEvent(entry); });
contextMenu->addAction(tr("Delete event"), this, [this, entry]() { emit delEvent(entry); });
}
break;
case ENTRY_TYPE_PHASE:
if (canHavePhasesEvents) {
contextMenu->addAction(tr("Edit phase..."), this, [this, entry]() { emit editPhase(entry); });
contextMenu->addAction(tr("Delete phase..."), this, [this, entry]() { emit delPhase(entry); });
}
break;
default:
break;
}
} else {
bool canAddActivity;
bool canAddPlanned;
if (time.isValid()) {
canAddActivity = day.date < QDate::currentDate()
|| ( day.date == QDate::currentDate()
&& time < QTime::currentTime());
canAddPlanned = ! canAddActivity;
} else {
canAddActivity = day.date <= QDate::currentDate();
canAddPlanned = day.date >= QDate::currentDate();
}
if (canAddActivity) {
contextMenu->addAction(tr("Log activity..."), this, [this, day, time]() {
QTime activityTime(time);
if (! activityTime.isValid()) {
activityTime = QTime::currentTime();
if (day.date == QDate::currentDate()) {
activityTime = activityTime.addSecs(std::max(-4 * 3600, activityTime.secsTo(QTime(0, 0))));
}
}
emit addActivity(false, day.date, activityTime);
});
}
if (canAddPlanned) {
contextMenu->addAction(tr("Plan activity..."), this, [this, day, time]() {
QTime activityTime(time);
if (! activityTime.isValid()) {
activityTime = QTime::currentTime();
if (day.date == QDate::currentDate()) {
activityTime = activityTime.addSecs(std::min(4 * 3600, activityTime.secsTo(QTime(23, 59, 59))));
}
}
emit addActivity(true, day.date, activityTime);
});
}
if (canHavePhasesEvents) {
contextMenu->addSeparator();
contextMenu->addAction(tr("Add phase..."), this, [this, day]() { emit addPhase(day.date); });
contextMenu->addAction(tr("Add event..."), this, [this, day]() { emit addEvent(day.date); });
}
if (day.date >= QDate::currentDate()) {
contextMenu->addSeparator();
contextMenu->addAction(tr("Repeat schedule..."), this, [this, day]() { emit repeatSchedule(day.date); });
bool hasPlannedActivity = false;
for (const CalendarEntry &dayEntry : day.entries) {
if (dayEntry.type == ENTRY_TYPE_PLANNED_ACTIVITY) {
hasPlannedActivity = true;
break;
}
}
if (hasPlannedActivity) {
contextMenu->addAction(tr("Insert rest day"), this, [this, day]() { emit insertRestday(day.date); });
} else {
contextMenu->addAction(tr("Delete rest day"), this, [this, day]() { emit delRestday(day.date); });
}
}
}
return contextMenu;
}
//////////////////////////////////////////////////////////////////////////////
// CalendarDayTable
CalendarDayTable::CalendarDayTable
(const QDate &date, CalendarDayTableType type, Qt::DayOfWeek firstDayOfWeek, QWidget *parent)
: CalendarBaseTable(parent), type(type)
{
int numDays = type == CalendarDayTableType::Day ? 1 : 7;
dragTimer.setSingleShot(true);
setAcceptDrops(true);
setColumnCount(numDays + 1);
setRowCount(3);
setFrameShape(QFrame::NoFrame);
QList<QStyledItemDelegate*> row0Delegates;
row0Delegates << new CalendarTimeScaleDelegate(&timeScaleData, this);
for (int i = 0; i < numDays; ++i) {
row0Delegates << new CalendarDetailedDayDelegate(&timeScaleData, this);
}
setItemDelegateForRow(0, new CalendarHeadlineDelegate(this));
setItemDelegateForRow(1, new ColumnDelegatingItemDelegate(row0Delegates, this));
setItemDelegateForRow(2, new CalendarSummaryDelegate(numDays > 1 ? 4 : 20, this));
horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
for (int i = 0; i < numDays; ++i) {
horizontalHeader()->setSectionResizeMode(i + 1, QHeaderView::Stretch);
}
horizontalHeader()->setVisible(false);
verticalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
verticalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
verticalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
verticalHeader()->setVisible(false);
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &CalendarDayTable::customContextMenuRequested, this, &CalendarDayTable::showContextMenu);
setFirstDayOfWeek(firstDayOfWeek);
setDay(date);
}
bool
CalendarDayTable::setDay
(const QDate &date)
{
if (! isInDateRange(date)) {
return false;
}
this->date = date;
clearContents();
QTableWidgetItem *timeScaleItem = new QTableWidgetItem();
timeScaleItem->setData(CalendarTimeScaleDelegate::CurrentYRole, -1);
setItem(1, 0, timeScaleItem);
if (type == CalendarDayTableType::Day) {
QTableWidgetItem *headlineItem = new QTableWidgetItem();
CalendarDay headlineDay;
headlineDay.date = date;
headlineDay.isDimmed = DayDimLevel::None;
headlineItem->setData(CalendarHeadlineDelegate::DayRole, QVariant::fromValue(headlineDay));
setItem(0, 1, headlineItem);
QTableWidgetItem *item = new QTableWidgetItem();
CalendarDay day;
day.date = date;
day.isDimmed = DayDimLevel::None;
item->setData(CalendarDetailedDayDelegate::DayRole, QVariant::fromValue(day));
setItem(1, 1, item);
} else {
QDate dayDate = firstVisibleDay();
for (int i = 0; i < 7; ++i) {
QTableWidgetItem *headlineItem = new QTableWidgetItem();
CalendarDay headlineDay;
headlineDay.date = dayDate;
headlineDay.isDimmed = DayDimLevel::None;
headlineItem->setData(CalendarHeadlineDelegate::DayRole, QVariant::fromValue(headlineDay));
setItem(0, i + 1, headlineItem);
QTableWidgetItem *item = new QTableWidgetItem();
CalendarDay day;
day.date = dayDate;
day.isDimmed = dr.pass(dayDate) ? DayDimLevel::None : DayDimLevel::Full;
item->setData(CalendarDetailedDayDelegate::DayRole, QVariant::fromValue(day));
setItem(1, i + 1, item);
dayDate = dayDate.addDays(1);
}
}
setSelectionMode(QAbstractItemView::NoSelection);
setEditTriggers(QAbstractItemView::NoEditTriggers);
emit dayChanged(date);
return true;
}
QDate
CalendarDayTable::firstVisibleDay
() const
{
return firstVisibleDay(date);
}
QDate
CalendarDayTable::firstVisibleDay
(const QDate &d) const
{
if (type == CalendarDayTableType::Day) {
return d;
} else {
int currentDayOfWeek = d.dayOfWeek();
int offset = currentDayOfWeek - firstDayOfWeek;
if (offset < 0) {
offset += 7;
}
return d.addDays(-offset);
}
}
QDate
CalendarDayTable::lastVisibleDay
() const
{
return lastVisibleDay(date);
}
QDate
CalendarDayTable::lastVisibleDay
(const QDate &d) const
{
if (type == CalendarDayTableType::Day) {
return d;
} else {
return firstVisibleDay(d).addDays(6);
}
}
QDate
CalendarDayTable::selectedDate
() const
{
return date;
}
bool
CalendarDayTable::isInDateRange
(const QDate &date) const
{
return date.isValid() && dr.pass(date);
}
void
CalendarDayTable::fillEntries
(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries)
{
int startHour = defaultStartHour;
int endHour = defaultEndHour;
int numDays = type == CalendarDayTableType::Day ? 1 : 7;
for (int i = 0; i < numDays; ++i) {
QTableWidgetItem *headlineItem = this->item(0, i + 1);
CalendarDay headlineDay = headlineItem->data(CalendarHeadlineDelegate::DayRole).value<CalendarDay>();
headlineDay.entries.clear();
headlineDay.headlineEntries = headlineEntries.value(headlineDay.date);
headlineItem->setData(CalendarHeadlineDelegate::DayRole, QVariant::fromValue(headlineDay));
QTableWidgetItem *item = this->item(1, i + 1);
CalendarDay day = item->data(CalendarDetailedDayDelegate::DayRole).value<CalendarDay>();
day.entries = activityEntries.value(day.date);
day.headlineEntries.clear();
item->setData(CalendarDetailedDayDelegate::DayRole, QVariant::fromValue(day));
for (const CalendarEntry &entry : day.entries) {
startHour = std::min(startHour, entry.start.hour());
QTime endHourTime = entry.start.addSecs(entry.durationSecs);
if (endHourTime < entry.start) {
endHour = 24;
} else {
endHour = std::max(endHour, endHourTime.hour() + 1);
}
}
endHour = std::min(24, endHour);
CalendarEntryLayouter layouter;
QList<CalendarEntryLayout> layout = layouter.layout(day.entries);
item->setData(CalendarDetailedDayDelegate::LayoutRole, QVariant::fromValue(layout));
QTableWidgetItem *summaryItem = new QTableWidgetItem();
summaryItem->setData(CalendarSummaryDelegate::SummaryRole, QVariant::fromValue(summaries.value(i)));
summaryItem->setFlags(Qt::ItemIsEnabled);
setItem(2, i + 1, summaryItem);
}
timeScaleData.setFirstMinute(startHour * 60);
timeScaleData.setLastMinute(endHour * 60);
}
void
CalendarDayTable::limitDateRange
(const DateRange &dr, bool canHavePhasesOrEvents)
{
if (dr.from.isValid() && dr.to.isValid() && dr.from > dr.to) {
return;
}
this->canHavePhasesOrEvents = canHavePhasesOrEvents;
this->dr = dr;
if (! dr.pass(selectedDate())) {
if (dr.pass(lastVisibleDay())) {
setDay(lastVisibleDay());
} else if (dr.pass(firstVisibleDay())) {
setDay(firstVisibleDay());
} else if (dr.pass(QDate::currentDate())) {
setDay(QDate::currentDate());
} else if (dr.to.isValid() && dr.to < QDate::currentDate()) {
setDay(dr.to);
} else if (dr.from.isValid() && dr.from > QDate::currentDate()) {
setDay(dr.from);
} else if (dr.to.isValid()) {
setDay(dr.to);
} else {
setDay(dr.from);
}
}
}
void
CalendarDayTable::setFirstDayOfWeek
(Qt::DayOfWeek firstDayOfWeek)
{
this->firstDayOfWeek = firstDayOfWeek;
if (type == CalendarDayTableType::Week) {
setDay(selectedDate());
}
}
void
CalendarDayTable::setStartHour
(int hour)
{
defaultStartHour = hour;
}
void
CalendarDayTable::setEndHour
(int hour)
{
defaultEndHour = hour;
}
void
CalendarDayTable::changeEvent
(QEvent *event)
{
if (event->type() == QEvent::PaletteChange) {
horizontalHeader()->setStyleSheet(QString("QHeaderView::section { border: none; background-color: %1; color: %2 }")
.arg(palette().color(QPalette::Active, QPalette::Window).name())
.arg(palette().color(QPalette::Active, QPalette::WindowText).name()));
}
QTableWidget::changeEvent(event);
}
void
CalendarDayTable::mouseDoubleClickEvent
(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton) {
return;
}
QPoint pos = event->pos();
QModelIndex index = indexAt(pos);
int row = index.row();
int column = index.column();
if (row == 1 && column > 0) {
ColumnDelegatingItemDelegate *delegatingDelegate = static_cast<ColumnDelegatingItemDelegate*>(itemDelegateForRow(row));
CalendarDetailedDayDelegate *delegate = static_cast<CalendarDetailedDayDelegate*>(delegatingDelegate->getDelegate(column));
CalendarDay day = index.data(CalendarDetailedDayDelegate::DayRole).value<CalendarDay>();
int entryIdx = delegate->entryTester.hitTest(index, event->pos());
if (entryIdx >= 0) {
emit viewActivity(day.entries[entryIdx]);
} else {
QTime time = timeScaleData.timeFromYInTable(pos.y(), visualRect(index));
bool past = day.date < QDate::currentDate()
|| ( day.date == QDate::currentDate()
&& time < QTime::currentTime());
emit addActivity(! past, day.date, time);
}
}
}
void
CalendarDayTable::mousePressEvent
(QMouseEvent *event)
{
isDraggable = false;
pressedPos = event->pos();
pressedIndex = indexAt(pressedPos);
if (pressedIndex.row() == 1 && pressedIndex.column() > 0) {
ColumnDelegatingItemDelegate *delegatingDelegate = static_cast<ColumnDelegatingItemDelegate*>(itemDelegateForRow(pressedIndex.row()));
CalendarDetailedDayDelegate *delegate = static_cast<CalendarDetailedDayDelegate*>(delegatingDelegate->getDelegate(pressedIndex.column()));
int entryIdx = delegate->entryTester.hitTest(pressedIndex, pressedPos);
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
if (item != nullptr) {
item->setData(CalendarDetailedDayDelegate::PressedEntryRole, entryIdx);
}
}
CalendarDay day = pressedIndex.data(CalendarDetailedDayDelegate::DayRole).value<CalendarDay>();
if (entryIdx >= 0) {
CalendarEntry calEntry = day.entries[entryIdx];
setRelated(day.entries[entryIdx].linkedReference);
isDraggable = day.entries[entryIdx].isRelocatable;
if (event->button() == Qt::LeftButton && isDraggable) {
dragTimer.start(QApplication::startDragTime());
}
}
}
}
void
CalendarDayTable::mouseReleaseEvent
(QMouseEvent *event)
{
if (dragTimer.isActive()) {
dragTimer.stop();
}
if (pressedIndex.isValid()) {
QPoint pos = event->pos();
QModelIndex releasedIndex = indexAt(pos);
if (releasedIndex == pressedIndex) {
if (pressedIndex.row() == 1 && pressedIndex.column() > 0) {
QTime time = timeScaleData.timeFromYInTable(pos.y(), visualRect(releasedIndex));
CalendarDay day = pressedIndex.data(CalendarDetailedDayDelegate::DayRole).value<CalendarDay>();
ColumnDelegatingItemDelegate *delegatingDelegate = static_cast<ColumnDelegatingItemDelegate*>(itemDelegateForRow(pressedIndex.row()));
CalendarDetailedDayDelegate *delegate = static_cast<CalendarDetailedDayDelegate*>(delegatingDelegate->getDelegate(pressedIndex.column()));
int entryIdx = delegate->entryTester.hitTest(pressedIndex, pressedPos);
if (event->button() == Qt::LeftButton) {
if (entryIdx >= 0) {
emit entryClicked(day, entryIdx);
} else {
emit dayClicked(day, time);
}
} else if (event->button() == Qt::RightButton) {
if (entryIdx >= 0) {
emit entryRightClicked(day, entryIdx);
} else {
emit dayRightClicked(day, time);
}
}
}
}
if (pressedIndex.isValid() && pressedIndex.row() == 1) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
if (item != nullptr) {
item->setData(CalendarDetailedDayDelegate::PressedEntryRole, QVariant());
}
}
pressedPos = QPoint();
pressedIndex = QModelIndex();
clearRelated();
}
}
void
CalendarDayTable::mouseMoveEvent
(QMouseEvent *event)
{
if ( ! (event->buttons() & Qt::LeftButton)
|| ! isDraggable
|| ! pressedIndex.isValid()
|| dragTimer.isActive()
|| (event->pos() - pressedPos).manhattanLength() < QApplication::startDragDistance()) {
return;
}
int row = pressedIndex.row();
int column = pressedIndex.column();
if (row != 1 || column == 0) {
return;
}
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData();
ColumnDelegatingItemDelegate *delegatingDelegate = static_cast<ColumnDelegatingItemDelegate*>(itemDelegateForRow(row));
CalendarDetailedDayDelegate *delegate = static_cast<CalendarDetailedDayDelegate*>(delegatingDelegate->getDelegate(column));
int entryIdx = delegate->entryTester.hitTest(pressedIndex, pressedPos);
CalendarEntry calEntry = pressedIndex.data(CalendarDetailedDayDelegate::DayRole).value<CalendarDay>().entries[entryIdx];
QSize pixmapSize(40 * dpiXFactor, 40 * dpiYFactor);
QPixmap pixmap = svgAsColoredPixmap(calEntry.iconFile, pixmapSize, 0, calEntry.color);
drag->setPixmap(pixmap);
drag->setHotSpot(QPoint(pixmapSize.width() / 2, pixmapSize.height() / 2));
QList<int> entryCoords = { row, column, entryIdx };
QString entryStr = QString::fromStdString(std::accumulate(entryCoords.begin(),
entryCoords.end(),
std::string(),
[](const std::string &a, int b) {
return a + (a.length() ? "," : "") + std::to_string(b);
}));
QByteArray entryBytes = entryStr.toUtf8();
mimeData->setData("application/x-hour-grid-entry", entryBytes);
drag->setMimeData(mimeData);
drag->exec(Qt::MoveAction);
QTableWidgetItem *item = this->item(row, column);
item->setData(CalendarDetailedDayDelegate::PressedEntryRole, QVariant());
pressedPos = QPoint();
pressedIndex = QModelIndex();
clearRelated();
}
void
CalendarDayTable::dragEnterEvent
(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("application/x-hour-grid-entry") && event->source() == this) {
event->acceptProposedAction();
}
}
void
CalendarDayTable::dragMoveEvent
(QDragMoveEvent *event)
{
QPoint pos = event->position().toPoint();
QModelIndex hoverIndex = indexAt(pos);
if ( hoverIndex.isValid()
&& hoverIndex.column() > 0
&& hoverIndex.row() == 1) {
CalendarDay day = hoverIndex.data(CalendarDetailedDayDelegate::DayRole).value<CalendarDay>();
QRect indexRect = visualRect(hoverIndex);
QTime time = timeScaleData.timeFromYInTable(pos.y(), indexRect);
bool past = day.date < QDate::currentDate()
|| ( day.date == QDate::currentDate()
&& time < QTime::currentTime());
bool conflict = false;
BlockIndicator blockIndicator = BlockIndicator::NoBlock;
if (day.date < QDate::currentDate()) {
blockIndicator = BlockIndicator::AllBlock;
} else if (day.date == QDate::currentDate()) {
blockIndicator = BlockIndicator::BlockBeforeNow;
}
setDropIndicator(pos.y(), blockIndicator);
for (const CalendarEntry &entry : day.entries) {
if (entry.start == time) {
conflict = true;
break;
}
}
if (past || conflict) {
event->ignore();
return;
}
event->accept();
} else {
setDropIndicator(-1, BlockIndicator::NoBlock);
event->ignore();
}
}
void
CalendarDayTable::dragLeaveEvent
(QDragLeaveEvent *event)
{
Q_UNUSED(event)
setDropIndicator(-1, BlockIndicator::NoBlock);
}
void
CalendarDayTable::dropEvent
(QDropEvent *event)
{
if (event->mimeData()->hasFormat("application/x-hour-grid-entry")) {
setDropIndicator(-1, BlockIndicator::NoBlock);
QByteArray entryBytes = event->mimeData()->data("application/x-hour-grid-entry");
QString entryStr = QString::fromUtf8(entryBytes);
QStringList entryStrList = entryStr.split(',');
QList<int> entryCoords;
for (const QString &str : entryStrList) {
entryCoords.append(str.toInt());
}
if (entryCoords.size() != 3) {
return;
}
if ( entryCoords[0] == 1
&& entryCoords[1] > 0
&& entryCoords[1] < columnCount()) {
QModelIndex srcIndex = model()->index(entryCoords[0], entryCoords[1]);
int entryIdx = entryCoords[2];
if (srcIndex.isValid() && entryIdx >= 0) {
CalendarDay srcDay = srcIndex.data(CalendarDetailedDayDelegate::DayRole).value<CalendarDay>();
if (entryIdx < srcDay.entries.count()) {
QPoint pos = event->position().toPoint();
QModelIndex destIndex = indexAt(pos);
QTime time = timeScaleData.timeFromYInTable(pos.y(), visualRect(destIndex));
CalendarDay destDay = destIndex.data(CalendarDetailedDayDelegate::DayRole).value<CalendarDay>();
emit entryMoved(srcDay.entries[entryIdx], srcDay.date, destDay.date, time);
}
}
}
}
}
void
CalendarDayTable::showContextMenu
(const QPoint &pos)
{
QModelIndex index = indexAt(pos);
if (! index.isValid()) {
return;
}
int row = index.row();
int column = index.column();
if (pressedIndex.isValid() && (row != 1 || column == 0)) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
if (item != nullptr) {
item->setData(CalendarDetailedDayDelegate::PressedEntryRole, QVariant());
}
}
QMenu *contextMenu = nullptr;
if (row == 0 && column > 0) {
contextMenu = makeHeaderMenu(index, pos);
} else if (row == 1 && column > 0) {
contextMenu = makeActivityMenu(index, pos);
}
if (contextMenu != nullptr) {
contextMenu->exec(viewport()->mapToGlobal(pos));
delete contextMenu;
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
if (item != nullptr) {
item->setData(CalendarDetailedDayDelegate::PressedEntryRole, QVariant());
}
}
}
clearRelated();
}
void
CalendarDayTable::setDropIndicator
(int y, BlockIndicator block)
{
QTableWidgetItem *scaleItem = item(1, 0);
scaleItem->setData(CalendarTimeScaleDelegate::CurrentYRole, y);
scaleItem->setData(CalendarTimeScaleDelegate::BlockRole, static_cast<int>(block));
}
QMenu*
CalendarDayTable::makeHeaderMenu
(const QModelIndex &index, const QPoint &pos)
{
CalendarHeadlineDelegate *delegate = static_cast<CalendarHeadlineDelegate*>(itemDelegateForRow(index.row()));
int entryIdx = delegate->headlineTester.hitTest(index, pos);
CalendarDay day = index.data(CalendarHeadlineDelegate::DayRole).value<CalendarDay>();
CalendarEntry *entry = nullptr;
if (entryIdx >= 0) {
entry = &day.headlineEntries[entryIdx];
}
return buildContextMenu(day, entry, QTime(), isInDateRange(day.date) && canHavePhasesOrEvents);
}
QMenu*
CalendarDayTable::makeActivityMenu
(const QModelIndex &index, const QPoint &pos)
{
ColumnDelegatingItemDelegate *delegatingDelegate = static_cast<ColumnDelegatingItemDelegate*>(itemDelegateForRow(index.row()));
CalendarDetailedDayDelegate *delegate = static_cast<CalendarDetailedDayDelegate*>(delegatingDelegate->getDelegate(index.column()));
int entryIdx = delegate->entryTester.hitTest(index, pos);
CalendarDay day = index.data(CalendarDetailedDayDelegate::DayRole).value<CalendarDay>();
QTime time = timeScaleData.timeFromYInTable(pos.y(), visualRect(index));
CalendarEntry *entry = nullptr;
if (entryIdx >= 0) {
entry = &day.entries[entryIdx];
}
return buildContextMenu(day, entry, time, isInDateRange(day.date) && canHavePhasesOrEvents);
}
void
CalendarDayTable::setRelated
(const QString &linkedReference)
{
if (! linkedReference.isEmpty()) {
for (int col = 1; col < columnCount(); ++col) {
QTableWidgetItem *item = this->item(1, col);
if (item) {
CalendarDay day = item->data(CalendarDetailedDayDelegate::DayRole).value<CalendarDay>();
for (int idx = 0; idx < day.entries.count(); ++idx) {
if (day.entries[idx].reference == linkedReference) {
item->setData(CalendarDetailedDayDelegate::RelatedEntryRole, idx);
return;
}
}
}
}
}
}
void
CalendarDayTable::clearRelated
()
{
for (int col = 1; col < columnCount(); ++col) {
QTableWidgetItem *item = this->item(1, col);
if (item) {
item->setData(CalendarDetailedDayDelegate::RelatedEntryRole, QVariant());
}
}
}
//////////////////////////////////////////////////////////////////////////////
// CalendarMonthTable
CalendarMonthTable::CalendarMonthTable
(Qt::DayOfWeek firstDayOfWeek, QWidget *parent)
: CalendarMonthTable(QDate::currentDate(), firstDayOfWeek, parent)
{
}
CalendarMonthTable::CalendarMonthTable
(const QDate &dateInMonth, Qt::DayOfWeek firstDayOfWeek, QWidget *parent)
: CalendarBaseTable(parent)
{
dragTimer.setSingleShot(true);
setAcceptDrops(true);
setColumnCount(8);
setFrameShape(QFrame::NoFrame);
setItemDelegateForColumn(7, new CalendarSummaryDelegate(4, this));
for (int i = 0; i < 7; ++i) {
setItemDelegateForColumn(i, new CalendarCompactDayDelegate(this));
}
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &CalendarMonthTable::customContextMenuRequested, this, &CalendarMonthTable::showContextMenu);
connect(this, &QTableWidget::itemSelectionChanged, this, [this]() {
QList<QTableWidgetItem*> selection = selectedItems();
if (selection.count() > 0) {
QTableWidgetItem *item = selection[0];
if (item->column() < 7) {
emit daySelected(item->data(CalendarCompactDayDelegate::DayRole).value<CalendarDay>());
}
}
});
setFirstDayOfWeek(firstDayOfWeek);
setMonth(dateInMonth);
}
bool
CalendarMonthTable::selectDay
(const QDate &day)
{
if (day < startDate || day > endDate) {
return false;
}
int daysAfterFirst = startDate.daysTo(day);
int row = daysAfterFirst / 7;
int col = daysAfterFirst % 7;
setCurrentCell(row, col);
return true;
}
bool
CalendarMonthTable::setMonth
(const QDate &dateInMonth, bool allowKeepMonth)
{
if ( ! dateInMonth.isValid()
|| ( ! isInDateRange(dateInMonth)
&& ! allowKeepMonth)) {
return false;
}
if (! (allowKeepMonth && dateInMonth >= startDate && dateInMonth <= endDate) || ! firstOfMonth.isValid()) {
firstOfMonth = QDate(dateInMonth.year(), dateInMonth.month(), 1);
}
clearContents();
int startDayOfWeek = firstOfMonth.dayOfWeek();
int offset = (startDayOfWeek - firstDayOfWeek + 7) % 7;
startDate = firstOfMonth.addDays(-offset);
int daysInMonth = dateInMonth.daysInMonth();
int totalDisplayedDays = offset + daysInMonth;
int totalRows = (totalDisplayedDays + 6) / 7;
endDate = startDate.addDays(totalRows * 7 - 1);
setRowCount(totalRows);
for (int i = 0; i < totalRows * 7; ++i) {
QDate date = startDate.addDays(i);
int row = i / 7;
int col = (date.dayOfWeek() - firstDayOfWeek + 7) % 7;
QTableWidgetItem *item = new QTableWidgetItem();
item->setData(DateRole, date);
DayDimLevel isDimmed = DayDimLevel::None;
if (! dr.pass(date)) {
isDimmed = DayDimLevel::Full;
} else if (date.month() != dateInMonth.month()) {
isDimmed = DayDimLevel::Partial;
}
CalendarDay day;
day.date = date;
day.isDimmed = isDimmed;
item->setData(CalendarCompactDayDelegate::DayRole, QVariant::fromValue(day));
setItem(row, col, item);
}
for (int i = 0; i < 7; ++i) {
horizontalHeader()->setSectionResizeMode(i, QHeaderView::Stretch);
}
horizontalHeader()->setSectionResizeMode(7, QHeaderView::Stretch);
verticalHeader()->setSectionResizeMode(QHeaderView::Stretch);
setSelectionMode(QAbstractItemView::SingleSelection);
setEditTriggers(QAbstractItemView::NoEditTriggers);
selectDay(dateInMonth);
emit monthChanged(firstOfMonth, startDate, endDate);
return true;
}
bool
CalendarMonthTable::isInDateRange
(const QDate &date) const
{
return date.isValid() && dr.pass(date);
}
void
CalendarMonthTable::fillEntries
(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries)
{
for (int i = 0; i < rowCount() * 7; ++i) {
QDate date = startDate.addDays(i);
int row = i / 7;
int col = (date.dayOfWeek() - firstDayOfWeek + 7) % 7;
QTableWidgetItem *item = this->item(row, col);
CalendarDay day = item->data(CalendarCompactDayDelegate::DayRole).value<CalendarDay>();
if (activityEntries.contains(date)) {
day.entries = activityEntries[date];
} else {
day.entries.clear();
}
if (headlineEntries.contains(date)) {
day.headlineEntries = headlineEntries[date];
} else {
day.headlineEntries.clear();
}
item->setData(CalendarCompactDayDelegate::DayRole, QVariant::fromValue(day));
}
for (int row = 0; row < rowCount() && row < summaries.count(); ++row) {
QTableWidgetItem *summaryItem = new QTableWidgetItem();
summaryItem->setData(CalendarSummaryDelegate::SummaryRole, QVariant::fromValue(summaries[row]));
summaryItem->setFlags(Qt::ItemIsEnabled);
setItem(row, 7, summaryItem);
}
}
QDate
CalendarMonthTable::firstOfCurrentMonth
() const
{
return firstOfMonth;
}
QDate
CalendarMonthTable::firstVisibleDay
() const
{
return startDate;
}
QDate
CalendarMonthTable::lastVisibleDay
() const
{
return endDate;
}
QDate
CalendarMonthTable::selectedDate
() const
{
QTableWidgetItem *item = currentItem();
if (item != nullptr) {
return item->data(DateRole).toDate();
}
return firstOfMonth;
}
void
CalendarMonthTable::limitDateRange
(const DateRange &dr, bool allowKeepMonth, bool canHavePhasesOrEvents)
{
if (dr.from.isValid() && dr.to.isValid() && dr.from > dr.to) {
return;
}
this->dr = dr;
this->canHavePhasesOrEvents = canHavePhasesOrEvents;
if (currentItem() != nullptr && isInDateRange(currentItem()->data(DateRole).toDate())) {
setMonth(currentItem()->data(DateRole).toDate());
} else if (isInDateRange(QDate::currentDate())) {
setMonth(QDate::currentDate());
} else if (isInDateRange(firstOfMonth)) {
setMonth(firstOfMonth, allowKeepMonth);
} else if (dr.to.isValid() && dr.to < QDate::currentDate()) {
setMonth(dr.to);
} else if (dr.from.isValid() && dr.from > QDate::currentDate()) {
setMonth(dr.from);
} else if (dr.to.isValid()) {
setMonth(dr.to);
} else {
setMonth(dr.from);
}
}
void
CalendarMonthTable::setFirstDayOfWeek
(Qt::DayOfWeek firstDayOfWeek)
{
clear();
QLocale locale;
QStringList headers;
this->firstDayOfWeek = firstDayOfWeek;
for (int i = Qt::Monday - 1; i < Qt::Sunday; ++i) {
headers << locale.dayName((i + firstDayOfWeek - 1) % 7 + 1, QLocale::ShortFormat);
}
headers << tr("Summary");
setHorizontalHeaderLabels(headers);
verticalHeader()->setVisible(false);
}
void
CalendarMonthTable::changeEvent
(QEvent *event)
{
if (event->type() == QEvent::PaletteChange) {
horizontalHeader()->setStyleSheet(QString("QHeaderView::section { border: none; background-color: %1; color: %2 }")
.arg(palette().color(QPalette::Active, QPalette::Window).name())
.arg(palette().color(QPalette::Active, QPalette::WindowText).name()));
}
QTableWidget::changeEvent(event);
}
void
CalendarMonthTable::mouseDoubleClickEvent
(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton) {
return;
}
QPoint pos = event->pos();
QModelIndex index = indexAt(pos);
int column = index.column();
if (column < 7) {
QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(index);
CalendarCompactDayDelegate *delegate = static_cast<CalendarCompactDayDelegate*>(abstractDelegate);
CalendarDay day = index.data(CalendarCompactDayDelegate::DayRole).value<CalendarDay>();
if (delegate->moreTester.hitTest(index, event->pos()) != -1) {
emit moreDblClicked(day);
} else {
int entryIdx = delegate->entryTester.hitTest(index, event->pos());
if (entryIdx >= 0) {
emit entryDblClicked(day, entryIdx);
} else {
emit dayDblClicked(day);
}
}
} else {
emit summaryDblClicked(index);
}
}
void
CalendarMonthTable::mousePressEvent
(QMouseEvent *event)
{
isDraggable = false;
pressedPos = event->pos();
pressedIndex = indexAt(pressedPos);
QTableWidget::mousePressEvent(event);
if (pressedIndex.column() < 7) {
QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(pressedIndex);
CalendarCompactDayDelegate *delegate = static_cast<CalendarCompactDayDelegate*>(abstractDelegate);
if (delegate->moreTester.hitTest(pressedIndex, pressedPos) == -1) {
int entryIdx = delegate->entryTester.hitTest(pressedIndex, pressedPos);
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
if (item != nullptr) {
item->setData(CalendarCompactDayDelegate::PressedEntryRole, entryIdx);
}
}
CalendarDay day = pressedIndex.data(CalendarCompactDayDelegate::DayRole).value<CalendarDay>();
if (entryIdx >= 0) {
CalendarEntry calEntry = day.entries[entryIdx];
setRelated(day.entries[entryIdx].linkedReference);
isDraggable = day.entries[entryIdx].isRelocatable;
if (event->button() == Qt::LeftButton && isDraggable) {
dragTimer.start(QApplication::startDragTime());
}
}
}
}
}
void
CalendarMonthTable::mouseReleaseEvent
(QMouseEvent *event)
{
if (dragTimer.isActive()) {
dragTimer.stop();
}
if (pressedIndex.isValid()) {
QPoint pos = event->pos();
QModelIndex releasedIndex = indexAt(pos);
if (releasedIndex == pressedIndex) {
if (pressedIndex.column() < 7) {
CalendarDay day = pressedIndex.data(CalendarCompactDayDelegate::DayRole).value<CalendarDay>();
QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(releasedIndex);
CalendarCompactDayDelegate *delegate = static_cast<CalendarCompactDayDelegate*>(abstractDelegate);
if (delegate->moreTester.hitTest(releasedIndex, event->pos()) != -1) {
emit moreClicked(day);
} else {
int entryIdx = delegate->entryTester.hitTest(releasedIndex, event->pos());
if (event->button() == Qt::LeftButton) {
if (entryIdx >= 0) {
emit entryClicked(day, entryIdx);
} else {
emit dayClicked(day);
}
} else if (event->button() == Qt::RightButton) {
if (entryIdx >= 0) {
emit entryRightClicked(day, entryIdx);
} else {
emit dayRightClicked(day);
}
}
}
} else {
if (event->button() == Qt::LeftButton) {
emit summaryClicked(pressedIndex);
} else if (event->button() == Qt::RightButton) {
emit summaryRightClicked(pressedIndex);
}
}
}
selectDay(pressedIndex.data(DateRole).toDate());
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
if (item != nullptr) {
item->setData(CalendarCompactDayDelegate::PressedEntryRole, QVariant());
}
}
pressedPos = QPoint();
pressedIndex = QModelIndex();
clearRelated();
}
QTableWidget::mouseReleaseEvent(event);
}
void
CalendarMonthTable::mouseMoveEvent
(QMouseEvent *event)
{
if ( ! (event->buttons() & Qt::LeftButton)
|| ! isDraggable
|| dragTimer.isActive()
|| (event->pos() - pressedPos).manhattanLength() < QApplication::startDragDistance()) {
return;
}
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData();
QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(pressedIndex);
CalendarCompactDayDelegate *delegate = static_cast<CalendarCompactDayDelegate*>(abstractDelegate);
int entryIdx = delegate->entryTester.hitTest(pressedIndex, pressedPos);
CalendarEntry calEntry = pressedIndex.data(CalendarCompactDayDelegate::DayRole).value<CalendarDay>().entries[entryIdx];
QSize pixmapSize(40 * dpiXFactor, 40 * dpiYFactor);
QPixmap pixmap = svgAsColoredPixmap(calEntry.iconFile, pixmapSize, 0, calEntry.color);
drag->setPixmap(pixmap);
drag->setHotSpot(QPoint(pixmapSize.width() / 2, pixmapSize.height() / 2));
QList<int> entryCoords = { pressedIndex.row(), pressedIndex.column(), entryIdx };
QString entryStr = QString::fromStdString(std::accumulate(entryCoords.begin(),
entryCoords.end(),
std::string(),
[](const std::string &a, int b) {
return a + (a.length() ? "," : "") + std::to_string(b);
}));
QByteArray entryBytes = entryStr.toUtf8();
mimeData->setData("application/x-day-grid-entry", entryBytes);
drag->setMimeData(mimeData);
drag->exec(Qt::MoveAction);
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
if (item != nullptr) {
item->setData(CalendarCompactDayDelegate::PressedEntryRole, QVariant());
}
}
pressedPos = QPoint();
pressedIndex = QModelIndex();
clearRelated();
}
void
CalendarMonthTable::dragEnterEvent
(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("application/x-day-grid-entry") && event->source() == this) {
event->acceptProposedAction();
}
}
void
CalendarMonthTable::dragMoveEvent
(QDragMoveEvent *event)
{
QModelIndex hoverIndex = indexAt(event->position().toPoint());
if ( hoverIndex.isValid()
&& hoverIndex.column() < 7
&& hoverIndex != pressedIndex) {
QTableWidgetItem *item = this->item(hoverIndex.row(), hoverIndex.column());
if (item != nullptr) {
setCurrentItem(item);
}
event->accept();
} else {
setCurrentItem(nullptr);
event->ignore();
}
}
void
CalendarMonthTable::dragLeaveEvent
(QDragLeaveEvent *event)
{
Q_UNUSED(event)
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
if (item != nullptr) {
setCurrentItem(item);
}
}
}
void
CalendarMonthTable::dropEvent
(QDropEvent *event)
{
if (event->mimeData()->hasFormat("application/x-day-grid-entry")) {
QByteArray entryBytes = event->mimeData()->data("application/x-day-grid-entry");
QString entryStr = QString::fromUtf8(entryBytes);
QStringList entryStrList = entryStr.split(',');
QList<int> entryCoords;
for (const QString &str : entryStrList) {
entryCoords.append(str.toInt());
}
if (entryCoords.size() != 3) {
return;
}
if ( entryCoords[0] >= 0
&& entryCoords[0] < rowCount()
&& entryCoords[1] >= 0
&& entryCoords[1] < 7) {
QModelIndex srcIndex = model()->index(entryCoords[0], entryCoords[1]);
int entryIdx = entryCoords[2];
if (srcIndex.isValid() && entryIdx >= 0) {
CalendarDay srcDay = srcIndex.data(CalendarCompactDayDelegate::DayRole).value<CalendarDay>();
if (entryIdx < srcDay.entries.count()) {
QModelIndex destIndex = indexAt(event->position().toPoint());
CalendarDay destDay = destIndex.data(CalendarCompactDayDelegate::DayRole).value<CalendarDay>();
emit entryMoved(srcDay.entries[entryIdx], srcDay.date, destDay.date, srcDay.entries[entryIdx].start);
}
}
}
}
}
void
CalendarMonthTable::setRelated
(const QString &linkedReference)
{
if (! linkedReference.isEmpty()) {
for (int row = 0; row < rowCount() - 1; ++row) {
for (int col = 0; col < 7; ++col) {
QTableWidgetItem *item = this->item(row, col);
if (item) {
CalendarDay day = item->data(CalendarCompactDayDelegate::DayRole).value<CalendarDay>();
for (int idx = 0; idx < day.entries.count(); ++idx) {
if (day.entries[idx].reference == linkedReference) {
item->setData(CalendarCompactDayDelegate::RelatedEntryRole, idx);
return;
}
}
}
}
}
}
}
void
CalendarMonthTable::clearRelated
()
{
for (int row = 0; row < rowCount() - 1; ++row) {
for (int col = 0; col < 7; ++col) {
QTableWidgetItem *item = this->item(row, col);
if (! item) {
continue;
}
item->setData(CalendarCompactDayDelegate::RelatedEntryRole, QVariant());
}
}
}
void
CalendarMonthTable::showContextMenu
(const QPoint &pos)
{
QModelIndex index = indexAt(pos);
if ( ! index.isValid()
|| index.column() > 6) {
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
if (item != nullptr) {
item->setData(CalendarCompactDayDelegate::PressedEntryRole, QVariant());
}
}
return;
}
QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(index);
CalendarCompactDayDelegate *delegate = static_cast<CalendarCompactDayDelegate*>(abstractDelegate);
if (delegate->moreTester.hitTest(index, pos) != -1) {
return;
}
int entryIdx = delegate->entryTester.hitTest(index, pos);
int headlineEntryIdx = delegate->headlineTester.hitTest(index, pos);
CalendarDay day = index.data(CalendarCompactDayDelegate::DayRole).value<CalendarDay>();
CalendarEntry *entry = nullptr;
if (entryIdx >= 0) {
entry = &day.entries[entryIdx];
} else if (headlineEntryIdx >= 0) {
entry = &day.headlineEntries[headlineEntryIdx];
}
QMenu *contextMenu = buildContextMenu(day, entry, QTime(), isInDateRange(day.date) && canHavePhasesOrEvents);
contextMenu->exec(viewport()->mapToGlobal(pos));
delete contextMenu;
if (pressedIndex.isValid()) {
QTableWidgetItem *item = this->item(pressedIndex.row(), pressedIndex.column());
if (item != nullptr) {
item->setData(CalendarCompactDayDelegate::PressedEntryRole, QVariant());
}
}
clearRelated();
}
//////////////////////////////////////////////////////////////////////////////
// CalendarDayView
CalendarDayView::CalendarDayView
(const QDate &dateInMonth, Measures * const athleteMeasures, QWidget *parent)
: QWidget(parent), athleteMeasures(athleteMeasures)
{
dayDateSelector = new CalendarOverview();
dayDateSelector->setFixedHeight(std::max(static_cast<int>(280 * dpiYFactor), dayDateSelector->sizeHint().height()));
measureTabs = new QTabWidget();
QWidget *dayLeftPane = new QWidget();
QVBoxLayout *leftPaneLayout = new QVBoxLayout(dayLeftPane);
leftPaneLayout->addWidget(dayDateSelector);
leftPaneLayout->addSpacing(10 * dpiYFactor);
leftPaneLayout->addWidget(measureTabs, 1);
dayLeftPane->setFixedWidth(dayDateSelector->sizeHint().width() + leftPaneLayout->contentsMargins().left() + leftPaneLayout->contentsMargins().right());
dayTable = new CalendarDayTable(dateInMonth);
QHBoxLayout *dayLayout = new QHBoxLayout(this);
dayLayout->addWidget(dayLeftPane);
dayLayout->addWidget(dayTable);
connect(dayDateSelector, &QCalendarWidget::selectionChanged, this, [this]() {
if (dayTable->selectedDate() != dayDateSelector->selectedDate()) {
setDay(dayDateSelector->selectedDate());
}
});
connect(dayTable, &CalendarDayTable::dayChanged, this, [this](const QDate &date) {
dayDateSelector->setSelectedDate(date);
emit dayChanged(date);
});
connect(dayTable, &CalendarDayTable::entryMoved, this, &CalendarDayView::entryMoved);
connect(dayTable, &CalendarDayTable::linkActivity, this, &CalendarDayView::linkActivity);
connect(dayTable, &CalendarDayTable::unlinkActivity, this, &CalendarDayView::unlinkActivity);
connect(dayTable, &CalendarDayTable::viewActivity, this, &CalendarDayView::viewActivity);
connect(dayTable, &CalendarDayTable::viewLinkedActivity, this, &CalendarDayView::viewLinkedActivity);
connect(dayTable, &CalendarDayTable::addActivity, this, &CalendarDayView::addActivity);
connect(dayTable, &CalendarDayTable::showInTrainMode, this, &CalendarDayView::showInTrainMode);
connect(dayTable, &CalendarDayTable::filterSimilar, this, &CalendarDayView::filterSimilar);
connect(dayTable, &CalendarDayTable::delActivity, this, &CalendarDayView::delActivity);
connect(dayTable, &CalendarDayTable::saveChanges, this, &CalendarDayView::saveChanges);
connect(dayTable, &CalendarDayTable::discardChanges, this, &CalendarDayView::discardChanges);
connect(dayTable, &CalendarDayTable::repeatSchedule, this, &CalendarDayView::repeatSchedule);
connect(dayTable, &CalendarDayTable::insertRestday, this, &CalendarDayView::insertRestday);
connect(dayTable, &CalendarDayTable::delRestday, this, &CalendarDayView::delRestday);
connect(dayTable, &CalendarDayTable::addPhase, this, &CalendarDayView::addPhase);
connect(dayTable, &CalendarDayTable::editPhase, this, &CalendarDayView::editPhase);
connect(dayTable, &CalendarDayTable::delPhase, this, &CalendarDayView::delPhase);
connect(dayTable, &CalendarDayTable::addEvent, this, &CalendarDayView::addEvent);
connect(dayTable, &CalendarDayTable::editEvent, this, &CalendarDayView::editEvent);
connect(dayTable, &CalendarDayTable::delEvent, this, &CalendarDayView::delEvent);
}
bool
CalendarDayView::setDay
(const QDate &date)
{
updateMeasures(date);
return dayTable->setDay(date);
}
void
CalendarDayView::setFirstDayOfWeek
(Qt::DayOfWeek firstDayOfWeek)
{
dayDateSelector->setFirstDayOfWeek(firstDayOfWeek);
}
void
CalendarDayView::setStartHour
(int hour)
{
dayTable->setStartHour(hour);
}
void
CalendarDayView::setEndHour
(int hour)
{
dayTable->setEndHour(hour);
}
void
CalendarDayView::setSummaryVisible
(bool visible)
{
dayTable->setRowHidden(2, ! visible);
}
void
CalendarDayView::fillEntries
(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries)
{
dayDateSelector->fillEntries(activityEntries, headlineEntries);
dayTable->fillEntries(activityEntries, summaries, headlineEntries);
}
void
CalendarDayView::limitDateRange
(const DateRange &dr, bool canHavePhasesOrEvents)
{
dayDateSelector->limitDateRange(dr);
dayTable->limitDateRange(dr, canHavePhasesOrEvents);
}
QDate
CalendarDayView::firstVisibleDay
() const
{
return dayDateSelector->firstVisibleDay();
}
QDate
CalendarDayView::lastVisibleDay
() const
{
return dayDateSelector->lastVisibleDay();
}
QDate
CalendarDayView::selectedDate
() const
{
return dayTable->selectedDate();
}
void
CalendarDayView::updateMeasures
()
{
updateMeasures(selectedDate());
}
void
CalendarDayView::updateMeasures
(const QDate &date)
{
int currentIndex = measureTabs->currentIndex();
while (measureTabs->count() > 0) {
QWidget *page = measureTabs->widget(0);
measureTabs->removeTab(0);
delete page;
}
bool metricUnits = GlobalContext::context()->useMetricUnits;
for (MeasuresGroup * const measuresGroup : athleteMeasures->getGroups()) {
QWidget *measureWidget = new QWidget();
Measure measure;
measuresGroup->getMeasure(date, measure);
QVBoxLayout *measureLayout = new QVBoxLayout();
int buttonType = 0;
if (measure.when.isValid()) {
QFormLayout *form = newQFormLayout();
for (int i = 0; i < measuresGroup->getFieldNames().count(); ++i) {
QString measureText;
double fieldValue = measuresGroup->getFieldValue(date, i, metricUnits);
if (fieldValue > 0) {
if (measuresGroup->getFieldUnits(i, metricUnits).size() > 0) {
measureText = QString("%1 %2").arg(fieldValue).arg(measuresGroup->getFieldUnits(i, metricUnits));
} else {
measureText = QString("%1").arg(fieldValue);
}
form->addRow(measuresGroup->getFieldNames()[i], new QLabel(measureText));
}
}
if (! measure.comment.isEmpty()) {
QTextEdit *commentField = new QTextEdit();
commentField->setAcceptRichText(false);
commentField->setReadOnly(true);
commentField->setText(measure.comment);
commentField->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
form->addRow(commentField);
}
QLocale locale;
QString validText = locale.toString(measure.when, QLocale::ShortFormat);
int validDays = measure.when.date().daysTo(date);
if (validDays > 1) {
validText.append(tr("\n(%1 days earlier)").arg(validDays));
} else if (validDays > 0) {
validText.append(tr("\n(%1 day earlier)").arg(validDays));
}
form->addRow(tr("Valid since"), new QLabel(validText));
QWidget *scrollWidget = new QWidget();
scrollWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
scrollWidget->setLayout(form);
QScrollArea *scrollArea = new QScrollArea();
scrollArea->setWidget(scrollWidget);
scrollArea->setWidgetResizable(true);
measureLayout->addWidget(scrollArea);
if (validDays == 0) {
buttonType = 1;
}
} else {
QLabel *noMeasureLabel = new QLabel(tr("No measure available"));
noMeasureLabel->setAlignment(Qt::AlignCenter);
measureLayout->addStretch();
measureLayout->addWidget(noMeasureLabel, Qt::AlignCenter);
measureLayout->addStretch();
}
if (buttonType == 0) {
QPushButton *addButton = new QPushButton(tr("Add Measure"));
connect(addButton, &QPushButton::clicked, this, [this, date, measuresGroup]() {
if (measureDialog(QDateTime(date, QTime::currentTime()), measuresGroup, false)) {
QTimer::singleShot(0, this, [this, date]() {
updateMeasures(date);
});
}
});
measureLayout->addWidget(addButton);
} else {
QPushButton *editButton = new QPushButton(tr("Edit Measure"));
connect(editButton, &QPushButton::clicked, this, [this, date, measure, measuresGroup]() {
if (measureDialog(measure.when, measuresGroup, true)) {
QTimer::singleShot(0, this, [this, date]() {
updateMeasures(date);
});
}
});
measureLayout->addWidget(editButton);
}
measureWidget->setLayout(measureLayout);
measureTabs->addTab(measureWidget, measuresGroup->getName());
}
if (measureTabs->count() > currentIndex) {
measureTabs->setCurrentIndex(currentIndex);
}
PaletteApplier::setPaletteRecursively(measureTabs, this->palette(), true);
}
bool
CalendarDayView::measureDialog
(const QDateTime &when, MeasuresGroup * const measuresGroup, bool update)
{
QDialog dialog;
dialog.setWindowTitle(update ? tr("Edit Measure") : tr("Add Measure"));
QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Apply
| QDialogButtonBox::Discard);
connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), &dialog, SLOT(accept()));
connect(buttonBox->button(QDialogButtonBox::Discard), SIGNAL(clicked()), &dialog, SLOT(reject()));
QFormLayout *form = newQFormLayout(&dialog);
QLocale locale;
form->addRow(tr("Start Date"), new QLabel(locale.toString(when, QLocale::ShortFormat)));
QList<double> unitsFactors = measuresGroup->getFieldUnitsFactors();
QStringList fieldNames = measuresGroup->getFieldNames();
QList<QLabel*> valuesLabel;
QList<QDoubleSpinBox*> valuesEdit;
bool metricUnits = GlobalContext::context()->useMetricUnits;
int i = 0;
for (QString &fieldName : fieldNames) {
valuesLabel << new QLabel(fieldName);
valuesEdit << new QDoubleSpinBox(this);
valuesEdit[i]->setMaximum(9999.99);
valuesEdit[i]->setMinimum(0.0);
valuesEdit[i]->setDecimals(2);
valuesEdit[i]->setValue(measuresGroup->getFieldValue(when.date(), i, metricUnits));
valuesEdit[i]->setSuffix(QString(" %1").arg(measuresGroup->getFieldUnits(i, metricUnits)));
form->addRow(valuesLabel[i], valuesEdit[i]);
++i;
}
Measure measure;
measuresGroup->getMeasure(when.date(), measure);
QTextEdit *commentEdit = new QTextEdit(measure.comment);
commentEdit->setAcceptRichText(false);
form->addRow(tr("Comment"), commentEdit);
form->addRow(buttonBox);
int dialogRet = dialog.exec();
if (dialogRet != QDialog::Accepted) {
return false;
}
for (i = 0; i < valuesEdit.count(); ++i) {
measure.values[i] = valuesEdit[i]->value();
}
measure.when = when;
measure.comment = commentEdit->toPlainText();
QList<Measure> measures = measuresGroup->measures();
bool found = false;
for (int measureIdx = 0; measureIdx < measures.count(); ++measureIdx) {
if (measures[measureIdx].when == measure.when) {
measures[measureIdx] = measure;
found = true;
break;
}
}
if (! found) {
measures << measure;
}
std::reverse(measures.begin(), measures.end());
measuresGroup->setMeasures(measures);
measuresGroup->write();
return true;
}
//////////////////////////////////////////////////////////////////////////////
// CalendarWeekView
CalendarWeekView::CalendarWeekView
(const QDate &date, QWidget *parent)
: QWidget(parent)
{
weekTable = new CalendarDayTable(date, CalendarDayTableType::Week);
QHBoxLayout *weekLayout = new QHBoxLayout(this);
weekLayout->addWidget(weekTable);
connect(weekTable, &CalendarDayTable::entryMoved, this, &CalendarWeekView::entryMoved);
connect(weekTable, &CalendarDayTable::dayChanged, this, &CalendarWeekView::dayChanged);
connect(weekTable, &CalendarDayTable::linkActivity, this, &CalendarWeekView::linkActivity);
connect(weekTable, &CalendarDayTable::unlinkActivity, this, &CalendarWeekView::unlinkActivity);
connect(weekTable, &CalendarDayTable::viewActivity, this, &CalendarWeekView::viewActivity);
connect(weekTable, &CalendarDayTable::viewLinkedActivity, this, &CalendarWeekView::viewLinkedActivity);
connect(weekTable, &CalendarDayTable::addActivity, this, &CalendarWeekView::addActivity);
connect(weekTable, &CalendarDayTable::showInTrainMode, this, &CalendarWeekView::showInTrainMode);
connect(weekTable, &CalendarDayTable::filterSimilar, this, &CalendarWeekView::filterSimilar);
connect(weekTable, &CalendarDayTable::delActivity, this, &CalendarWeekView::delActivity);
connect(weekTable, &CalendarDayTable::saveChanges, this, &CalendarWeekView::saveChanges);
connect(weekTable, &CalendarDayTable::discardChanges, this, &CalendarWeekView::discardChanges);
connect(weekTable, &CalendarDayTable::repeatSchedule, this, &CalendarWeekView::repeatSchedule);
connect(weekTable, &CalendarDayTable::insertRestday, this, &CalendarWeekView::insertRestday);
connect(weekTable, &CalendarDayTable::delRestday, this, &CalendarWeekView::delRestday);
connect(weekTable, &CalendarDayTable::addPhase, this, &CalendarWeekView::addPhase);
connect(weekTable, &CalendarDayTable::editPhase, this, &CalendarWeekView::editPhase);
connect(weekTable, &CalendarDayTable::delPhase, this, &CalendarWeekView::delPhase);
connect(weekTable, &CalendarDayTable::addEvent, this, &CalendarWeekView::addEvent);
connect(weekTable, &CalendarDayTable::editEvent, this, &CalendarWeekView::editEvent);
connect(weekTable, &CalendarDayTable::delEvent, this, &CalendarWeekView::delEvent);
setDay(date);
}
bool
CalendarWeekView::setDay
(const QDate &date)
{
return weekTable->setDay(date);
}
void
CalendarWeekView::setFirstDayOfWeek
(Qt::DayOfWeek firstDayOfWeek)
{
weekTable->setFirstDayOfWeek(firstDayOfWeek);
}
void
CalendarWeekView::setStartHour
(int hour)
{
weekTable->setStartHour(hour);
}
void
CalendarWeekView::setEndHour
(int hour)
{
weekTable->setEndHour(hour);
}
void
CalendarWeekView::setSummaryVisible
(bool visible)
{
weekTable->setRowHidden(2, ! visible);
}
void
CalendarWeekView::fillEntries
(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries)
{
weekTable->fillEntries(activityEntries, summaries, headlineEntries);
}
void
CalendarWeekView::limitDateRange
(const DateRange &dr, bool canHavePhasesOrEvents)
{
weekTable->limitDateRange(dr, canHavePhasesOrEvents);
}
QDate
CalendarWeekView::firstVisibleDay
() const
{
return weekTable->firstVisibleDay();
}
QDate
CalendarWeekView::firstVisibleDay
(const QDate &date) const
{
return weekTable->firstVisibleDay(date);
}
QDate
CalendarWeekView::lastVisibleDay
() const
{
return weekTable->lastVisibleDay();
}
QDate
CalendarWeekView::lastVisibleDay
(const QDate &date) const
{
return weekTable->lastVisibleDay(date);
}
QDate
CalendarWeekView::selectedDate
() const
{
return weekTable->selectedDate();
}
//////////////////////////////////////////////////////////////////////////////
// Calendar
Calendar::Calendar
(const QDate &dateInMonth, Qt::DayOfWeek firstDayOfWeek, Measures * const athleteMeasures, QWidget *parent)
: QWidget(parent)
{
qRegisterMetaType<CalendarDay>("CalendarDay");
qRegisterMetaType<CalendarSummary>("CalendarSummary");
dayView = new CalendarDayView(dateInMonth, athleteMeasures);
weekView = new CalendarWeekView(dateInMonth);
monthView = new CalendarMonthTable(dateInMonth, firstDayOfWeek);
viewStack = new QStackedWidget();
viewStack->addWidget(dayView);
viewStack->addWidget(weekView);
viewStack->addWidget(monthView);
toolbar = new QToolBar();
prevAction = toolbar->addAction(tr("Previous Month"));
nextAction = toolbar->addAction(tr("Next Month"));
todayAction = toolbar->addAction(tr("Today"));
separator = toolbar->addSeparator();
dateNavigator = new QToolButton();
dateNavigator->setPopupMode(QToolButton::InstantPopup);
dateNavigatorAction = toolbar->addWidget(dateNavigator);
dateMenu = new QMenu(this);
connect(dateMenu, &QMenu::aboutToShow, this, &Calendar::populateDateMenu);
dateNavigator->setMenu(dateMenu);
seasonLabel = new QLabel();
seasonLabelAction = toolbar->addWidget(seasonLabel);
QWidget *spacer = new QWidget(toolbar);
spacer->setFixedWidth(10 * dpiXFactor);
spacer->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
filterSpacerAction = toolbar->addWidget(spacer);
filterLabel = new QLabel("<i>" + tr("Filters applied") + "</i>");
filterLabelAction = toolbar->addWidget(filterLabel);
QWidget *stretch = new QWidget();
stretch->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
toolbar->addWidget(stretch);
QActionGroup *viewGroup = new QActionGroup(toolbar);
dayAction = toolbar->addAction(tr("Day"));
dayAction->setCheckable(true);
dayAction->setActionGroup(viewGroup);
connect(dayAction, &QAction::triggered, this, [this]() { setView(CalendarView::Day); });
weekAction = toolbar->addAction(tr("Week"));
weekAction->setCheckable(true);
weekAction->setActionGroup(viewGroup);
connect(weekAction, &QAction::triggered, this, [this]() { setView(CalendarView::Week); });
monthAction = toolbar->addAction(tr("Month"));
monthAction->setCheckable(true);
monthAction->setActionGroup(viewGroup);
connect(monthAction, &QAction::triggered, this, [this]() { setView(CalendarView::Month); });
applyNavIcons();
connect(dayView, &CalendarDayView::dayChanged, this, [this](const QDate &date) {
if (currentView() == CalendarView::Day) {
emit dayChanged(date);
updateHeader();
setNavButtonState();
}
});
connect(dayView, &CalendarDayView::entryMoved, this, &Calendar::moveActivity);
connect(dayView, &CalendarDayView::linkActivity, this, &Calendar::linkActivity);
connect(dayView, &CalendarDayView::unlinkActivity, this, &Calendar::unlinkActivity);
connect(dayView, &CalendarDayView::viewActivity, this, &Calendar::viewActivity);
connect(dayView, &CalendarDayView::viewLinkedActivity, this, &Calendar::viewLinkedActivity);
connect(dayView, &CalendarDayView::addActivity, this, &Calendar::addActivity);
connect(dayView, &CalendarDayView::showInTrainMode, this, &Calendar::showInTrainMode);
connect(dayView, &CalendarDayView::filterSimilar, this, &Calendar::filterSimilar);
connect(dayView, &CalendarDayView::delActivity, this, &Calendar::delActivity);
connect(dayView, &CalendarDayView::saveChanges, this, &Calendar::saveChanges);
connect(dayView, &CalendarDayView::discardChanges, this, &Calendar::discardChanges);
connect(dayView, &CalendarDayView::repeatSchedule, this, &Calendar::repeatSchedule);
connect(dayView, &CalendarDayView::insertRestday, this, &Calendar::insertRestday);
connect(dayView, &CalendarDayView::delRestday, this, &Calendar::delRestday);
connect(dayView, &CalendarDayView::addPhase, this, &Calendar::addPhase);
connect(dayView, &CalendarDayView::editPhase, this, &Calendar::editPhase);
connect(dayView, &CalendarDayView::delPhase, this, &Calendar::delPhase);
connect(dayView, &CalendarDayView::addEvent, this, &Calendar::addEvent);
connect(dayView, &CalendarDayView::editEvent, this, &Calendar::editEvent);
connect(dayView, &CalendarDayView::delEvent, this, &Calendar::delEvent);
connect(weekView, &CalendarWeekView::dayChanged, this, [this](const QDate &date) {
if (currentView() == CalendarView::Week) {
emit dayChanged(date);
updateHeader();
setNavButtonState();
}
});
connect(weekView, &CalendarWeekView::entryMoved, this, &Calendar::moveActivity);
connect(weekView, &CalendarWeekView::linkActivity, this, &Calendar::linkActivity);
connect(weekView, &CalendarWeekView::unlinkActivity, this, &Calendar::unlinkActivity);
connect(weekView, &CalendarWeekView::viewActivity, this, &Calendar::viewActivity);
connect(weekView, &CalendarWeekView::viewLinkedActivity, this, &Calendar::viewLinkedActivity);
connect(weekView, &CalendarWeekView::addActivity, this, &Calendar::addActivity);
connect(weekView, &CalendarWeekView::showInTrainMode, this, &Calendar::showInTrainMode);
connect(weekView, &CalendarWeekView::filterSimilar, this, &Calendar::filterSimilar);
connect(weekView, &CalendarWeekView::delActivity, this, &Calendar::delActivity);
connect(weekView, &CalendarWeekView::saveChanges, this, &Calendar::saveChanges);
connect(weekView, &CalendarWeekView::discardChanges, this, &Calendar::discardChanges);
connect(weekView, &CalendarWeekView::repeatSchedule, this, &Calendar::repeatSchedule);
connect(weekView, &CalendarWeekView::insertRestday, this, &Calendar::insertRestday);
connect(weekView, &CalendarWeekView::delRestday, this, &Calendar::delRestday);
connect(weekView, &CalendarWeekView::addPhase, this, &Calendar::addPhase);
connect(weekView, &CalendarWeekView::editPhase, this, &Calendar::editPhase);
connect(weekView, &CalendarWeekView::delPhase, this, &Calendar::delPhase);
connect(weekView, &CalendarWeekView::addEvent, this, &Calendar::addEvent);
connect(weekView, &CalendarWeekView::editEvent, this, &Calendar::editEvent);
connect(weekView, &CalendarWeekView::delEvent, this, &Calendar::delEvent);
connect(monthView, &CalendarMonthTable::entryDblClicked, this, [this](const CalendarDay &day, int entryIdx) {
viewActivity(day.entries[entryIdx]);
});
connect(monthView, &CalendarMonthTable::daySelected, this, [this](const CalendarDay &day) {
emit daySelected(day.date);
});
connect(monthView, &CalendarMonthTable::moreClicked, this, [this]() {
setView(CalendarView::Day);
});
connect(monthView, &CalendarMonthTable::dayDblClicked, this, [this]() {
setView(CalendarView::Day);
});
connect(monthView, &CalendarMonthTable::showInTrainMode, this, &Calendar::showInTrainMode);
connect(monthView, &CalendarMonthTable::filterSimilar, this, &Calendar::filterSimilar);
connect(monthView, &CalendarMonthTable::linkActivity, this, &Calendar::linkActivity);
connect(monthView, &CalendarMonthTable::unlinkActivity, this, &Calendar::unlinkActivity);
connect(monthView, &CalendarMonthTable::viewActivity, this, &Calendar::viewActivity);
connect(monthView, &CalendarMonthTable::viewLinkedActivity, this, &Calendar::viewLinkedActivity);
connect(monthView, &CalendarMonthTable::addActivity, this, &Calendar::addActivity);
connect(monthView, &CalendarMonthTable::repeatSchedule, this, &Calendar::repeatSchedule);
connect(monthView, &CalendarMonthTable::insertRestday, this, &Calendar::insertRestday);
connect(monthView, &CalendarMonthTable::delRestday, this, &Calendar::delRestday);
connect(monthView, &CalendarMonthTable::delActivity, this, &Calendar::delActivity);
connect(monthView, &CalendarMonthTable::saveChanges, this, &Calendar::saveChanges);
connect(monthView, &CalendarMonthTable::discardChanges, this, &Calendar::discardChanges);
connect(monthView, &CalendarMonthTable::entryMoved, this, &Calendar::moveActivity);
connect(monthView, &CalendarMonthTable::addPhase, this, &Calendar::addPhase);
connect(monthView, &CalendarMonthTable::editPhase, this, &Calendar::editPhase);
connect(monthView, &CalendarMonthTable::delPhase, this, &Calendar::delPhase);
connect(monthView, &CalendarMonthTable::addEvent, this, &Calendar::addEvent);
connect(monthView, &CalendarMonthTable::editEvent, this, &Calendar::editEvent);
connect(monthView, &CalendarMonthTable::delEvent, this, &Calendar::delEvent);
connect(monthView, &CalendarMonthTable::monthChanged, this, [this](const QDate &month, const QDate &firstVisible, const QDate &lastVisible) {
if (currentView() == CalendarView::Month) {
emit monthChanged(month, firstVisible, lastVisible);
updateHeader();
setNavButtonState();
}
});
connect(prevAction, &QAction::triggered, this, [this]() { goNext(-1); });
connect(nextAction, &QAction::triggered, this, [this]() { goNext(1); });
connect(todayAction, &QAction::triggered, this, [this]() { setDate(QDate::currentDate()); });
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(toolbar);
mainLayout->addWidget(viewStack);
setView(CalendarView::Month);
setDate(dateInMonth);
}
void
Calendar::setDate
(const QDate &date, bool allowKeepMonth)
{
if (currentView() == CalendarView::Day) {
if (isInDateRange(date)) {
dayView->setDay(date);
}
} else if (currentView() == CalendarView::Week) {
if (isInDateRange(date)) {
weekView->setDay(date);
}
} else if (currentView() == CalendarView::Month) {
if (monthView->isInDateRange(date)) {
monthView->setMonth(date, allowKeepMonth);
}
}
}
void
Calendar::fillEntries
(const QHash<QDate, QList<CalendarEntry>> &activityEntries, const QList<CalendarSummary> &summaries, const QHash<QDate, QList<CalendarEntry>> &headlineEntries, bool isFiltered)
{
if (currentView() == CalendarView::Day) {
dayView->fillEntries(activityEntries, summaries, headlineEntries);
} else if (currentView() == CalendarView::Week) {
weekView->fillEntries(activityEntries, summaries, headlineEntries);
} else if (currentView() == CalendarView::Month) {
monthView->fillEntries(activityEntries, summaries, headlineEntries);
}
filterSpacerAction->setVisible(isFiltered);
filterLabelAction->setVisible(isFiltered);
}
QDate
Calendar::firstOfCurrentMonth
() const
{
return monthView->firstOfCurrentMonth();
}
QDate
Calendar::firstVisibleDay
() const
{
if (currentView() == CalendarView::Day) {
return dayView->firstVisibleDay();
} else if (currentView() == CalendarView::Week) {
return weekView->firstVisibleDay();
} else if (currentView() == CalendarView::Month) {
return monthView->firstVisibleDay();
}
return QDate();
}
QDate
Calendar::lastVisibleDay
() const
{
if (currentView() == CalendarView::Day) {
return dayView->lastVisibleDay();
} else if (currentView() == CalendarView::Week) {
return weekView->lastVisibleDay();
} else if (currentView() == CalendarView::Month) {
return monthView->lastVisibleDay();
}
return QDate();
}
QDate
Calendar::selectedDate
() const
{
if (currentView() == CalendarView::Day) {
return dayView->selectedDate();
} else if (currentView() == CalendarView::Week) {
return weekView->selectedDate();
} else if (currentView() == CalendarView::Month) {
return monthView->selectedDate();
}
return QDate();
}
CalendarView
Calendar::currentView
() const
{
return static_cast<CalendarView>(viewStack->currentIndex());
}
bool
Calendar::goNext
(int amount)
{
bool ret = true;
if (currentView() == CalendarView::Day) {
if ((ret = canGoNext(amount))) {
setDate(dayView->selectedDate().addDays(amount));
}
} else if (currentView() == CalendarView::Week) {
QDate newDate = selectedDate().addDays(7 * amount);
if ((ret = newDate.isValid())) {
setDate(newDate);
}
} else if (currentView() == CalendarView::Month) {
QDate newDate = fitToMonth(selectedDate().addMonths(amount), true);
if ((ret = newDate.isValid())) {
setDate(newDate);
}
}
return ret;
}
QDate
Calendar::fitToMonth
(const QDate &date, bool preferToday) const
{
QDate newDate(date);
QDate today = QDate::currentDate();
if (! newDate.isValid()) {
newDate = today;
}
if ( preferToday
&& newDate.year() == today.year()
&& newDate.month() == today.month()
&& isInDateRange(today)) {
newDate = today;
} else if (! isInDateRange(newDate)) {
if (newDate < dateRange.to) {
newDate = QDate(newDate.year(), newDate.month(), newDate.daysInMonth());
} else {
newDate = QDate(newDate.year(), newDate.month(), 1);
}
}
return isInDateRange(newDate) ? newDate : QDate();
}
bool
Calendar::canGoNext
(int amount) const
{
if (currentView() == CalendarView::Day) {
return isInDateRange(dayView->selectedDate().addDays(amount));
} else if (currentView() == CalendarView::Week) {
return isInDateRange(weekView->selectedDate().addDays(7 * amount));
} else if (currentView() == CalendarView::Month) {
QDate fom = monthView->firstOfCurrentMonth();
QDate lom(fom.year(), fom.month(), fom.daysInMonth());
fom = fom.addMonths(amount);
lom = lom.addMonths(amount);
return isInDateRange(fom) || isInDateRange(lom);
}
return false;
}
int
Calendar::weekNumber
(const QDate &date) const
{
int isoWeekNumber = date.weekNumber();
int dayOfWeek = date.dayOfWeek();
int offset = (dayOfWeek - firstDayOfWeek + 7) % 7;
if (offset != 0) {
isoWeekNumber++;
}
return isoWeekNumber;
}
bool
Calendar::isInDateRange
(const QDate &date) const
{
return date.isValid() && dateRange.pass(date);
}
void
Calendar::activateDateRange
(const DateRange &dr, bool allowKeepMonth, bool canHavePhasesOrEvents)
{
QDate currentDate = selectedDate();
dateRange = dr;
monthView->limitDateRange(dr, allowKeepMonth, canHavePhasesOrEvents);
weekView->limitDateRange(dr, canHavePhasesOrEvents);
dayView->limitDateRange(dr, canHavePhasesOrEvents);
if (currentView() == CalendarView::Day || currentView() == CalendarView::Week) {
setDate(currentDate, false);
} else if (currentView() == CalendarView::Month) {
setDate(fitToMonth(currentDate, false), true);
}
seasonLabel->setText(tr("Season: %1").arg(dateRange.name));
emit dateRangeActivated(dr.name);
}
void
Calendar::setFirstDayOfWeek
(Qt::DayOfWeek firstDayOfWeek)
{
QDate currentDate = selectedDate();
this->firstDayOfWeek = firstDayOfWeek;
monthView->setFirstDayOfWeek(firstDayOfWeek);
weekView->setFirstDayOfWeek(firstDayOfWeek);
dayView->setFirstDayOfWeek(firstDayOfWeek);
if (currentView() == CalendarView::Week) {
setDate(currentDate, false);
} else if (currentView() == CalendarView::Month) {
setDate(fitToMonth(currentDate, false), true);
}
}
void
Calendar::setStartHour
(int hour)
{
weekView->setStartHour(hour);
dayView->setStartHour(hour);
}
void
Calendar::setEndHour
(int hour)
{
weekView->setEndHour(hour);
dayView->setEndHour(hour);
}
void
Calendar::setSummaryDayVisible
(bool visible)
{
dayView->setSummaryVisible(visible);
}
void
Calendar::setSummaryWeekVisible
(bool visible)
{
weekView->setSummaryVisible(visible);
}
void
Calendar::setSummaryMonthVisible
(bool visible)
{
monthView->setColumnHidden(7, ! visible);
}
void
Calendar::setNavButtonState
()
{
prevAction->setEnabled(canGoNext(-1));
nextAction->setEnabled(canGoNext(1));
todayAction->setEnabled(isInDateRange(QDate::currentDate()));
}
void
Calendar::updateHeader
()
{
QLocale locale;
if (currentView() == CalendarView::Day) {
dateNavigator->setText(locale.toString(dayView->selectedDate(), QLocale::LongFormat));
prevAction->setVisible(true);
nextAction->setVisible(true);
todayAction->setVisible(true);
separator->setVisible(true);
dateNavigatorAction->setVisible(true);
seasonLabelAction->setVisible(false);
} else if (currentView() == CalendarView::Week) {
dateNavigator->setText(tr("Week %1 (%2 - %3)")
.arg(weekNumber(weekView->selectedDate()))
.arg(locale.toString(weekView->firstVisibleDay(), QLocale::ShortFormat))
.arg(locale.toString(weekView->lastVisibleDay(), QLocale::ShortFormat)));
prevAction->setVisible(true);
nextAction->setVisible(true);
todayAction->setVisible(true);
separator->setVisible(true);
dateNavigatorAction->setVisible(true);
seasonLabelAction->setVisible(false);
} else if (currentView() == CalendarView::Month) {
dateNavigator->setText(locale.toString(monthView->firstOfCurrentMonth(), "MMMM yyyy"));
prevAction->setVisible(true);
nextAction->setVisible(true);
todayAction->setVisible(true);
separator->setVisible(true);
dateNavigatorAction->setVisible(true);
seasonLabelAction->setVisible(false);
}
}
void
Calendar::applyNavIcons
()
{
double scale = appsettings->value(this, GC_FONT_SCALE, 1.0).toDouble();
QFont font;
font.setPointSize(font.pointSizeF() * scale * 1.3);
font.setWeight(QFont::Bold);
dateNavigator->setFont(font);
seasonLabel->setFont(font);
QString mode = GCColor::isPaletteDark(palette()) ? "dark" : "light";
toolbar->setMinimumHeight(dateNavigator->sizeHint().height() + 12 * dpiYFactor);
prevAction->setIcon(QIcon(QString(":images/breeze/%1/go-previous.svg").arg(mode)));
nextAction->setIcon(QIcon(QString(":images/breeze/%1/go-next.svg").arg(mode)));
}
void
Calendar::updateMeasures
()
{
if (currentView() == CalendarView::Day) {
dayView->updateMeasures();
}
}
void
Calendar::setView
(CalendarView view)
{
int idx = static_cast<int>(view);
int oldIdx = viewStack->currentIndex();
if (idx != oldIdx) {
QDate useDate = selectedDate();
if (view == CalendarView::Day) {
dayAction->setChecked(true);
dayView->setDay(useDate);
} else if (view == CalendarView::Week) {
weekAction->setChecked(true);
weekView->setDay(useDate);
} else if (view == CalendarView::Month) {
monthAction->setChecked(true);
monthView->setMonth(fitToMonth(selectedDate(), false), true);
}
viewStack->setCurrentIndex(idx);
emit viewChanged(view, static_cast<CalendarView>(oldIdx));
updateHeader();
setNavButtonState();
}
}
void
Calendar::populateDateMenu
()
{
dateMenu->clear();
dateMenu->addSection(tr("Season: %1").arg(dateRange.name));
dateMenu->setEnabled(true);
if (currentView() == CalendarView::Day || currentView() == CalendarView::Week) {
int currentYear = selectedDate().year();
int currentMonth = selectedDate().month();
int firstMonth = 1;
int lastMonth = 12;
if (dateRange.from.isValid() && dateRange.from.year() == currentYear) {
firstMonth = dateRange.from.month();
}
if (dateRange.to.isValid() && dateRange.to.year() == currentYear) {
lastMonth = dateRange.to.month();
}
QDate firstDate(currentYear, firstMonth, 1);
QDate lastDate(currentYear, lastMonth, 1);
QLocale locale;
for (QDate date = firstDate; date <= lastDate; date = date.addMonths(1)) {
QAction *action = dateMenu->addAction(locale.toString(date, "MMMM yyyy"));
if (currentMonth == date.month()) {
action->setEnabled(false);
} else {
QDate actualDate = date;
while (! dateRange.pass(actualDate)) {
actualDate = actualDate.addDays(1);
if (actualDate.month() != date.month()) {
break;
}
}
if (actualDate.month() == date.month()) {
connect(action, &QAction::triggered, this, [this, actualDate]() {
setDate(actualDate);
});
}
}
}
} else if (currentView() == CalendarView::Month) {
int yearFrom = dateRange.from.isValid() ? dateRange.from.year() : 2020;
int yearTo = dateRange.to.isValid() ? dateRange.to.year() : 2030;
for (int year = yearFrom; year <= yearTo; ++year) {
QAction *action = dateMenu->addAction(QString::number(year));
if (year == selectedDate().year()) {
action->setEnabled(false);
} else {
QDate date(year, 1, 1);
if (! dateRange.pass(date)) {
if (date.year() == dateRange.from.year()) {
date = dateRange.from;
} else {
date = dateRange.to;
}
}
connect(action, &QAction::triggered, this, [this, date]() {
setDate(date);
});
}
}
}
}