diff --git a/src/Charts/CalendarWindow.cpp b/src/Charts/CalendarWindow.cpp index e508fe732..5ee2dd6c2 100644 --- a/src/Charts/CalendarWindow.cpp +++ b/src/Charts/CalendarWindow.cpp @@ -359,6 +359,8 @@ CalendarWindow::CalendarWindow(Context *context) this->context->mainWindow->selectAnalysis(); } }); + connect(calendar, &Calendar::copyPlannedActivity, this, &CalendarWindow::copyPlannedActivity); + connect(calendar, &Calendar::pastePlannedActivity, this, &CalendarWindow::pastePlannedActivity); connect(calendar, &Calendar::addActivity, this, [this](bool plan, const QDate &day, const QTime &time) { this->context->tab->setNoSwitch(true); ManualActivityWizard wizard(this->context, plan, QDateTime(day, time)); @@ -1190,6 +1192,142 @@ CalendarWindow::getPrimary } +QTime +CalendarWindow::findFreeSlot +(RideItem *sourceItem, QDate newDate, QTime targetTime) +{ + if (sourceItem == nullptr) { + return QTime(); + } + QList> busySlots; + busySlots.append(std::make_pair(QTime(0, 0), getStartHour() * 60 * 60)); + for (RideItem *rideItem : context->athlete->rideCache->rides()) { + if (rideItem != nullptr && rideItem->planned == sourceItem->planned && rideItem->dateTime.date() == newDate) { + busySlots.append(std::make_pair(rideItem->dateTime.time(), static_cast(rideItem->getForSymbol("workout_time")))); + } + } + busySlots.append(std::make_pair(QTime(getEndHour(), 0), (24 - getEndHour()) * 60 * 60 - 1)); + if (! targetTime.isValid()) { + targetTime = sourceItem->dateTime.time(); + } + QTime newTime = findFreeSlot(busySlots, targetTime, static_cast(sourceItem->getForSymbol("workout_time"))); + return newTime; +} + + +QTime +CalendarWindow::findFreeSlot +(QList> busySlots, QTime targetTime, int requiredDurationSeconds) const +{ + std::sort(busySlots.begin(), busySlots.end(), [](const std::pair &a, const std::pair &b) { + return a.first < b.first; + }); + QSet forbiddenTimes; + for (const std::pair &busySlot : busySlots) { + forbiddenTimes.insert(busySlot.first); + } + QTime bestSlot; + int bestDistance = 24 * 60 * 60; + bool foundFullSlot = false; + QTime dayStart = QTime(0, 0, 0); + QTime dayEnd = QTime(23, 59, 59); + QList> busyPeriods; + for (const std::pair &busySlot : busySlots) { + QTime start = busySlot.first; + QTime end = busySlot.first.addSecs(busySlot.second); + if (busyPeriods.isEmpty()) { + busyPeriods.append({start, end}); + } else { + std::pair &last = busyPeriods.last(); + if (start <= last.second) { + last.second = std::max(last.second, end); + } else { + busyPeriods.append({start, end}); + } + } + } + QList> freeIntervals; + if (busyPeriods.isEmpty()) { + freeIntervals.append({dayStart, dayEnd}); + } else { + if (busyPeriods.first().first > dayStart) { + freeIntervals.append({dayStart, busyPeriods.first().first.addSecs(-1)}); + } + for (int i = 0; i < busyPeriods.size() - 1; ++i) { + QTime currentEnd = busyPeriods[i].second; + QTime nextStart = busyPeriods[i + 1].first; + if (currentEnd < nextStart) { + freeIntervals.append({currentEnd, nextStart.addSecs(-1)}); + } + } + QTime lastEnd = busyPeriods.last().second; + if (lastEnd <= dayEnd) { + freeIntervals.append({lastEnd, dayEnd}); + } + } + for (const std::pair &freeInterval : freeIntervals) { + QTime intervalStart = freeInterval.first; + QTime intervalEnd = freeInterval.second; + int intervalDuration = intervalStart.secsTo(intervalEnd) + 1; + if (intervalDuration >= requiredDurationSeconds) { + QTime candidate = intervalStart; + if (! forbiddenTimes.contains(candidate)) { + int distance = qAbs(candidate.secsTo(targetTime)); + if (distance < bestDistance) { + bestDistance = distance; + bestSlot = candidate; + foundFullSlot = true; + } + } + if (targetTime >= intervalStart && targetTime <= intervalEnd) { + if (targetTime.secsTo(intervalEnd) + 1 >= requiredDurationSeconds) { + if (! forbiddenTimes.contains(targetTime)) { + bestSlot = targetTime; + foundFullSlot = true; + break; + } + } + } + candidate = intervalEnd.addSecs(-(requiredDurationSeconds - 1)); + if (candidate >= intervalStart && ! forbiddenTimes.contains(candidate)) { + int distance = std::abs(candidate.secsTo(targetTime)); + if (distance < bestDistance) { + bestDistance = distance; + bestSlot = candidate; + foundFullSlot = true; + } + } + } + } + if (foundFullSlot) { + return bestSlot; + } + if (! forbiddenTimes.contains(targetTime)) { + return targetTime; + } + const int increment = 15 * 60; + int targetSecs = QTime(0, 0, 0).secsTo(targetTime); + int maxSecs = QTime(0, 0, 0).secsTo(QTime(23, 59, 59)); + int maxSteps = (maxSecs / increment) + 1; + for (int step = 1; step <= maxSteps; ++step) { + int offset = step * increment; + if (targetSecs + offset <= maxSecs) { + QTime candidate = QTime(0, 0, 0).addSecs(targetSecs + offset); + if (! forbiddenTimes.contains(candidate)) { + return candidate; + } + } + if (targetSecs - offset >= 0) { + QTime candidate = QTime(0, 0, 0).addSecs(targetSecs - offset); + if (! forbiddenTimes.contains(candidate)) { + return candidate; + } + } + } + return QTime(); +} + + void CalendarWindow::updateActivities () @@ -1405,6 +1543,66 @@ CalendarWindow::unlinkActivities } +void +CalendarWindow::copyPlannedActivity +(CalendarEntry activity) +{ + RideItem *rideItem = getRideItem(activity); + if (rideItem != nullptr) { + QClipboard *clipboard = QGuiApplication::clipboard(); + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + stream << activity.primary << activity.reference; + QMimeData *mimeData = new QMimeData(); + mimeData->setData(PLANNED_MIME_TYPE, data); + clipboard->setMimeData(mimeData); + } +} + + +void +CalendarWindow::pastePlannedActivity +(QDate day, QTime time) +{ + QClipboard *clipboard = QGuiApplication::clipboard(); + const QMimeData *mimeData = clipboard->mimeData(); + if (mimeData && mimeData->hasFormat(PLANNED_MIME_TYPE)) { + QByteArray data = mimeData->data(PLANNED_MIME_TYPE); + QDataStream stream(data); + QString primary; + QString reference; + stream >> primary >> reference; + + RideItem *sourceItem = nullptr; + for (RideItem *rideItem : context->athlete->rideCache->rides()) { + if (rideItem != nullptr && rideItem->planned && rideItem->fileName == reference) { + sourceItem = rideItem; + break; + } + } + time = findFreeSlot(sourceItem, day, time); + RideCache::OperationPreCheck check = context->athlete->rideCache->checkCopyPlannedActivity(sourceItem, day, time); + if (check.canProceed) { + if (proceedDialog(context, check)) { + context->tab->setNoSwitch(true); + RideCache::OperationResult result = context->athlete->rideCache->copyPlannedActivity(sourceItem, day, time); + if (result.success) { + QString error; + context->athlete->rideCache->saveActivities(check.affectedItems, error); + // Context::rideDeleted is not always emitted, therefore forcing the update + updateActivities(); + } else { + QMessageBox::warning(this, tr("Paste failed: %1").arg(primary), result.error); + } + context->tab->setNoSwitch(false); + } + } else { + QMessageBox::warning(this, tr("Paste failed: %1").arg(primary), check.blockingReason); + } + } +} + + void CalendarWindow::addEvent (const QDate &date) diff --git a/src/Charts/CalendarWindow.h b/src/Charts/CalendarWindow.h index a9539323f..526d4aed8 100644 --- a/src/Charts/CalendarWindow.h +++ b/src/Charts/CalendarWindow.h @@ -159,6 +159,8 @@ class CalendarWindow : public GcChartWindow QHash> getPhasesEvents(const Season &season, const QDate &firstDay, const QDate &lastDay) const; RideItem *getRideItem(const CalendarEntry &entry, bool linked = false); QString getPrimary(RideItem const * const rideItem) const; + QTime findFreeSlot(RideItem *sourceItem, QDate newDate, QTime time); + QTime findFreeSlot(QList> busySlots, QTime targetTime, int requiredDurationSeconds) const; private slots: void updateActivities(); @@ -168,6 +170,8 @@ class CalendarWindow : public GcChartWindow void shiftPlannedActivities(const QDate &destDay, int offset); void linkActivities(const CalendarEntry &entry, bool autoLink); bool unlinkActivities(const CalendarEntry &entry); + void copyPlannedActivity(CalendarEntry activity); + void pastePlannedActivity(QDate day, QTime time); void addEvent(const QDate &date); void editEvent(const CalendarEntry &entry); void delEvent(const CalendarEntry &entry); diff --git a/src/Core/RideCache.cpp b/src/Core/RideCache.cpp index bdc83def7..0bf3b2aae 100644 --- a/src/Core/RideCache.cpp +++ b/src/Core/RideCache.cpp @@ -1246,7 +1246,7 @@ RideCache::moveActivity RideCache::OperationPreCheck RideCache::checkCopyPlannedActivity -(RideItem *sourceItem, const QDate &newDate) +(RideItem *sourceItem, const QDate &newDate, QTime newTime) { OperationPreCheck check; @@ -1260,8 +1260,12 @@ RideCache::checkCopyPlannedActivity check.blockingReason = tr("Invalid date specified"); return check; } + QTime time(sourceItem->dateTime.time()); + if (newTime.isValid()) { + time = newTime; + } - QDateTime newDateTime(newDate, sourceItem->dateTime.time()); + QDateTime newDateTime(newDate, time); QFileInfo oldInfo(sourceItem->fileName); QString newFileName = newDateTime.toString("yyyy_MM_dd_HH_mm_ss") + "." + oldInfo.suffix(); QString newPath = plannedDirectory.canonicalPath() + "/" + newFileName; @@ -1277,12 +1281,16 @@ RideCache::checkCopyPlannedActivity RideCache::OperationResult RideCache::copyPlannedActivity -(RideItem *sourceItem, const QDate &newDate) +(RideItem *sourceItem, const QDate &newDate, QTime newTime) { OperationResult result; QString error; - RideItem *newItem = copyPlannedRideFile(sourceItem, newDate, error); + QTime time(sourceItem->dateTime.time()); + if (newTime.isValid()) { + time = newTime; + } + RideItem *newItem = copyPlannedRideFile(sourceItem, newDate, time, error); if (! newItem) { result.error = error; @@ -1366,7 +1374,7 @@ RideCache::copyPlannedActivities QStringList failedFiles; for (const std::pair &pair : sourceItemsAndTargets) { QString error; - RideItem *newItem = copyPlannedRideFile(pair.first, pair.second, error); + RideItem *newItem = copyPlannedRideFile(pair.first, pair.second, QTime(), error); if (newItem) { newItems << newItem; } else { @@ -1806,9 +1814,9 @@ RideCache::isValidLink RideItem* RideCache::copyPlannedRideFile -(RideItem *sourceItem, const QDate &newDate, QString &error) +(RideItem *sourceItem, const QDate &newDate, const QTime &newTime, QString &error) { - QDateTime newDateTime(newDate, sourceItem->dateTime.time()); + QDateTime newDateTime(newDate, newTime); QFileInfo oldInfo(sourceItem->fileName); QString newFileName = newDateTime.toString("yyyy_MM_dd_HH_mm_ss") + "." + oldInfo.suffix(); QString newPath = plannedDirectory.canonicalPath() + "/" + newFileName; diff --git a/src/Core/RideCache.h b/src/Core/RideCache.h index a50bbf4ea..6bbcf06c8 100644 --- a/src/Core/RideCache.h +++ b/src/Core/RideCache.h @@ -135,8 +135,8 @@ class RideCache : public QObject OperationPreCheck checkMoveActivity(RideItem *item, const QDateTime &newDateTime); OperationResult moveActivity(RideItem *item, const QDateTime &newDateTime); - OperationPreCheck checkCopyPlannedActivity(RideItem *sourceItem, const QDate &newDate); - OperationResult copyPlannedActivity(RideItem *sourceItem, const QDate &newDate); + OperationPreCheck checkCopyPlannedActivity(RideItem *sourceItem, const QDate &newDate, QTime newTime = QTime()); + OperationResult copyPlannedActivity(RideItem *sourceItem, const QDate &newDate, QTime newTime = QTime()); OperationPreCheck checkCopyPlannedActivities(const QList> &sourceItemsAndTargets); OperationResult copyPlannedActivities(const QList> &sourceItemsAndTargets); @@ -220,7 +220,7 @@ class RideCache : public QObject private: bool renameRideFiles(const QString& oldFileName, const QString& newFileName, bool isPlanned, QString &error); bool isValidLink(RideItem *item1, RideItem *item2, QString &error); - RideItem* copyPlannedRideFile(RideItem *sourceItem, const QDate &newDate, QString &error); + RideItem* copyPlannedRideFile(RideItem *sourceItem, const QDate &newDate, const QTime &newTime, QString &error); }; class AthleteBest diff --git a/src/Gui/Calendar.cpp b/src/Gui/Calendar.cpp index bf5b9b293..0652602c3 100644 --- a/src/Gui/Calendar.cpp +++ b/src/Gui/Calendar.cpp @@ -199,6 +199,7 @@ CalendarBaseTable::buildContextMenu contextMenu->addAction(tr("View actual activity") % ellipsis, this, [this, entry]() { emit viewLinkedActivity(entry); }); } contextMenu->addAction(tr("View planned activity") % ellipsis, this, [this, entry]() { emit viewActivity(entry); }); + contextMenu->addAction(tr("Copy planned activity"), this, [this, entry]() { emit copyPlannedActivity(entry); }); contextMenu->addSeparator(); if (entry.linkedReference.isEmpty()) { contextMenu->addAction(tr("Link to actual activity"), this, [this, entry]() { emit linkActivity(entry, true); }); @@ -264,6 +265,13 @@ CalendarBaseTable::buildContextMenu } emit addActivity(true, day.date, activityTime); }); + QClipboard *clipboard = QGuiApplication::clipboard(); + const QMimeData *mimeData = clipboard->mimeData(); + if (mimeData && mimeData->hasFormat(PLANNED_MIME_TYPE)) { + contextMenu->addAction(tr("Paste planned activity"), this, [this, day, time]() { + emit pastePlannedActivity(day.date, time); + }); + } } if (canHavePhasesEvents) { contextMenu->addSeparator(); @@ -1545,6 +1553,8 @@ CalendarDayView::CalendarDayView connect(dayTable, &CalendarDayTable::unlinkActivity, this, &CalendarDayView::unlinkActivity); connect(dayTable, &CalendarDayTable::viewActivity, this, &CalendarDayView::viewActivity); connect(dayTable, &CalendarDayTable::viewLinkedActivity, this, &CalendarDayView::viewLinkedActivity); + connect(dayTable, &CalendarDayTable::copyPlannedActivity, this, &CalendarDayView::copyPlannedActivity); + connect(dayTable, &CalendarDayTable::pastePlannedActivity, this, &CalendarDayView::pastePlannedActivity); connect(dayTable, &CalendarDayTable::addActivity, this, &CalendarDayView::addActivity); connect(dayTable, &CalendarDayTable::showInTrainMode, this, &CalendarDayView::showInTrainMode); connect(dayTable, &CalendarDayTable::filterSimilar, this, &CalendarDayView::filterSimilar); @@ -1845,6 +1855,8 @@ CalendarWeekView::CalendarWeekView connect(weekTable, &CalendarDayTable::unlinkActivity, this, &CalendarWeekView::unlinkActivity); connect(weekTable, &CalendarDayTable::viewActivity, this, &CalendarWeekView::viewActivity); connect(weekTable, &CalendarDayTable::viewLinkedActivity, this, &CalendarWeekView::viewLinkedActivity); + connect(weekTable, &CalendarDayTable::copyPlannedActivity, this, &CalendarWeekView::copyPlannedActivity); + connect(weekTable, &CalendarDayTable::pastePlannedActivity, this, &CalendarWeekView::pastePlannedActivity); connect(weekTable, &CalendarDayTable::addActivity, this, &CalendarWeekView::addActivity); connect(weekTable, &CalendarDayTable::showInTrainMode, this, &CalendarWeekView::showInTrainMode); connect(weekTable, &CalendarDayTable::filterSimilar, this, &CalendarWeekView::filterSimilar); @@ -2043,6 +2055,8 @@ Calendar::Calendar connect(dayView, &CalendarDayView::unlinkActivity, this, &Calendar::unlinkActivity); connect(dayView, &CalendarDayView::viewActivity, this, &Calendar::viewActivity); connect(dayView, &CalendarDayView::viewLinkedActivity, this, &Calendar::viewLinkedActivity); + connect(dayView, &CalendarDayView::copyPlannedActivity, this, &Calendar::copyPlannedActivity); + connect(dayView, &CalendarDayView::pastePlannedActivity, this, &Calendar::pastePlannedActivity); connect(dayView, &CalendarDayView::addActivity, this, &Calendar::addActivity); connect(dayView, &CalendarDayView::showInTrainMode, this, &Calendar::showInTrainMode); connect(dayView, &CalendarDayView::filterSimilar, this, &Calendar::filterSimilar); @@ -2072,6 +2086,8 @@ Calendar::Calendar connect(weekView, &CalendarWeekView::unlinkActivity, this, &Calendar::unlinkActivity); connect(weekView, &CalendarWeekView::viewActivity, this, &Calendar::viewActivity); connect(weekView, &CalendarWeekView::viewLinkedActivity, this, &Calendar::viewLinkedActivity); + connect(weekView, &CalendarWeekView::copyPlannedActivity, this, &Calendar::copyPlannedActivity); + connect(weekView, &CalendarWeekView::pastePlannedActivity, this, &Calendar::pastePlannedActivity); connect(weekView, &CalendarWeekView::addActivity, this, &Calendar::addActivity); connect(weekView, &CalendarWeekView::showInTrainMode, this, &Calendar::showInTrainMode); connect(weekView, &CalendarWeekView::filterSimilar, this, &Calendar::filterSimilar); @@ -2106,6 +2122,8 @@ Calendar::Calendar connect(monthView, &CalendarMonthTable::unlinkActivity, this, &Calendar::unlinkActivity); connect(monthView, &CalendarMonthTable::viewActivity, this, &Calendar::viewActivity); connect(monthView, &CalendarMonthTable::viewLinkedActivity, this, &Calendar::viewLinkedActivity); + connect(monthView, &CalendarMonthTable::copyPlannedActivity, this, &Calendar::copyPlannedActivity); + connect(monthView, &CalendarMonthTable::pastePlannedActivity, this, &Calendar::pastePlannedActivity); connect(monthView, &CalendarMonthTable::addActivity, this, &Calendar::addActivity); connect(monthView, &CalendarMonthTable::repeatSchedule, this, &Calendar::repeatSchedule); connect(monthView, &CalendarMonthTable::insertRestday, this, &Calendar::insertRestday); diff --git a/src/Gui/Calendar.h b/src/Gui/Calendar.h index bda377b86..bbf7b3c94 100644 --- a/src/Gui/Calendar.h +++ b/src/Gui/Calendar.h @@ -72,6 +72,8 @@ signals: void linkActivity(CalendarEntry activity, bool autoLink); void unlinkActivity(CalendarEntry activity); void viewActivity(CalendarEntry activity); + void copyPlannedActivity(CalendarEntry activity); + void pastePlannedActivity(QDate day, QTime time); void viewLinkedActivity(CalendarEntry activity); void addActivity(bool plan, QDate day, QTime time); void delActivity(CalendarEntry activity); @@ -268,6 +270,8 @@ signals: void unlinkActivity(CalendarEntry activity); void viewActivity(CalendarEntry activity); void viewLinkedActivity(CalendarEntry activity); + void copyPlannedActivity(CalendarEntry activity); + void pastePlannedActivity(QDate day, QTime time); void addActivity(bool plan, QDate day, QTime time); void delActivity(CalendarEntry activity); void saveChanges(CalendarEntry activity); @@ -322,6 +326,8 @@ signals: void unlinkActivity(CalendarEntry activity); void viewActivity(CalendarEntry activity); void viewLinkedActivity(CalendarEntry activity); + void copyPlannedActivity(CalendarEntry activity); + void pastePlannedActivity(QDate day, QTime time); void addActivity(bool plan, QDate day, QTime time); void delActivity(CalendarEntry activity); void saveChanges(CalendarEntry activity); @@ -389,6 +395,8 @@ signals: void unlinkActivity(CalendarEntry activity); void viewActivity(CalendarEntry activity); void viewLinkedActivity(CalendarEntry activity); + void copyPlannedActivity(CalendarEntry activity); + void pastePlannedActivity(QDate day, QTime time); void addActivity(bool plan, QDate day, QTime time); void delActivity(CalendarEntry activity); void saveChanges(CalendarEntry activity); diff --git a/src/Gui/CalendarData.h b/src/Gui/CalendarData.h index 2b6c8ef9a..94de6d18b 100644 --- a/src/Gui/CalendarData.h +++ b/src/Gui/CalendarData.h @@ -34,6 +34,8 @@ #define ENTRY_TYPE_PHASE 11 #define ENTRY_TYPE_OTHER 99 +#define PLANNED_MIME_TYPE "application/x-goldencheetah-planned-activity" + struct AgendaEntry { QString primary;