diff --git a/core/app/items/imageviewutilities.h b/core/app/items/imageviewutilities.h --- a/core/app/items/imageviewutilities.h +++ b/core/app/items/imageviewutilities.h @@ -78,6 +78,7 @@ void createGroupByTimeFromInfoList(const ItemInfoList& imageInfoList); void createGroupByFilenameFromInfoList(const ItemInfoList& imageInfoList); + void createGroupByTimelapseFromInfoList(const ItemInfoList& imageInfoList); Q_SIGNALS: diff --git a/core/app/items/imageviewutilities.cpp b/core/app/items/imageviewutilities.cpp --- a/core/app/items/imageviewutilities.cpp +++ b/core/app/items/imageviewutilities.cpp @@ -29,6 +29,7 @@ // Qt includes #include +#include #include // KDE includes @@ -460,4 +461,108 @@ } } +namespace +{ + +struct NumberInFilenameMatch +{ + NumberInFilenameMatch() : value(0), containsValue(false) + { + } + + NumberInFilenameMatch(const QString& filename) : NumberInFilenameMatch() + { + if (filename.isEmpty()) + { + return; + } + + auto firstDigit = std::find_if(filename.begin(), filename.end(), [](const QChar c){ return c.isDigit(); }); + prefix = filename.leftRef(std::distance(filename.begin(), firstDigit)); + if (firstDigit == filename.end()) + { + return; + } + + auto lastDigit = std::find_if(firstDigit, filename.end(), [](const QChar c){ return !c.isDigit(); }); + value = filename.midRef(prefix.size(), std::distance(firstDigit, lastDigit)).toULongLong(&containsValue); + + suffix = filename.midRef(std::distance(lastDigit, filename.end())); + } + + bool directlyPreceeds(NumberInFilenameMatch const& other) const + { + if (!containsValue || !other.containsValue) + { + return false; + } + if (prefix != other.prefix) + { + return false; + } + if (suffix != other.suffix) + { + return false; + } + return value+1 == other.value; + } + + QStringRef prefix, suffix; + unsigned long long value; + bool containsValue; +}; + +bool imageMatchesTimelapseGroup(const ItemInfoList& group, const ItemInfo& imageInfo) +{ + if (group.size() < 2) + { + return true; + } + + auto const timeBetweenPhotos = qAbs(group.front().dateTime().secsTo(group.back().dateTime())) / (group.size()-1); + auto const predictedNextTimestamp = group.back().dateTime().addSecs(timeBetweenPhotos); + + return qAbs(imageInfo.dateTime().secsTo(predictedNextTimestamp)) <= 1; +} + +} // namespace + +void ImageViewUtilities::createGroupByTimelapseFromInfoList(const ItemInfoList& imageInfoList) +{ + if (imageInfoList.size() < 3) + { + return; + } + + ItemInfoList groupingList = imageInfoList; + + std::stable_sort(groupingList.begin(), groupingList.end(), lowerThanByNameForItemInfo); + + NumberInFilenameMatch previousNumberMatch; + ItemInfoList group; + + for (const auto& imageInfo : groupingList) + { + NumberInFilenameMatch numberMatch(imageInfo.name()); + + // if this is an end of currently processed group + if (!previousNumberMatch.directlyPreceeds(numberMatch) || !imageMatchesTimelapseGroup(group, imageInfo)) + { + if (group.size() > 2) + { + FileActionMngr::instance()->addToGroup(group.takeFirst(), group); + } + group.clear(); + } + + group.append(imageInfo); + previousNumberMatch = std::move(numberMatch); + } + + if (group.size() > 2) + { + FileActionMngr::instance()->addToGroup(group.takeFirst(), group); + } +} + } // namespace Digikam diff --git a/core/app/utils/contextmenuhelper.h b/core/app/utils/contextmenuhelper.h --- a/core/app/utils/contextmenuhelper.h +++ b/core/app/utils/contextmenuhelper.h @@ -103,6 +103,7 @@ void signalCreateGroup(); void signalCreateGroupByTime(); void signalCreateGroupByFilename(); + void signalCreateGroupByTimelapse(); void signalUngroup(); void signalRemoveFromGroup(); diff --git a/core/app/utils/contextmenuhelper.cpp b/core/app/utils/contextmenuhelper.cpp --- a/core/app/utils/contextmenuhelper.cpp +++ b/core/app/utils/contextmenuhelper.cpp @@ -983,6 +983,10 @@ connect(closeActionType, SIGNAL(triggered()), this, SIGNAL(signalCreateGroupByFilename())); actions << closeActionType; + QAction* const closeActionTimelapse = new QAction(i18nc("@action:inmenu", "Group Selected By Timelapse / Burst"), this); + connect(closeActionTimelapse, SIGNAL(triggered()), this, SIGNAL(signalCreateGroupByTimelapse())); + actions << closeActionTimelapse; + QAction* const separator = new QAction(this); separator->setSeparator(true); actions << separator; diff --git a/core/app/views/digikamview.h b/core/app/views/digikamview.h --- a/core/app/views/digikamview.h +++ b/core/app/views/digikamview.h @@ -266,6 +266,7 @@ void slotCreateGroupFromSelection(); void slotCreateGroupByTimeFromSelection(); void slotCreateGroupByFilenameFromSelection(); + void slotCreateGroupByTimelapseFromSelection(); void slotRemoveSelectedFromGroup(); void slotUngroupSelected(); diff --git a/core/app/views/digikamview.cpp b/core/app/views/digikamview.cpp --- a/core/app/views/digikamview.cpp +++ b/core/app/views/digikamview.cpp @@ -2703,6 +2703,9 @@ connect(&cmHelper, SIGNAL(signalCreateGroupByFilename()), this, SLOT(slotCreateGroupByFilenameFromSelection())); + connect(&cmHelper, SIGNAL(signalCreateGroupByTimelapse()), + this, SLOT(slotCreateGroupByTimelapseFromSelection())); + connect(&cmHelper, SIGNAL(signalRemoveFromGroup()), this, SLOT(slotRemoveSelectedFromGroup())); @@ -2746,6 +2749,9 @@ connect(&cmhelper, SIGNAL(signalCreateGroupByFilename()), this, SLOT(slotCreateGroupByFilenameFromSelection())); + connect(&cmhelper, SIGNAL(signalCreateGroupByTimelapse()), + this, SLOT(slotCreateGroupByTimelapseFromSelection())); + connect(&cmhelper, SIGNAL(signalUngroup()), this, SLOT(slotUngroupSelected())); @@ -2775,6 +2781,11 @@ d->utilities->createGroupByFilenameFromInfoList(selectedInfoList(false, true)); } +void DigikamView::slotCreateGroupByTimelapseFromSelection() +{ + d->utilities->createGroupByTimelapseFromInfoList(selectedInfoList(false, true)); +} + void DigikamView::slotRemoveSelectedFromGroup() { FileActionMngr::instance()->removeFromGroup(selectedInfoList(false, true));