/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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> &activityEntries, const QHash> &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 &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 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> &activityEntries, const QList &summaries, const QHash> &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(); 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(); 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 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(itemDelegateForRow(row)); CalendarDetailedDayDelegate *delegate = static_cast(delegatingDelegate->getDelegate(column)); CalendarDay day = index.data(CalendarDetailedDayDelegate::DayRole).value(); 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(itemDelegateForRow(pressedIndex.row())); CalendarDetailedDayDelegate *delegate = static_cast(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(); 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(); ColumnDelegatingItemDelegate *delegatingDelegate = static_cast(itemDelegateForRow(pressedIndex.row())); CalendarDetailedDayDelegate *delegate = static_cast(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(itemDelegateForRow(row)); CalendarDetailedDayDelegate *delegate = static_cast(delegatingDelegate->getDelegate(column)); int entryIdx = delegate->entryTester.hitTest(pressedIndex, pressedPos); CalendarEntry calEntry = pressedIndex.data(CalendarDetailedDayDelegate::DayRole).value().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 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(); 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 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(); 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(); 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(block)); } QMenu* CalendarDayTable::makeHeaderMenu (const QModelIndex &index, const QPoint &pos) { CalendarHeadlineDelegate *delegate = static_cast(itemDelegateForRow(index.row())); int entryIdx = delegate->headlineTester.hitTest(index, pos); CalendarDay day = index.data(CalendarHeadlineDelegate::DayRole).value(); 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(itemDelegateForRow(index.row())); CalendarDetailedDayDelegate *delegate = static_cast(delegatingDelegate->getDelegate(index.column())); int entryIdx = delegate->entryTester.hitTest(index, pos); CalendarDay day = index.data(CalendarDetailedDayDelegate::DayRole).value(); 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(); 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 selection = selectedItems(); if (selection.count() > 0) { QTableWidgetItem *item = selection[0]; if (item->column() < 7) { emit daySelected(item->data(CalendarCompactDayDelegate::DayRole).value()); } } }); 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> &activityEntries, const QList &summaries, const QHash> &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(); 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(abstractDelegate); CalendarDay day = index.data(CalendarCompactDayDelegate::DayRole).value(); 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(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(); 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(); QAbstractItemDelegate *abstractDelegate = itemDelegateForIndex(releasedIndex); CalendarCompactDayDelegate *delegate = static_cast(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(abstractDelegate); int entryIdx = delegate->entryTester.hitTest(pressedIndex, pressedPos); CalendarEntry calEntry = pressedIndex.data(CalendarCompactDayDelegate::DayRole).value().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 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 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(); if (entryIdx < srcDay.entries.count()) { QModelIndex destIndex = indexAt(event->position().toPoint()); CalendarDay destDay = destIndex.data(CalendarCompactDayDelegate::DayRole).value(); 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(); 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(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(); 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(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> &activityEntries, const QList &summaries, const QHash> &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 unitsFactors = measuresGroup->getFieldUnitsFactors(); QStringList fieldNames = measuresGroup->getFieldNames(); QList valuesLabel; QList 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 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> &activityEntries, const QList &summaries, const QHash> &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"); qRegisterMetaType("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("" + tr("Filters applied") + ""); 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> &activityEntries, const QList &summaries, const QHash> &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(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(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(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); }); } } } }