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"