File indexing completed on 2025-01-19 03:50:39
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2009-05-04 0007 * Description : Various operation on items 0008 * 0009 * SPDX-FileCopyrightText: 2002-2005 by Renchi Raju <renchi dot raju at gmail dot com> 0010 * SPDX-FileCopyrightText: 2002-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0011 * SPDX-FileCopyrightText: 2006-2010 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de> 0012 * SPDX-FileCopyrightText: 2009-2010 by Andi Clemens <andi dot clemens at gmail dot com> 0013 * 0014 * SPDX-License-Identifier: GPL-2.0-or-later 0015 * 0016 * ============================================================ */ 0017 0018 #include "itemviewutilities.h" 0019 0020 // Qt includes 0021 0022 #include <QStandardPaths> 0023 #include <QStringView> 0024 #include <QFileInfo> 0025 #include <QUrl> 0026 0027 // KDE includes 0028 0029 #include <klocalizedstring.h> 0030 #include <ksharedconfig.h> 0031 #include <kconfiggroup.h> 0032 0033 // Local includes 0034 0035 #include "digikam_debug.h" 0036 #include "album.h" 0037 #include "albummanager.h" 0038 #include "albumselectdialog.h" 0039 #include "applicationsettings.h" 0040 #include "deletedialog.h" 0041 #include "dfiledialog.h" 0042 #include "dio.h" 0043 #include "imagewindow.h" 0044 #include "lighttablewindow.h" 0045 #include "loadingcacheinterface.h" 0046 #include "queuemgrwindow.h" 0047 #include "thumbnailloadthread.h" 0048 #include "fileactionmngr.h" 0049 #include "dfileoperations.h" 0050 #include "coredb.h" 0051 #include "coredbaccess.h" 0052 0053 namespace Digikam 0054 { 0055 0056 ItemViewUtilities::ItemViewUtilities(QWidget* const parentWidget) 0057 : QObject (parentWidget), 0058 m_widget(parentWidget) 0059 { 0060 connect(this, SIGNAL(signalImagesDeleted(QList<qlonglong>)), 0061 AlbumManager::instance(), SLOT(slotImagesDeleted(QList<qlonglong>))); 0062 } 0063 0064 void ItemViewUtilities::setAsAlbumThumbnail(Album* album, 0065 const ItemInfo& itemInfo) 0066 { 0067 if (!album) 0068 { 0069 return; 0070 } 0071 0072 if (album->type() == Album::PHYSICAL) 0073 { 0074 PAlbum* const palbum = static_cast<PAlbum*>(album); 0075 0076 QString err; 0077 AlbumManager::instance()->updatePAlbumIcon(palbum, itemInfo.id(), err); 0078 } 0079 else if (album->type() == Album::TAG) 0080 { 0081 TAlbum* const talbum = static_cast<TAlbum*>(album); 0082 0083 QString err; 0084 AlbumManager::instance()->updateTAlbumIcon(talbum, QString(), itemInfo.id(), err); 0085 } 0086 } 0087 0088 void ItemViewUtilities::rename(const QUrl& imageUrl, 0089 const QString& newName, 0090 bool overwrite) 0091 { 0092 if (imageUrl.isEmpty() || !imageUrl.isLocalFile() || newName.isEmpty()) 0093 { 0094 return; 0095 } 0096 0097 DIO::rename(imageUrl, newName, overwrite); 0098 } 0099 0100 bool ItemViewUtilities::deleteImages(const QList<ItemInfo>& infos, 0101 const DeleteMode deleteMode) 0102 { 0103 if (infos.isEmpty()) 0104 { 0105 return false; 0106 } 0107 0108 QList<ItemInfo> deleteInfos = infos; 0109 0110 QList<QUrl> urlList; 0111 QList<qlonglong> imageIds; 0112 0113 // Buffer the urls for deletion and imageids for notification of the AlbumManager 0114 0115 Q_FOREACH (const ItemInfo& info, deleteInfos) 0116 { 0117 urlList << info.fileUrl(); 0118 imageIds << info.id(); 0119 } 0120 0121 DeleteDialog dialog(m_widget); 0122 0123 DeleteDialogMode::DeleteMode deleteDialogMode = DeleteDialogMode::NoChoiceTrash; 0124 0125 if (deleteMode == ItemViewUtilities::DeletePermanently) 0126 { 0127 deleteDialogMode = DeleteDialogMode::NoChoiceDeletePermanently; 0128 } 0129 0130 if (!dialog.confirmDeleteList(urlList, DeleteDialogMode::Files, deleteDialogMode)) 0131 { 0132 return false; 0133 } 0134 0135 const bool useTrash = !dialog.shouldDelete(); 0136 0137 DIO::del(deleteInfos, useTrash); 0138 0139 // Signal the Albummanager about the ids of the deleted images. 0140 0141 Q_EMIT signalImagesDeleted(imageIds); 0142 0143 return true; 0144 } 0145 0146 void ItemViewUtilities::deleteImagesDirectly(const QList<ItemInfo>& infos, 0147 const DeleteMode deleteMode) 0148 { 0149 // This method deletes the selected items directly, without confirmation. 0150 // It is not used in the default setup. 0151 0152 if (infos.isEmpty()) 0153 { 0154 return; 0155 } 0156 0157 QList<qlonglong> imageIds; 0158 0159 Q_FOREACH (const ItemInfo& info, infos) 0160 { 0161 imageIds << info.id(); 0162 } 0163 0164 const bool useTrash = (deleteMode == ItemViewUtilities::DeleteUseTrash); 0165 0166 DIO::del(infos, useTrash); 0167 0168 // Signal the Albummanager about the ids of the deleted images. 0169 0170 Q_EMIT signalImagesDeleted(imageIds); 0171 } 0172 0173 void ItemViewUtilities::notifyFileContentChanged(const QList<QUrl>& urls) 0174 { 0175 Q_FOREACH (const QUrl& url, urls) 0176 { 0177 QString path = url.toLocalFile(); 0178 ThumbnailLoadThread::deleteThumbnail(path); 0179 0180 // clean LoadingCache as well - be pragmatic, do it here. 0181 0182 LoadingCacheInterface::fileChanged(path); 0183 } 0184 } 0185 0186 void ItemViewUtilities::copyItemsToExternalFolder(const QList<ItemInfo>& infos) 0187 { 0188 if (infos.isEmpty()) 0189 { 0190 return; 0191 } 0192 0193 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0194 KConfigGroup group = config->group(QLatin1String("Copy To Folder Settings")); 0195 QString startingPath = group.readEntry(QLatin1String("Last Copy To Folder Path"), QString()); 0196 0197 if (startingPath.isEmpty()) 0198 { 0199 startingPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); 0200 } 0201 0202 QUrl url = DFileDialog::getExistingDirectoryUrl(m_widget, i18nc("@title:window", "Select Target Folder"), 0203 QUrl::fromLocalFile(startingPath)); 0204 0205 if (url.isEmpty() || !url.isLocalFile()) 0206 { 0207 return; 0208 } 0209 0210 group.writeEntry(QLatin1String("Last Copy To Folder Path"), url.toLocalFile()); 0211 0212 DIO::copy(infos, url); 0213 } 0214 0215 void ItemViewUtilities::createNewAlbumForInfos(const QList<ItemInfo>& infos, 0216 Album* currentAlbum) 0217 { 0218 if (infos.isEmpty()) 0219 { 0220 return; 0221 } 0222 0223 if (currentAlbum && (currentAlbum->type() != Album::PHYSICAL)) 0224 { 0225 currentAlbum = nullptr; 0226 } 0227 0228 QString header(i18n("<p>Please select the destination album from the digiKam library to " 0229 "move the selected images into.</p>")); 0230 0231 Album* const album = AlbumSelectDialog::selectAlbum(m_widget, static_cast<PAlbum*>(currentAlbum), header); 0232 0233 if (!album) 0234 { 0235 return; 0236 } 0237 0238 PAlbum* const palbum = dynamic_cast<PAlbum*>(album); 0239 0240 if (!palbum) 0241 { 0242 return; 0243 } 0244 0245 DIO::move(infos, palbum); 0246 } 0247 0248 void ItemViewUtilities::insertToLightTableAuto(const QList<ItemInfo>& all, 0249 const QList<ItemInfo>& selected, 0250 const ItemInfo& current) 0251 { 0252 ItemInfoList list = ItemInfoList(selected); 0253 ItemInfo singleInfo = current; 0254 0255 if (list.isEmpty() || ((list.size() == 1) && LightTableWindow::lightTableWindow()->isEmpty())) 0256 { 0257 list = ItemInfoList(all); 0258 } 0259 0260 if (singleInfo.isNull() && !list.isEmpty()) 0261 { 0262 singleInfo = list.first(); 0263 } 0264 0265 insertToLightTable(list, current, (list.size() <= 1)); 0266 } 0267 0268 void ItemViewUtilities::insertToLightTable(const QList<ItemInfo>& list, 0269 const ItemInfo& current, 0270 bool addTo) 0271 { 0272 LightTableWindow* const ltview = LightTableWindow::lightTableWindow(); 0273 0274 // If addTo is false, the light table will be emptied before adding 0275 // the images. 0276 0277 ltview->loadItemInfos(ItemInfoList(list), current, addTo); 0278 ltview->setLeftRightItems(ItemInfoList(list), addTo); 0279 0280 if (ltview->isHidden()) 0281 { 0282 ltview->show(); 0283 } 0284 0285 ltview->unminimizeAndActivateWindow(); 0286 } 0287 0288 void ItemViewUtilities::insertToQueueManager(const QList<ItemInfo>& list, const ItemInfo& current, bool newQueue) 0289 { 0290 Q_UNUSED(current); 0291 0292 QueueMgrWindow* const bqmview = QueueMgrWindow::queueManagerWindow(); 0293 0294 if (bqmview->isHidden()) 0295 { 0296 bqmview->show(); 0297 } 0298 0299 bqmview->unminimizeAndActivateWindow(); 0300 0301 if (newQueue) 0302 { 0303 bqmview->loadItemInfosToNewQueue(ItemInfoList(list)); 0304 } 0305 else 0306 { 0307 bqmview->loadItemInfosToCurrentQueue(ItemInfoList(list)); 0308 } 0309 } 0310 0311 void ItemViewUtilities::insertSilentToQueueManager(const QList<ItemInfo>& list, 0312 const ItemInfo& /*current*/, 0313 int queueid) 0314 { 0315 QueueMgrWindow* const bqmview = QueueMgrWindow::queueManagerWindow(); 0316 bqmview->loadItemInfos(ItemInfoList(list), queueid); 0317 } 0318 0319 void ItemViewUtilities::openInfos(const ItemInfo& info, 0320 const QList<ItemInfo>& allInfosToOpen, 0321 Album* currentAlbum) 0322 { 0323 if (info.isNull()) 0324 { 0325 return; 0326 } 0327 0328 QFileInfo fi(info.filePath()); 0329 QString imagefilter = ApplicationSettings::instance()->getImageFileFilter(); 0330 imagefilter += ApplicationSettings::instance()->getRawFileFilter(); 0331 0332 // If the current item is not an image file. 0333 0334 if (!imagefilter.contains(fi.suffix().toLower())) 0335 { 0336 // Openonly the first one from the list. 0337 0338 openInfosWithDefaultApplication(QList<ItemInfo>() << info); 0339 return; 0340 } 0341 0342 // Run digiKam ImageEditor with all image from current Album. 0343 0344 ImageWindow* const imview = ImageWindow::imageWindow(); 0345 0346 imview->disconnect(this); 0347 0348 connect(imview, SIGNAL(signalURLChanged(QUrl)), 0349 this, SIGNAL(editorCurrentUrlChanged(QUrl))); 0350 0351 imview->loadItemInfos(ItemInfoList(allInfosToOpen), info, 0352 currentAlbum ? i18n("Album \"%1\"", currentAlbum->title()) 0353 : QString()); 0354 0355 if (imview->isHidden()) 0356 { 0357 imview->show(); 0358 } 0359 0360 imview->unminimizeAndActivateWindow(); 0361 } 0362 0363 void ItemViewUtilities::openInfosWithDefaultApplication(const QList<ItemInfo>& infos) 0364 { 0365 if (infos.isEmpty()) 0366 { 0367 return; 0368 } 0369 0370 QList<QUrl> urls; 0371 0372 Q_FOREACH (const ItemInfo& inf, infos) 0373 { 0374 urls << inf.fileUrl(); 0375 } 0376 0377 DFileOperations::openFilesWithDefaultApplication(urls); 0378 } 0379 0380 namespace 0381 { 0382 0383 bool lessThanByTimeForItemInfo(const ItemInfo& a, const ItemInfo& b) 0384 { 0385 return (a.dateTime() < b.dateTime()); 0386 } 0387 0388 bool lowerThanByNameForItemInfo(const ItemInfo& a, const ItemInfo& b) 0389 { 0390 return (a.name() < b.name()); 0391 } 0392 0393 bool lowerThanBySizeForItemInfo(const ItemInfo& a, const ItemInfo& b) 0394 { 0395 return (a.fileSize() < b.fileSize()); 0396 } 0397 0398 } // namespace 0399 0400 void ItemViewUtilities::createGroupByTimeFromInfoList(const ItemInfoList& itemInfoList) 0401 { 0402 QList<ItemInfo> groupingList = itemInfoList; 0403 0404 // sort by time 0405 0406 std::stable_sort(groupingList.begin(), groupingList.end(), lessThanByTimeForItemInfo); 0407 0408 QList<ItemInfo>::iterator it, it2; 0409 0410 for (it = groupingList.begin() ; it != groupingList.end() ; ) 0411 { 0412 const ItemInfo& leader = *it; 0413 QList<ItemInfo> group; 0414 QDateTime time = it->dateTime(); 0415 0416 if (time.isValid()) 0417 { 0418 for (it2 = it + 1 ; it2 != groupingList.end() ; ++it2) 0419 { 0420 if (qAbs(time.secsTo(it2->dateTime())) < 2) 0421 { 0422 group << *it2; 0423 } 0424 else 0425 { 0426 break; 0427 } 0428 } 0429 } 0430 else 0431 { 0432 ++it; 0433 continue; 0434 } 0435 0436 // increment to next item not put in the group 0437 0438 it = it2; 0439 0440 if (!group.isEmpty()) 0441 { 0442 FileActionMngr::instance()->addToGroup(leader, group); 0443 } 0444 } 0445 } 0446 0447 void ItemViewUtilities::createGroupByFilenameFromInfoList(const ItemInfoList& itemInfoList) 0448 { 0449 QList<ItemInfo> groupingList = itemInfoList; 0450 0451 // sort by Name 0452 0453 std::stable_sort(groupingList.begin(), groupingList.end(), lowerThanByNameForItemInfo); 0454 0455 QList<ItemInfo>::iterator it, it2; 0456 0457 for (it = groupingList.begin() ; it != groupingList.end() ; ) 0458 { 0459 QList<ItemInfo> group; 0460 QString fname = it->name().left(it->name().indexOf(QLatin1Char('.'))); 0461 0462 // don't know the leader yet so put first element also in group 0463 0464 group << *it; 0465 0466 for (it2 = it + 1 ; it2 != groupingList.end() ; ++it2) 0467 { 0468 QString fname2 = it2->name().left(it2->name().indexOf(QLatin1Char('.'))); 0469 0470 if (fname == fname2) 0471 { 0472 group << *it2; 0473 } 0474 else 0475 { 0476 break; 0477 } 0478 } 0479 0480 // increment to next item not put in the group 0481 0482 it = it2; 0483 0484 if (group.count() > 1) 0485 { 0486 // sort by filesize and take smallest as leader 0487 0488 std::stable_sort(group.begin(), group.end(), lowerThanBySizeForItemInfo); 0489 const ItemInfo& leader = group.takeFirst(); 0490 FileActionMngr::instance()->addToGroup(leader, group); 0491 } 0492 } 0493 } 0494 0495 namespace 0496 { 0497 0498 class Q_DECL_HIDDEN NumberInFilenameMatch 0499 { 0500 public: 0501 0502 NumberInFilenameMatch() 0503 : value(0), 0504 containsValue(false) 0505 { 0506 } 0507 0508 explicit NumberInFilenameMatch(const QString& filename) 0509 : NumberInFilenameMatch() 0510 { 0511 if (filename.isEmpty()) 0512 { 0513 return; 0514 } 0515 0516 auto firstDigit = std::find_if(filename.begin(), filename.end(), 0517 [](const QChar& c) 0518 { 0519 return c.isDigit(); 0520 } 0521 ); 0522 0523 prefix = QStringView{filename}.left(std::distance(filename.begin(), firstDigit)); 0524 0525 if (firstDigit == filename.end()) 0526 { 0527 return; 0528 } 0529 0530 auto lastDigit = std::find_if(firstDigit, filename.end(), 0531 [](const QChar& c) 0532 { 0533 return !c.isDigit(); 0534 } 0535 ); 0536 0537 value = filename.mid(prefix.size(), 0538 std::distance(firstDigit, 0539 lastDigit)).toULongLong(&containsValue); 0540 0541 suffix = QStringView{filename}.mid(std::distance(lastDigit, filename.end())); 0542 } 0543 0544 bool directlyPreceeds(NumberInFilenameMatch const& other) const 0545 { 0546 if (!containsValue || !other.containsValue) 0547 { 0548 return false; 0549 } 0550 0551 if (prefix != other.prefix) 0552 { 0553 return false; 0554 } 0555 0556 if (suffix != other.suffix) 0557 { 0558 return false; 0559 } 0560 0561 return ((value + 1) == other.value); 0562 } 0563 0564 public: 0565 0566 qulonglong value; 0567 QStringView prefix; 0568 QStringView suffix; 0569 bool containsValue; 0570 }; 0571 0572 bool imageMatchesTimelapseGroup(const ItemInfoList& group, const ItemInfo& itemInfo) 0573 { 0574 if (group.size() < 2) 0575 { 0576 return true; 0577 } 0578 0579 auto const timeBetweenPhotos = qAbs(group.first().dateTime() 0580 .secsTo(group.last() 0581 .dateTime())) / (group.size()-1); 0582 0583 auto const predictedNextTimestamp = group.last().dateTime() 0584 .addSecs(timeBetweenPhotos); 0585 0586 return (qAbs(itemInfo.dateTime().secsTo(predictedNextTimestamp)) <= 1); 0587 } 0588 0589 } // namespace 0590 0591 void ItemViewUtilities::createGroupByTimelapseFromInfoList(const ItemInfoList& itemInfoList) 0592 { 0593 if (itemInfoList.size() < 3) 0594 { 0595 return; 0596 } 0597 0598 ItemInfoList groupingList = itemInfoList; 0599 0600 std::stable_sort(groupingList.begin(), groupingList.end(), lowerThanByNameForItemInfo); 0601 0602 NumberInFilenameMatch previousNumberMatch; 0603 ItemInfoList group; 0604 0605 for (const auto& itemInfo : groupingList) 0606 { 0607 NumberInFilenameMatch numberMatch(itemInfo.name()); 0608 0609 // if this is an end of currently processed group 0610 0611 if (!previousNumberMatch.directlyPreceeds(numberMatch) || !imageMatchesTimelapseGroup(group, itemInfo)) 0612 { 0613 if (group.size() > 2) 0614 { 0615 FileActionMngr::instance()->addToGroup(group.takeFirst(), group); 0616 } 0617 0618 group.clear(); 0619 } 0620 0621 group.append(itemInfo); 0622 previousNumberMatch = std::move(numberMatch); 0623 } 0624 0625 if (group.size() > 2) 0626 { 0627 FileActionMngr::instance()->addToGroup(group.takeFirst(), group); 0628 } 0629 } 0630 0631 } // namespace Digikam 0632 0633 #include "moc_itemviewutilities.cpp"