diff --git a/NEWS b/NEWS index 0c1da29816..6b004e39b8 100644 --- a/NEWS +++ b/NEWS @@ -1,236 +1,242 @@ digiKam 6.0.0 - Release date: 2018-xx-xx ***************************************************************************************************** NEW FEATURES: ImageEditor : New clone tool to replace old CImg in-painting tool. ImageEditor : Add Web services Import and Export tools. Showfoto : Add Web services Import and Export tools. LightTable : Add Web services Import and Export tools. Database : Similarity database has been moved to a dedicated file to not bloat core database with computed finger-prints. This will speed-up query in core database in case of Similarity feature is used. Database : New video metadata parser based on ffmpeg to populate database. Search : Add video support to find files based on properties registered on database. General : Add QWebEngine support. General : Update internal Libraw to last 0.18.11. General : Fix all Krazy static analyzer reports. Tags : Add possibility to merge tags by drag & drop. HTML Gallery : New Vanilla theme to emulate export to HTML from Adobe LightRoom. HTML Gallery : New Blue Frame theme. HTML Gallery : New Kiosk-Mode theme. ***************************************************************************************************** BUGFIXES: 001 ==> 172650 - No export tools available. 002 ==> 149591 - The export menu is blank. 003 ==> 300424 - Export tools not detected. 004 ==> 327743 - MediaWiki export not displayed. 005 ==> 348146 - Export tools configuration module. 006 ==> 243275 - Crashing when calling configuration. 007 ==> 257134 - Crashes when entering its settings. 008 ==> 230666 - Crash during start. 009 ==> 306698 - Crashes after update to KDE. 010 ==> 207640 - Crashes immediately at startup, sometimes at closing. 011 ==> 245628 - Crash when enabling/disabling Facebook import/export. 012 ==> 097317 - sigsegv [New Thread 1100241184 (LWP 5665)]. 013 ==> 245776 - Crashes when selecting Settings. 014 ==> 245775 - Crashes even without export tools installed. 015 ==> 254283 - Crash as soon as i click settings. 016 ==> 255733 - crash when reopening configuration dialog. 017 ==> 262050 - Crash after new install and scan to mysql database. 018 ==> 263871 - Crashed after searching for duplicates. 019 ==> 268242 - Crashes after I clicked on 'Settings'. 020 ==> 276882 - Add export tool buttons to toolbar. 021 ==> 284801 - Export to picasaweb crashes. 022 ==> 277669 - Crash after files moved. 023 ==> 281250 - Crash after disabling Export tools. 024 ==> 202637 - Crash trying to start export tool. 025 ==> 306693 - after update to KDE host application crashes on startup. 026 ==> 282781 - Crashes when reopening configuration dialog after disabling export tools. 027 ==> 285499 - Crash when settings window opened. 028 ==> 297629 - Crashed when importing from Nikon P7000. 029 ==> 303338 - Crashes when clicking "send to" button. 030 ==> 307619 - Refuses to load Export tools. 031 ==> 311812 - Export tools not loading, SO version not defined. 032 ==> 313186 - Crashes on attempt to use the "Send to" menu. 033 ==> 313356 - Crashed when clicking the "send to" button. 034 ==> 313577 - Crashes when pressing the "send to" button. 035 ==> 313890 - Crash when clicking "Send to...". 036 ==> 315033 - Crashes on pressing Send To... button. 037 ==> 315914 - The facebook tool crashes everytime on initialization. 038 ==> 326556 - Export tools are not loaded when starting host application for second time. 039 ==> 095175 - crash on loading, signal 11 SIGSEGV. 040 ==> 175844 - Crashes at startup loading Export tools. 041 ==> 306511 - Crash during start. 042 ==> 234021 - Crash on loading. 043 ==> 219772 - Opening the application causes crash. 044 ==> 294173 - Crash after Image resize start. 045 ==> 306881 - Crashed when attempting to open Export tools. 046 ==> 306495 - Crash changing settings linux ubuntu lucid. 047 ==> 306497 - Crash after changing settings segmentation fault possible 2nd report? 048 ==> 185470 - "Import from facebook" is listed twice in import menu. 049 ==> 334045 - MediaWiki option not available in Export menu when plugin activated. 050 ==> 142112 - Can't save on my webspace with ShowFoto. 051 ==> 167417 - Showfoto cannot save files of CIFS mount. 052 ==> 125164 - Flickr export tool should respect host application selection. 053 ==> 238927 - Host application quits when uploading to Flickr. 054 ==> 326740 - Selection of tools is set to default after each update. 055 ==> 233063 - Add progress indicator when moving or copy files [patch]. 056 ==> 361829 - Rotated MP4 video with "Orientation" flag are not played back in the correct rotation angle. 057 ==> 329854 - digiKam doesn't rotate video thumbnail. 058 ==> 376661 - When importing ~200,000 video files Digikam crashes in about 2-5 seconds of starting. 059 ==> 377072 - Cannot read video metadata. 060 ==> 374453 - Extract right video date from MP4 files metadata. 061 ==> 377177 - Geolocation / GPS information doesn't appear in video metadata. 062 ==> 383588 - Imported video files have time shifted exactly 2 hours later. 063 ==> 380847 - MP4 video not importing with correct date. 064 ==> 373682 - geolocalisation filter does not take care of the videos geolocalisation tags. 065 ==> 340925 - digiKam crash when start it. 066 ==> 331506 - digiKam crashes on startup. 067 ==> 335816 - Crash when trying to add a big collection. 068 ==> 353295 - digiKam repeatedly crashes while importing pictures. 069 ==> 341433 - Crash when opening digiKam application. 070 ==> 375562 - digiKam crashes while scanning images into sqlite database. 071 ==> 334782 - Crash while doing nothing. 072 ==> 362672 - Crash on start of digiKam. 073 ==> 341023 - Crash after startup during check for updated images. 074 ==> 386891 - Crashed while adding pictures. 075 ==> 342666 - digiKam crashes during find new items. 076 ==> 341274 - digiKam crash on startup. 077 ==> 343708 - Crash when scanning album. 078 ==> 332721 - Crash when reading a certain MP4 video file. 079 ==> 343736 - Crashes when rebuilding thumbnails from database. 080 ==> 346356 - digiKam crashes when adding 90.000 pictures to library. 081 ==> 343714 - digiKam crash when scanning for new items. 082 ==> 341091 - digiKam crashes when updating the MySQL database of a a hudge photo collection. 083 ==> 340879 - digiKam crashes after getting unexpected but reasonable output from libexiv2. 084 ==> 342712 - Crash on collection scanning. 085 ==> 356704 - digiKam still crashes while scanning a new photo directory and subdirs. 086 ==> 339269 - Segfault when opening a folder that contains unknown file types (mov, avi, xcf). 087 ==> 364635 - digiKam crashes on startup. 088 ==> 357356 - digiKam crash on startup while scanning photos. 089 ==> 341554 - digiKam crashed by Data-Import from NFS. 090 ==> 345457 - digiKam crashes at "loading tools". 091 ==> 349635 - Crash of digiKam - moving album. 092 ==> 342604 - digiKam crash. 093 ==> 331450 - Signal 8 on album opening. 094 ==> 342030 - digiKam crashes when checking an AVI video file using exiv2. 095 ==> 352777 - Crash during scan. 096 ==> 352944 - digiKam crashes on start. 097 ==> 343643 - digiKam crashes while perform initial scanning of custom photo folder. 098 ==> 342000 - digiKam crash when opening folder with Videos (Album or SD Card import). 099 ==> 353447 - digiKam crashes when scanning files. 100 ==> 346807 - Crashes on startup. 101 ==> 364639 - digiKam crashed while opening database. 102 ==> 341504 - Crash while using application. 103 ==> 367691 - Searching for pictures crashes at 30% every time. 104 ==> 334604 - Crash after changing disk partions. 105 ==> 351689 - Crash on opening digiKam. 106 ==> 149267 - digiKam crashes after finding avi and so on. 107 ==> 170387 - Add movies management. 108 ==> 369629 - digiKam does not use GPS data from video files. 109 ==> 367880 - Nexus 5X videos show up upside-down in digiKam. 110 ==> 330116 - digiKam does not take care about GPS info stored in MP4 video files. 111 ==> 339150 - digikam crashes when trying to display video file. 112 ==> 344406 - Crash at start. 113 ==> 339909 - digiKam Segmentation Fault on open. 114 ==> 343231 - Crash at scanning for new fotos. 115 ==> 340373 - Crash on scanning video directory. 116 ==> 134679 - Video and audio files are not imported. 117 ==> 375357 - No video preview. 118 ==> 261773 - Batch renaming does not complete when MP4 video file is processed. 119 ==> 185915 - Album View: "Created" time of video set to "00:00". 120 ==> 303218 - digiKam import crashes when you select video files. 121 ==> 374241 - Bad video date rename. 122 ==> 375646 - Be able to scan only photo, not video and audio. 123 ==> 262499 - Cannot rename .AVI files. 124 ==> 199261 - Import avi movies from sdcard wrong date and no thumbnail. 125 ==> 181521 - NEF's in descending order, AVI in ascending order in import from SD-card. 126 ==> 392019 - Two persons can point to the same face tag in pictures. 127 ==> 392013 - Metadata explorer does not show XMP face rectangles. 128 ==> 389508 - Dates Side Menu Is Not Updated Automatically After Exif Date Change [patch]. 129 ==> 331864 - Merge Tags with same name when moving to same hierarchy level. 130 ==> 347302 - Reassign name to face. 131 ==> 391747 - BQM Tool "Remove Metadata" doesn't remove all metadata from image. 132 ==> 285683 - Already imported pictures not recognized after daylight savings time. 133 ==> 392309 - Icons are pixelated when my display scale factor is 1.2 134 ==> 392405 - Function 'getImageIdsFromArea' argument order different. 135 ==> 386224 - Metadata is not updated when moving tags. 136 ==> 370245 - Be able to rename tags which have been setted in pictures. 137 ==> 374470 - Deleted tags are not removed from file metadata. 138 ==> 374516 - Persons metadata are not updated after a tag removed. 139 ==> 392436 - Count Files in directory. 140 ==> 363859 - digiKam core port from QWebKit to QWebEngine [patch]. 141 ==> 392427 - Cannot add collection on network drive. 142 ==> 392022 - Position of a face tag appears on top of bottom of the list, instead of being sorted alphabetically. 143 ==> 372763 - Rename does not give options on Conflict. 144 ==> 391533 - Feature request: add "NOT" tag matching condition in "Filters" panel. 145 ==> 381222 - digiKam crash on fuzzy search. 146 ==> 386275 - Crash caused by QtAV. 147 ==> 372342 - Face tag area is very short [patch]. 148 ==> 391348 - People Side Menu Shows Only Faces Not People Tagged Images. 149 ==> 385630 - Views Requiring Maps Takes ~30s to Launch. 150 ==> 192908 - Allow to split icon-view in order to show multiple albums at the same time. 151 ==> 339088 - GIT master: crash when clicking through images in preview, with face recognition running in background. 152 ==> 341605 - Crash if I attempt to use left-sidebar tags tab. 153 ==> 227266 - Handle Video Date from metadata. 154 ==> 227259 - Needs to Edit Video Date. 155 ==> 373284 - digiKam crashed with SIGSEGV in QSortFilterProxyModel::parent(). 156 ==> 384807 - digikam 5.7.0 AppImage bundle : provide a more recent ffmpeg version for video support. 157 ==> 391835 - Deleted pictures still appear in group. 158 ==> 387483 - Elegant theme: Selected frame colors swapped [patch]. 159 ==> 375424 - Thumbnails are not being removed from AlbumsView after moving the images to Trash. 160 ==> 368796 - Problem with Exif-tags: ImageDescription and UserComment. 161 ==> 392417 - AppImage (5.9.0-01-x86-64) does not support "--install" cli parameter. 162 ==> 392922 - digikam-6.0.0 fail to start. 163 ==> 391399 - Not possible to add location bookmarks in Digikam >5.6.0. 164 ==> 380876 - Tags in Digikam DB maintained after being removed from file and file re-scanned. 165 ==> 392017 - Merging, renaming and removing face tags. 166 ==> 352711 - Externally removed tags are not removed from digiKam. 167 ==> 393108 - Tags not always visible when selecting multiple pictures in a group. 168 ==> 392656 - Selecting a root album for face scan doesn't include subfolders, but rather scans an unexpected album set. 169 ==> 329438 - Rename function with Date & Time does not work with NTFS. 170 ==> 376473 - Can"t set empty IPTC country code when using metadata templates. 171 ==> 380289 - Cannot write to Albums residing on NFS. 172 ==> 384465 - With Compact Flash Card Created date in thumbnails is wrong. 173 ==> 381958 - Cannot add additional collection. 174 ==> 383747 - "Rotate only by setting a flag" Changes Image Instead. 175 ==> 387977 - No icon only view of "Extras sidebar": sidebar taking up a lot of space. 176 ==> 277502 - All versions of version set always displayed in Album view [patch]. 177 ==> 393283 - Caption not updating Exif.Image.ImageDescription field. 178 ==> 391060 - Crashes on undo of very large tif. 179 ==> 366305 - Add a message at startup about the lack of temporary space to perform Undo operations. 180 ==> 366391 - Rotating an image seems to forget to reset the orientation flag. 181 ==> 393654 - Not able to select gpx file. 182 ==> 367596 - Sub-folder count images but don't show them (unsupported JPEG file?). 183 ==> 379922 - Digikam won't remove tags set by Windows Explorer. 184 ==> 379081 - GPS data are in file but geolocation indicator is not shown and map view empty. 185 ==> 354819 - Specific pictures not showing up in digikam. 186 ==> 393855 - MySQL/MariaDB upgrade fails. 187 ==> 384603 - Camera Creation Date not set from EXIF data. 188 ==> 386959 - Properties view: wrong creation date [patch]. 189 ==> 393970 - No mts video thumbnails. 190 ==> 393728 - Reread metadata from Video uses sidecar only. 191 ==> 393925 - UpdateSchemaFromV7ToV9 fails due to duplicate key in album_2. 192 ==> 393773 - showfoto crashes when geotagging. 193 ==> 388199 - No context menu to copy coordinates from map. 194 ==> 393399 - Windows defender freaks out in windows 10 and Edge. 195 ==> 392134 - SIGSEGV While Scanning Faces [patch]. 196 ==> 394168 - OSM Search Yields No Results. 197 ==> 377719 - Cannot rename file with overwrite [patch]. 198 ==> 388002 - remove kio related legacy [patch] 199 ==> 394242 - Import settings unneccesarily asks to overwrite image database, and crashes when I decline. 200 ==> 394278 - A slideshow theme for kiosk mode. 201 ==> 340389 - digiKam crashes while editing pictures for color balancing on OSX [patch]. 202 ==> 394413 - Unify group handling [patch]. 203 ==> 394573 - Revers geodata from open street map does not work. 204 ==> 394590 - Feature request: being able to filter on all metadatas fields. -205 ==> - +205 ==> 394671 - Distortion on Panasonic DMC-LX15. +206 ==> 393205 - Advanced rename very slow. +207 ==> 382474 - Thumbnail regeneration. +208 ==> 394865 - digikam suspicious crash on exit. +209 ==> 390541 - Tooltip background cannot be changed. +210 ==> 391521 - "Tool-tip" box difficult to read due to default color scheme. +211 ==> 377849 - Albums disappear when the network is interrupted. +212 ==> diff --git a/core/app/items/digikamimageview.cpp b/core/app/items/digikamimageview.cpp index e940e1f680..93a0798daf 100644 --- a/core/app/items/digikamimageview.cpp +++ b/core/app/items/digikamimageview.cpp @@ -1,566 +1,564 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-04-24 * Description : Qt item view for images * * Copyright (C) 2009-2011 by Marcel Wiesweg * Copyright (C) 2009-2018 by Gilles Caulier * Copyright (C) 2011 by Andi Clemens * Copyright (C) 2013 by Michael G. Hansen * Copyright (C) 2014 by Mohamed_Anwer * Copyright (C) 2017 by Simon Frei * * 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, 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. * * ============================================================ */ #include "digikamimageview.h" #include "digikamimageview_p.h" // Qt includes #include #include #include #include #include // Local includes #include "digikam_debug.h" #include "albummanager.h" #include "coredb.h" #include "advancedrenamedialog.h" #include "advancedrenameprocessdialog.h" #include "applicationsettings.h" #include "assignnameoverlay.h" #include "contextmenuhelper.h" #include "coredbaccess.h" #include "ddragobjects.h" #include "digikamapp.h" #include "digikamimagedelegate.h" #include "digikamimagefacedelegate.h" #include "dio.h" #include "groupindicatoroverlay.h" #include "imagealbumfiltermodel.h" #include "imagealbummodel.h" #include "imagedragdrop.h" #include "imageratingoverlay.h" #include "imagefsoverlay.h" #include "imagecoordinatesoverlay.h" #include "tagslineeditoverlay.h" #include "imageviewutilities.h" #include "imagewindow.h" #include "fileactionmngr.h" #include "fileactionprogress.h" #include "thumbnailloadthread.h" #include "tagregion.h" #include "addtagslineedit.h" #include "facerejectionoverlay.h" #include "facetagsiface.h" namespace Digikam { DigikamImageView::DigikamImageView(QWidget* const parent) : ImageCategorizedView(parent), d(new Private(this)) { installDefaultModels(); d->editPipeline.plugDatabaseEditor(); d->editPipeline.plugTrainer(); d->editPipeline.construct(); connect(&d->editPipeline, SIGNAL(scheduled()), this, SLOT(slotInitProgressIndicator())); d->normalDelegate = new DigikamImageDelegate(this); d->faceDelegate = new DigikamImageFaceDelegate(this); setItemDelegate(d->normalDelegate); setSpacing(10); ApplicationSettings* const settings = ApplicationSettings::instance(); imageFilterModel()->setCategorizationMode(ImageSortSettings::CategoryByAlbum); imageAlbumModel()->setThumbnailLoadThread(ThumbnailLoadThread::defaultIconViewThread()); setThumbnailSize(ThumbnailSize(settings->getDefaultIconSize())); imageAlbumModel()->setPreloadThumbnails(true); imageModel()->setDragDropHandler(new ImageDragDropHandler(imageModel())); setDragEnabled(true); setAcceptDrops(true); setDropIndicatorShown(false); setToolTipEnabled(settings->showToolTipsIsValid()); imageFilterModel()->setStringTypeNatural(settings->isStringTypeNatural()); imageFilterModel()->setSortRole((ImageSortSettings::SortRole)settings->getImageSortOrder()); imageFilterModel()->setSortOrder((ImageSortSettings::SortOrder)settings->getImageSorting()); imageFilterModel()->setCategorizationMode((ImageSortSettings::CategorizationMode)settings->getImageSeparationMode()); imageFilterModel()->setCategorizationSortOrder((ImageSortSettings::SortOrder) settings->getImageSeparationSortOrder()); // selection overlay addSelectionOverlay(d->normalDelegate); addSelectionOverlay(d->faceDelegate); // rotation overlays d->rotateLeftOverlay = ImageRotateOverlay::left(this); d->rotateRightOverlay = ImageRotateOverlay::right(this); d->fullscreenOverlay = ImageFsOverlay::instance(this); d->updateOverlays(); // rating overlay ImageRatingOverlay* const ratingOverlay = new ImageRatingOverlay(this); addOverlay(ratingOverlay); // face overlays // NOTE: order to plug this overlay is important, else rejection cant be suitable (see bug #324759). addAssignNameOverlay(d->faceDelegate); addRejectionOverlay(d->faceDelegate); GroupIndicatorOverlay* const groupOverlay = new GroupIndicatorOverlay(this); addOverlay(groupOverlay); addOverlay(new ImageCoordinatesOverlay(this)); connect(ratingOverlay, SIGNAL(ratingEdited(QList,int)), this, SLOT(assignRating(QList,int))); connect(groupOverlay, SIGNAL(toggleGroupOpen(QModelIndex)), this, SLOT(groupIndicatorClicked(QModelIndex))); connect(groupOverlay, SIGNAL(showButtonContextMenu(QModelIndex,QContextMenuEvent*)), this, SLOT(showGroupContextMenu(QModelIndex,QContextMenuEvent*))); d->utilities = new ImageViewUtilities(this); connect(imageModel()->dragDropHandler(), SIGNAL(assignTags(QList,QList)), FileActionMngr::instance(), SLOT(assignTags(QList,QList))); connect(imageModel()->dragDropHandler(), SIGNAL(addToGroup(ImageInfo,QList)), FileActionMngr::instance(), SLOT(addToGroup(ImageInfo,QList))); connect(imageModel()->dragDropHandler(), SIGNAL(dragDropSort(ImageInfo,QList)), this, SLOT(dragDropSort(ImageInfo,QList))); connect(d->utilities, SIGNAL(editorCurrentUrlChanged(QUrl)), this, SLOT(setCurrentUrlWhenAvailable(QUrl))); connect(settings, SIGNAL(setupChanged()), this, SLOT(slotSetupChanged())); slotSetupChanged(); } DigikamImageView::~DigikamImageView() { delete d; } ImageViewUtilities* DigikamImageView::utilities() const { return d->utilities; } void DigikamImageView::setThumbnailSize(const ThumbnailSize& size) { imageThumbnailModel()->setPreloadThumbnailSize(size); ImageCategorizedView::setThumbnailSize(size); } ImageInfoList DigikamImageView::allImageInfos(bool grouping) const { if (grouping) { return resolveGrouping(ImageCategorizedView::allImageInfos()); } return ImageCategorizedView::allImageInfos(); } ImageInfoList DigikamImageView::selectedImageInfos(bool grouping) const { if (grouping) { return resolveGrouping(ImageCategorizedView::selectedImageInfos()); } return ImageCategorizedView::selectedImageInfos(); } ImageInfoList DigikamImageView::selectedImageInfosCurrentFirst(bool grouping) const { if (grouping) { return resolveGrouping(ImageCategorizedView::selectedImageInfosCurrentFirst()); } return ImageCategorizedView::selectedImageInfosCurrentFirst(); } void DigikamImageView::dragDropSort(const ImageInfo& pick, const QList& infos) { qCDebug(DIGIKAM_GENERAL_LOG) << "---lyj--- drag drop sort slot in digikam imageview"; ImageInfoList info_list = this->allImageInfos(false); int order_ = 0; for(auto iinfo: info_list) { qCDebug(DIGIKAM_GENERAL_LOG) << iinfo.name(); iinfo.setManualOrder(order_++); } qCDebug(DIGIKAM_GENERAL_LOG) << "---lyj--- picked image"; qCDebug(DIGIKAM_GENERAL_LOG) << pick.name(); qCDebug(DIGIKAM_GENERAL_LOG) << "---lyj--- infos image"; for(auto iinfo: infos) { qCDebug(DIGIKAM_GENERAL_LOG) << iinfo.name(); } } bool DigikamImageView::allNeedGroupResolving(const ApplicationSettings::OperationType type) const { return needGroupResolving(type, allImageInfos()); } bool DigikamImageView::selectedNeedGroupResolving(const ApplicationSettings::OperationType type) const { return needGroupResolving(type, selectedImageInfos()); } int DigikamImageView::fitToWidthIcons() { return delegate()->calculatethumbSizeToFit(viewport()->size().width()); } void DigikamImageView::slotSetupChanged() { imageFilterModel()->setStringTypeNatural(ApplicationSettings::instance()->isStringTypeNatural()); setToolTipEnabled(ApplicationSettings::instance()->showToolTipsIsValid()); setFont(ApplicationSettings::instance()->getIconViewFont()); d->updateOverlays(); ImageCategorizedView::slotSetupChanged(); } bool DigikamImageView::hasHiddenGroupedImages(const ImageInfo& info) const { return info.hasGroupedImages() && !imageFilterModel()->isGroupOpen(info.id()); } ImageInfoList DigikamImageView::imageInfos(const QList& indexes, ApplicationSettings::OperationType type) const { ImageInfoList infos = ImageCategorizedView::imageInfos(indexes); if (needGroupResolving(type, infos)) { return resolveGrouping(infos); } return infos; } void DigikamImageView::setFaceMode(bool on) { d->faceMode = on; if (on) { // See ImageLister, which creates a search the implements listing tag in the ioslave imageAlbumModel()->setSpecialTagListing(QLatin1String("faces")); setItemDelegate(d->faceDelegate); // grouping is not very much compatible with faces imageFilterModel()->setAllGroupsOpen(true); } else { imageAlbumModel()->setSpecialTagListing(QString()); setItemDelegate(d->normalDelegate); imageFilterModel()->setAllGroupsOpen(false); } } void DigikamImageView::addRejectionOverlay(ImageDelegate* delegate) { FaceRejectionOverlay* const rejectionOverlay = new FaceRejectionOverlay(this); connect(rejectionOverlay, SIGNAL(rejectFaces(QList)), this, SLOT(removeFaces(QList))); addOverlay(rejectionOverlay, delegate); } /* void DigikamImageView::addTagEditOverlay(ImageDelegate* delegate) { TagsLineEditOverlay* tagOverlay = new TagsLineEditOverlay(this); connect(tagOverlay, SIGNAL(tagEdited(QModelIndex,QString)), this, SLOT(assignTag(QModelIndex,QString))); addOverlay(tagOverlay, delegate); } */ void DigikamImageView::addAssignNameOverlay(ImageDelegate* delegate) { AssignNameOverlay* const nameOverlay = new AssignNameOverlay(this); addOverlay(nameOverlay, delegate); connect(nameOverlay, SIGNAL(confirmFaces(QList,int)), this, SLOT(confirmFaces(QList,int))); connect(nameOverlay, SIGNAL(removeFaces(QList)), this, SLOT(removeFaces(QList))); } void DigikamImageView::confirmFaces(const QList& indexes, int tagId) { QList infos; QList faces; QList sourceIndexes; // fast-remove in the "unknown person" view bool needFastRemove = false; if (imageAlbumModel()->currentAlbums().size() == 1) { needFastRemove = d->faceMode && (tagId != imageAlbumModel()->currentAlbums().first()->id()); } foreach (const QModelIndex& index, indexes) { infos << ImageModel::retrieveImageInfo(index); faces << d->faceDelegate->face(index); if (needFastRemove) { sourceIndexes << imageSortFilterModel()->mapToSourceImageModel(index); } } imageAlbumModel()->removeIndexes(sourceIndexes); for (int i = 0 ; i < infos.size() ; i++) { d->editPipeline.confirm(infos[i], faces[i], tagId); } } void DigikamImageView::removeFaces(const QList& indexes) { QList infos; QList faces; QList sourceIndexes; foreach (const QModelIndex& index, indexes) { infos << ImageModel::retrieveImageInfo(index); faces << d->faceDelegate->face(index); sourceIndexes << imageSortFilterModel()->mapToSourceImageModel(index); } imageAlbumModel()->removeIndexes(sourceIndexes); for (int i = 0 ; i < infos.size() ; i++) { d->editPipeline.remove(infos[i], faces[i]); } } void DigikamImageView::activated(const ImageInfo& info, Qt::KeyboardModifiers modifiers) { if (info.isNull()) { return; } if (modifiers != Qt::MetaModifier) { if (ApplicationSettings::instance()->getItemLeftClickAction() == ApplicationSettings::ShowPreview) { emit previewRequested(info); } else { openFile(info); } } else { d->utilities->openInfosWithDefaultApplication(QList() << info); } } void DigikamImageView::showContextMenuOnInfo(QContextMenuEvent* event, const ImageInfo& info) { emit signalShowContextMenuOnInfo(event, info, QList(), imageFilterModel()); } void DigikamImageView::showGroupContextMenu(const QModelIndex& index, QContextMenuEvent* event) { Q_UNUSED(index); emit signalShowGroupContextMenu(event, selectedImageInfosCurrentFirst(), imageFilterModel()); } void DigikamImageView::showContextMenu(QContextMenuEvent* event) { emit signalShowContextMenu(event); } void DigikamImageView::openFile(const ImageInfo& info) { d->utilities->openInfos(info, allImageInfos(), currentAlbum()); } void DigikamImageView::deleteSelected(const ImageViewUtilities::DeleteMode deleteMode) { ImageInfoList imageInfoList = selectedImageInfos(true); if (d->utilities->deleteImages(imageInfoList, deleteMode)) { awayFromSelection(); } } void DigikamImageView::deleteSelectedDirectly(const ImageViewUtilities::DeleteMode deleteMode) { ImageInfoList imageInfoList = selectedImageInfos(true); d->utilities->deleteImagesDirectly(imageInfoList, deleteMode); awayFromSelection(); } void DigikamImageView::assignRating(const QList& indexes, int rating) { ImageInfoList infos = imageInfos(indexes, ApplicationSettings::Metadata); FileActionMngr::instance()->assignRating(infos, rating); } void DigikamImageView::groupIndicatorClicked(const QModelIndex& index) { ImageInfo info = imageFilterModel()->imageInfo(index); if (info.isNull()) { return; } setCurrentIndex(index); imageFilterModel()->toggleGroupOpen(info.id()); imageAlbumModel()->ensureHasGroupedImages(info); } void DigikamImageView::rename() { ImageInfoList infos = selectedImageInfos(); if (needGroupResolving(ApplicationSettings::Rename, infos)) { infos = resolveGrouping(infos); } QList urls = infos.toImageUrlList(); bool loop = false; NewNamesList newNamesList; do { qCDebug(DIGIKAM_GENERAL_LOG) << "Selected URLs to rename: " << urls; QPointer dlg = new AdvancedRenameDialog(this); dlg->slotAddImages(urls); if (dlg->exec() != QDialog::Accepted) { delete dlg; break; } if (!loop) { QUrl nextUrl = nextInOrder(infos.last(), 1).fileUrl(); setCurrentUrl(nextUrl); loop = true; } newNamesList = dlg->newNames(); delete dlg; - setFocus(); if (!newNamesList.isEmpty()) { QPointer dlg = new AdvancedRenameProcessDialog(newNamesList, this); dlg->exec(); urls = dlg->failedUrls(); delete dlg; - setFocus(); } } while (!urls.isEmpty() && !newNamesList.isEmpty()); } void DigikamImageView::slotRotateLeft(const QList& indexes) { ImageInfoList infos = imageInfos(indexes, ApplicationSettings::Metadata); FileActionMngr::instance()->transform(infos, MetaEngineRotation::Rotate270); } void DigikamImageView::slotRotateRight(const QList& indexes) { ImageInfoList infos = imageInfos(indexes, ApplicationSettings::Metadata); FileActionMngr::instance()->transform(infos, MetaEngineRotation::Rotate90); } void DigikamImageView::slotFullscreen(const QList& indexes) { - QList infos = imageInfos(indexes); + QList infos = imageInfos(indexes, ApplicationSettings::Slideshow); if (infos.isEmpty()) { return; } // Just fullscreen the first. const ImageInfo& info = infos.at(0); emit fullscreenRequested(info); } void DigikamImageView::slotInitProgressIndicator() { if (!ProgressManager::instance()->findItembyId(QLatin1String("FaceActionProgress"))) { FileActionProgress* const item = new FileActionProgress(QLatin1String("FaceActionProgress")); connect(&d->editPipeline, SIGNAL(started(QString)), item, SLOT(slotProgressStatus(QString))); connect(&d->editPipeline, SIGNAL(progressValueChanged(float)), item, SLOT(slotProgressValue(float))); connect(&d->editPipeline, SIGNAL(finished()), item, SLOT(slotCompleted())); } } } // namespace Digikam diff --git a/core/app/views/tableview/tableview.cpp b/core/app/views/tableview/tableview.cpp index d452365f3a..4e2c8d1e69 100644 --- a/core/app/views/tableview/tableview.cpp +++ b/core/app/views/tableview/tableview.cpp @@ -1,648 +1,646 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2013-02-11 * Description : Table view * * Copyright (C) 2013 by Michael G. Hansen * Copyright (C) 2017 by Simon Frei * * 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, 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. * * ============================================================ */ #include "tableview.h" // Qt includes #include #include #include #include #include #include #include #include #include // Local includes #include "advancedrenamedialog.h" #include "advancedrenameprocessdialog.h" #include "applicationsettings.h" #include "contextmenuhelper.h" #include "digikam_debug.h" #include "fileactionmngr.h" #include "album.h" #include "imageviewutilities.h" #include "tableview_columnfactory.h" #include "tableview_model.h" #include "tableview_selection_model_syncer.h" #include "tableview_shared.h" #include "tableview_treeview.h" namespace Digikam { class ImageAlbumModel; class ImageFilterModel; class TableView::Private { public: explicit Private() : columnProfiles(), thumbnailSize(), imageViewUtilities(0) { } QList columnProfiles; ThumbnailSize thumbnailSize; ImageViewUtilities* imageViewUtilities; }; TableView::TableView(QItemSelectionModel* const selectionModel, DCategorizedSortFilterProxyModel* const imageFilterModel, QWidget* const parent) : QWidget(parent), StateSavingObject(this), d(new Private()), s(new TableViewShared()) { s->isActive = false; s->tableView = this; s->thumbnailLoadThread = new ThumbnailLoadThread(this); s->imageFilterModel = dynamic_cast(imageFilterModel); s->imageModel = dynamic_cast(imageFilterModel->sourceModel()); s->imageFilterSelectionModel = selectionModel; s->columnFactory = new TableViewColumnFactory(s.data(), this); QVBoxLayout* const vbox1 = new QVBoxLayout(); s->tableViewModel = new TableViewModel(s.data(), this); s->tableViewSelectionModel = new QItemSelectionModel(s->tableViewModel); s->tableViewSelectionModelSyncer = new TableViewSelectionModelSyncer(s.data(), this); s->treeView = new TableViewTreeView(s.data(), this); s->treeView->installEventFilter(this); d->imageViewUtilities = new ImageViewUtilities(this); connect(s->treeView, SIGNAL(activated(QModelIndex)), this, SLOT(slotItemActivated(QModelIndex))); connect(s->treeView, SIGNAL(signalZoomInStep()), this, SIGNAL(signalZoomInStep())); connect(s->treeView, SIGNAL(signalZoomOutStep()), this, SIGNAL(signalZoomOutStep())); connect(s->tableViewSelectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SIGNAL(signalItemsChanged())); connect(s->treeView, SIGNAL(collapsed(QModelIndex)), this, SIGNAL(signalItemsChanged())); connect(s->treeView, SIGNAL(expanded(QModelIndex)), this, SIGNAL(signalItemsChanged())); connect(s->tableViewModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SIGNAL(signalItemsChanged())); connect(s->tableViewModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SIGNAL(signalItemsChanged())); connect(s->tableViewModel, SIGNAL(layoutChanged()), this, SIGNAL(signalItemsChanged())); connect(s->tableViewModel, SIGNAL(modelReset()), this, SIGNAL(signalItemsChanged())); vbox1->addWidget(s->treeView); setLayout(vbox1); } TableView::~TableView() { } void TableView::doLoadState() { const KConfigGroup group = getConfigGroup(); TableViewColumnProfile profile; const KConfigGroup groupCurrentProfile = group.group("Current Profile"); profile.loadSettings(groupCurrentProfile); s->tableViewModel->loadColumnProfile(profile); const TableViewModel::GroupingMode groupingMode = TableViewModel::GroupingMode(group.readEntry("Grouping mode", int(TableViewModel::GroupingShowSubItems))); s->tableViewModel->setGroupingMode(groupingMode); if (!profile.headerState.isEmpty()) { s->treeView->header()->restoreState(profile.headerState); } } void TableView::doSaveState() { KConfigGroup group = getConfigGroup(); TableViewColumnProfile profile = s->tableViewModel->getColumnProfile(); profile.headerState = s->treeView->header()->saveState(); KConfigGroup groupCurrentProfile = group.group("Current Profile"); profile.saveSettings(groupCurrentProfile); group.writeEntry("Grouping mode", int(s->tableViewModel->groupingMode())); } void TableView::slotItemActivated(const QModelIndex& tableViewIndex) { const ImageInfo info = s->tableViewModel->imageInfo(tableViewIndex); if (info.isNull()) { return; } if (qApp->queryKeyboardModifiers() != Qt::MetaModifier) { if (ApplicationSettings::instance()->getItemLeftClickAction() == ApplicationSettings::ShowPreview) { emit signalPreviewRequested(info); } else { d->imageViewUtilities->openInfos(info, allImageInfos(), currentAlbum()); } } else { d->imageViewUtilities->openInfosWithDefaultApplication(QList() << info); } } bool TableView::eventFilter(QObject* watched, QEvent* event) { // we are looking for context menu events for the table view if ((watched == s->treeView) && (event->type() == QEvent::ContextMenu)) { QContextMenuEvent* const e = static_cast(event); e->accept(); const QModelIndex contextMenuIndex = s->treeView->indexAt(e->pos()); if (contextMenuIndex.isValid()) { emit signalShowContextMenuOnInfo( e, s->tableViewModel->imageInfo(contextMenuIndex), getExtraGroupingActions()); } else { emit signalShowContextMenu(e, getExtraGroupingActions()); } // event has been filtered by us return true; } return QObject::eventFilter(watched, event); } void TableView::setThumbnailSize(const ThumbnailSize& size) { d->thumbnailSize = size; const QList columnObjects = s->tableViewModel->getColumnObjects(); foreach(TableViewColumn* const iColumn, columnObjects) { iColumn->updateThumbnailSize(); } } ThumbnailSize TableView::getThumbnailSize() const { return d->thumbnailSize; } Album* TableView::currentAlbum() const { ImageAlbumModel* const albumModel = qobject_cast(s->imageModel); if (!albumModel) { return 0; } if (albumModel->currentAlbums().isEmpty()) { return 0; } return albumModel->currentAlbums().first(); } void TableView::slotPaste() { DragDropViewImplementation* const dragDropViewImplementation = s->treeView; dragDropViewImplementation->paste(); } ImageInfo TableView::currentInfo() const { return s->tableViewModel->imageInfo(s->tableViewSelectionModel->currentIndex()); } ImageInfoList TableView::allImageInfos(bool grouping) const { if (grouping) { return s->treeView->resolveGrouping(s->tableViewModel->allImageInfo()); } return s->tableViewModel->allImageInfo(); } bool TableView::allNeedGroupResolving(const ApplicationSettings::OperationType type) const { return s->treeView->needGroupResolving(type, allImageInfos()); } bool TableView::selectedNeedGroupResolving(const ApplicationSettings::OperationType type) const { return s->treeView->needGroupResolving(type, selectedImageInfos()); } void TableView::slotDeleteSelected(const ImageViewUtilities::DeleteMode deleteMode) { const ImageInfoList infoList = selectedImageInfos(true); /// @todo Update parameter naming for deleteImages if (d->imageViewUtilities->deleteImages(infoList, deleteMode)) { slotAwayFromSelection(); } } void TableView::slotDeleteSelectedWithoutConfirmation(const ImageViewUtilities::DeleteMode deleteMode) { const ImageInfoList infoList = selectedImageInfos(true); d->imageViewUtilities->deleteImagesDirectly(infoList, deleteMode); slotAwayFromSelection(); } QList TableView::getExtraGroupingActions() { QList actionList; const TableViewModel::GroupingMode currentGroupingMode = s->tableViewModel->groupingMode(); QAction* const actionHideGrouped = new QAction(i18n("Hide grouped items"), this); actionHideGrouped->setCheckable(true); actionHideGrouped->setChecked(currentGroupingMode == TableViewModel::GroupingHideGrouped); actionHideGrouped->setData(QVariant::fromValue(TableViewModel::GroupingHideGrouped)); connect(actionHideGrouped, SIGNAL(triggered(bool)), this, SLOT(slotGroupingModeActionTriggered())); actionList << actionHideGrouped; QAction* const actionIgnoreGrouping = new QAction(i18n("Ignore grouping"), this); actionIgnoreGrouping->setCheckable(true); actionIgnoreGrouping->setChecked(currentGroupingMode == TableViewModel::GroupingIgnoreGrouping); actionIgnoreGrouping->setData(QVariant::fromValue(TableViewModel::GroupingIgnoreGrouping)); connect(actionIgnoreGrouping, SIGNAL(triggered(bool)), this, SLOT(slotGroupingModeActionTriggered())); actionList << actionIgnoreGrouping; QAction* const actionShowSubItems = new QAction(i18n("Show grouping in tree"), this); actionShowSubItems->setCheckable(true); actionShowSubItems->setChecked(currentGroupingMode == TableViewModel::GroupingShowSubItems); actionShowSubItems->setData(QVariant::fromValue(TableViewModel::GroupingShowSubItems)); connect(actionShowSubItems, SIGNAL(triggered(bool)), this, SLOT(slotGroupingModeActionTriggered())); actionList << actionShowSubItems; return actionList; } void TableView::slotGroupingModeActionTriggered() { const QAction* const senderAction = qobject_cast(sender()); if (!senderAction) { return; } const TableViewModel::GroupingMode newGroupingMode = senderAction->data().value(); s->tableViewModel->setGroupingMode(newGroupingMode); } int TableView::numberOfSelectedItems() const { return s->tableViewSelectionModel->selectedRows().count(); } void TableView::slotGoToRow(const int rowNumber, const bool relativeMove) { int nextDeepRowNumber = rowNumber; if (relativeMove) { const QModelIndex currentTableViewIndex = s->tableViewSelectionModel->currentIndex(); const int currentDeepRowNumber = s->tableViewModel->indexToDeepRowNumber(currentTableViewIndex); nextDeepRowNumber += currentDeepRowNumber; } const QModelIndex nextIndex = s->tableViewModel->deepRowIndex(nextDeepRowNumber); if (nextIndex.isValid()) { const QItemSelection rowSelection = s->tableViewSelectionModelSyncer->targetIndexToRowItemSelection(nextIndex); s->tableViewSelectionModel->select(rowSelection, QItemSelectionModel::ClearAndSelect); s->tableViewSelectionModel->setCurrentIndex(nextIndex, QItemSelectionModel::Select); } } ImageInfo TableView::deepRowImageInfo(const int rowNumber, const bool relative) const { int targetRowNumber = rowNumber; if (relative) { const QModelIndex& currentTableViewIndex = s->tableViewSelectionModel->currentIndex(); if (!currentTableViewIndex.isValid()) { return ImageInfo(); } const int currentDeepRowNumber = s->tableViewModel->indexToDeepRowNumber(currentTableViewIndex); targetRowNumber += currentDeepRowNumber; } const QModelIndex targetIndex = s->tableViewModel->deepRowIndex(targetRowNumber); return s->tableViewModel->imageInfo(targetIndex); } ImageInfo TableView::nextInfo() const { const QModelIndex cIndex = s->tableViewSelectionModel->currentIndex(); const int currentDeepRowNumber = s->tableViewModel->indexToDeepRowNumber(cIndex); const int nextDeepRowNumber = currentDeepRowNumber + 1; if (nextDeepRowNumber>=s->tableViewModel->deepRowCount()) { return ImageInfo(); } const QModelIndex nextDeepRowIndex = s->tableViewModel->deepRowIndex(nextDeepRowNumber); return s->tableViewModel->imageInfo(nextDeepRowIndex); } ImageInfo TableView::previousInfo() const { const QModelIndex cIndex = s->tableViewSelectionModel->currentIndex(); const int currentDeepRowNumber = s->tableViewModel->indexToDeepRowNumber(cIndex); const int previousDeepRowNumber = currentDeepRowNumber - 1; if (previousDeepRowNumber < 0) { return ImageInfo(); } const QModelIndex previousDeepRowIndex = s->tableViewModel->deepRowIndex(previousDeepRowNumber); return s->tableViewModel->imageInfo(previousDeepRowIndex); } void TableView::slotSetCurrentWhenAvailable(const qlonglong id) { const QModelIndex idx = s->tableViewModel->indexFromImageId(id, 0); if (!idx.isValid()) { /// @todo Actually buffer this request until the model is fully populated return; } s->tableViewSelectionModel->setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect); } /** * @brief Unselects the current selection and changes the current item * * @todo This may not work correctly if grouped items are deleted, but are not selected */ void TableView::slotAwayFromSelection() { QModelIndexList selection = s->tableViewSelectionModel->selectedRows(0); if (selection.isEmpty()) { return; } const QModelIndex firstIndex = s->tableViewModel->deepRowIndex(0); const QModelIndex lastIndex = s->tableViewModel->deepRowIndex(-1); if (selection.contains(firstIndex) && selection.contains(lastIndex)) { // both the first and the last index are selected, we have to // select an index inbetween const int nextFreeDeepRow = s->tableViewModel->firstDeepRowNotInList(selection); if (nextFreeDeepRow < 0) { s->tableViewSelectionModel->clearSelection(); s->tableViewSelectionModel->setCurrentIndex(QModelIndex(), QItemSelectionModel::ClearAndSelect); } else { const QModelIndex nextFreeIndex = s->tableViewModel->deepRowIndex(nextFreeDeepRow); s->tableViewSelectionModel->setCurrentIndex(nextFreeIndex, QItemSelectionModel::ClearAndSelect); const QItemSelection nextFreeIndexAsRow = s->tableViewSelectionModelSyncer->targetIndexToRowItemSelection(nextFreeIndex); s->tableViewSelectionModel->select(nextFreeIndexAsRow, QItemSelectionModel::ClearAndSelect); } } else if (selection.contains(lastIndex)) { const int firstSelectedRowNumber = s->tableViewModel->indexToDeepRowNumber(selection.first()); const QModelIndex newIndex = s->tableViewModel->deepRowIndex(firstSelectedRowNumber-1); s->tableViewSelectionModel->setCurrentIndex(newIndex, QItemSelectionModel::ClearAndSelect); const QItemSelection newIndexAsRow = s->tableViewSelectionModelSyncer->targetIndexToRowItemSelection(newIndex); s->tableViewSelectionModel->select(newIndexAsRow, QItemSelectionModel::ClearAndSelect); } else { const int lastSelectedRowNumber = s->tableViewModel->indexToDeepRowNumber(selection.last()); const QModelIndex newIndex = s->tableViewModel->deepRowIndex(lastSelectedRowNumber+1); s->tableViewSelectionModel->setCurrentIndex(newIndex, QItemSelectionModel::ClearAndSelect); const QItemSelection newIndexAsRow = s->tableViewSelectionModelSyncer->targetIndexToRowItemSelection(newIndex); s->tableViewSelectionModel->select(newIndexAsRow, QItemSelectionModel::ClearAndSelect); } } void TableView::clearSelection() { s->tableViewSelectionModel->clearSelection(); } void TableView::invertSelection() { const int deepRowCount = s->tableViewModel->deepRowCount(); QList rowsToSelect; int lastSelectedRow = -1; /// @todo Create a DeepRowIterator because there is a lot of overhead here for (int i = 0; i < deepRowCount; ++i) { const QModelIndex iIndex = s->tableViewModel->deepRowIndex(i); if (s->tableViewSelectionModel->isSelected(iIndex)) { if (i - 1 > lastSelectedRow) { for (int j = lastSelectedRow + 1; j < i; ++j) { rowsToSelect << j; } } lastSelectedRow = i; } } if (lastSelectedRow + 1 < deepRowCount) { for (int j = lastSelectedRow + 1; j < deepRowCount; ++j) { rowsToSelect << j; } } s->tableViewSelectionModel->clearSelection(); foreach(const int i, rowsToSelect) { const QModelIndex iIndex = s->tableViewModel->deepRowIndex(i); const QItemSelection is = s->tableViewSelectionModelSyncer->targetIndexToRowItemSelection(iIndex); s->tableViewSelectionModel->select(is, QItemSelectionModel::Select); } } void TableView::selectAll() { /// @todo This only selects expanded items. s->treeView->selectAll(); } void TableView::slotSetActive(const bool isActive) { if (s->isActive != isActive) { s->isActive = isActive; s->tableViewModel->slotSetActive(isActive); s->tableViewSelectionModelSyncer->slotSetActive(isActive); } } ImageInfoList TableView::selectedImageInfos(bool grouping) const { ImageInfoList infos = s->tableViewModel->imageInfos(s->tableViewSelectionModel->selectedRows()); if (grouping) { return s->treeView->resolveGrouping(infos); } return infos; } ImageInfoList TableView::selectedImageInfosCurrentFirst(bool grouping) const { QModelIndexList indexes = s->tableViewSelectionModel->selectedRows(); const QModelIndex current = s->tableViewModel->toCol0(s->tableViewSelectionModel->currentIndex()); if (!indexes.isEmpty()) { if (indexes.first() != current) { if (indexes.removeOne(current)) { indexes.prepend(current); } } } if (grouping) { return s->treeView->resolveGrouping(s->tableViewModel->imageInfos(indexes)); } return s->tableViewModel->imageInfos(indexes); } void TableView::rename() { ImageInfoList infos = selectedImageInfos(); if (s->treeView->needGroupResolving(ApplicationSettings::Rename, infos)) { infos = s->treeView->resolveGrouping(infos); } QList urls = infos.toImageUrlList(); bool loop = false; NewNamesList newNamesList; do { qCDebug(DIGIKAM_GENERAL_LOG) << "Selected URLs to rename: " << urls; QPointer dlg = new AdvancedRenameDialog(this); dlg->slotAddImages(urls); if (dlg->exec() != QDialog::Accepted) { delete dlg; break; } if (!loop) { slotAwayFromSelection(); loop = true; } newNamesList = dlg->newNames(); delete dlg; - setFocus(); if (!newNamesList.isEmpty()) { QPointer dlg = new AdvancedRenameProcessDialog(newNamesList, this); dlg->exec(); urls = dlg->failedUrls(); delete dlg; - setFocus(); } } while (!urls.isEmpty() && !newNamesList.isEmpty()); } } // namespace Digikam diff --git a/core/data/htmlgallery/themes/bluecurved/bluecurved.desktop b/core/data/htmlgallery/themes/bluecurved/bluecurved.desktop index 4e76ccbcba..7a1b22e334 100644 --- a/core/data/htmlgallery/themes/bluecurved/bluecurved.desktop +++ b/core/data/htmlgallery/themes/bluecurved/bluecurved.desktop @@ -1,147 +1,148 @@ [Desktop Entry] Type=Theme Name=Blue curves Name[ca]=Corbes blaves Name[ca@valencia]=Corbes blaves Name[cs]=Modré křivky Name[el]=Μπλε καμπύλες Name[en_GB]=Blue curves Name[es]=Curvas azules Name[fr]=Courbes bleues Name[gl]=Curvas azuis Name[is]=Bláar sveigjur Name[it]=Curve blu Name[nl]=Blauwe krommen Name[nn]=Blå kurver Name[pl]=Niebieskie krzywe Name[pt]=Curvas azuis Name[ru]=Синие изгибы Name[sv]=Blå kurvor Name[tr]=Mavi eğriler Name[uk]=Сині криві Name[x-test]=xxBlue curvesxx Name[zh_CN]=蓝色曲线 Comment=A stylised theme Comment[ca]=Un tema estilitzat Comment[ca@valencia]=Un tema estilitzat +Comment[cs]=Stylizovaný motiv Comment[el]=Ένα στιλιστικό θέμα Comment[en_GB]=A stylised theme Comment[es]=Un tema estilizado Comment[fr]=Un thème stylisé Comment[gl]=Un tema estilizado Comment[is]=Stílíserað þema Comment[it]=Tema stilizzato Comment[nl]=Een gestileerd thema Comment[nn]=Eit stilisert tema Comment[pl]=Stylizowany wygląd Comment[pt]=Um tema com estilo Comment[ru]=Стилизованное оформление Comment[sv]=Ett stiliserat tema Comment[tr]=Stilleştirilmiş bir tema Comment[uk]=Стилізована тема Comment[x-test]=xxA stylised themexx [X-HTMLGallery Preview] Name=Blue curves Name[ca]=Corbes blaves Name[ca@valencia]=Corbes blaves Name[cs]=Modré křivky Name[el]=Μπλε καμπύλες Name[en_GB]=Blue curves Name[es]=Curvas azules Name[fr]=Courbes bleues Name[gl]=Curvas azuis Name[is]=Bláar sveigjur Name[it]=Curve blu Name[nl]=Blauwe krommen Name[nn]=Blå kurver Name[pl]=Niebieskie krzywe Name[pt]=Curvas azuis Name[ru]=Синие изгибы Name[sv]=Blå kurvor Name[tr]=Mavi eğriler Name[uk]=Сині криві Name[x-test]=xxBlue curvesxx Name[zh_CN]=蓝色曲线 Url=preview.png [X-HTMLGallery Author] Name=Vincent Deroo Blanquart Name[ca]=Vincent Deroo Blanquart Name[ca@valencia]=Vincent Deroo Blanquart Name[cs]=Vincent Deroo Blanquart Name[de]=Vincent Deroo Blanquart Name[el]=Vincent Deroo Blanquart Name[en_GB]=Vincent Deroo Blanquart Name[es]=Vincent Deroo Blanquart Name[fr]=Vincent Deroo Blanquart Name[gl]=Vincent Deroo Blanquart Name[is]=Vincent Deroo Blanquart Name[it]=Vincent Deroo Blanquart Name[nl]=Vincent Deroo Blanquart Name[nn]=Vincent Deroo Blanquart Name[pl]=Vincent Deroo Blanquart Name[pt]=Vincent Deroo Blanquart Name[ru]=Vincent Deroo Blanquart Name[sv]=Vincent Deroo Blanquart Name[tr]=Vincent Deroo Blanquart Name[uk]=Vincent Deroo Blanquart Name[x-test]=xxVincent Deroo Blanquartxx Url=mailto:vincent.deroo@free.fr [X-HTMLGallery Parameter TitleOfPage] Name=Gallery Introduction Name[ca]=Introducció a galeries Name[ca@valencia]=Introducció a galeries Name[cs]=Představení galerie Name[de]=Galerie-Einführung Name[el]=Εισαγωγή στη συλλογή Name[en_GB]=Gallery Introduction Name[es]=Introducción a la galería Name[fr]=Introduction de galerie Name[gl]=Introdución á galería Name[is]=Kynning á myndasafni Name[it]=Introduzione galleria Name[nb]=Introduksjon til Gallery Name[nl]=Introductie tot de galerij Name[nn]=Galleri-introduksjon Name[pl]=Wprowadzenie do galerii Name[pt]=Introdução à Galeria Name[ru]=Приветствие при открытии галереи Name[sv]=Introduktion till Galleri Name[tr]=Galeri Tanıtımı Name[uk]=Вступ до галереї Name[x-test]=xxGallery Introductionxx Name[zh_CN]=相册介绍 Type=string Default=Title of my web gallery [X-HTMLGallery Parameter thumbnailPerRow] Name=Thumbnails per row Name[ca]=Miniatures per fila Name[ca@valencia]=Miniatures per fila Name[cs]=Náhledy na řádek Name[de]=Vorschaubilder pro Reihe Name[el]=Προεπισκόπηση ανά γραμμή Name[en_GB]=Thumbnails per row Name[es]=Miniaturas por fila Name[fr]=Vignettes par ligne Name[gl]=Miniaturas por fila Name[is]=Smámyndir í hverri röð Name[it]=Miniature per riga Name[nb]=Miniatyrer pr. rad Name[nl]=Miniaturen per rij Name[nn]=Miniatyrbilete per rad Name[pl]=Miniatur na wiersz Name[pt]=Miniaturas por linha Name[ru]=Миниатюр в ряду Name[sv]=Miniatyrbilder per rad Name[tr]=Satır başına küçük resimler Name[uk]=Мініатюр на ряд Name[x-test]=xxThumbnails per rowxx Name[zh_CN]=每行缩略图 Type=int Default=4 Min=1 Max=100 diff --git a/core/data/htmlgallery/themes/blueframes/blueframes.desktop b/core/data/htmlgallery/themes/blueframes/blueframes.desktop index df99a1cdff..29809eba4e 100644 --- a/core/data/htmlgallery/themes/blueframes/blueframes.desktop +++ b/core/data/htmlgallery/themes/blueframes/blueframes.desktop @@ -1,69 +1,71 @@ [Desktop Entry] Type=Theme Name=Blue Frames Name[ca]=Marcs blaus Name[ca@valencia]=Marcs blaus +Name[cs]=Modré rámce Name[en_GB]=Blue Frames Name[es]=Marcos azules Name[gl]=Marcos azuis Name[nl]=Blauwe frames Name[nn]=Blå rammer Name[pl]=Niebieskie ramki Name[pt]=Molduras Azuis Name[sv]=Blåa ramar Name[uk]=Сині рамки Name[x-test]=xxBlue Framesxx Comment=A theme variation based Frames theme Comment[ca]=Una variació del tema basat en el tema Marcs Comment[ca@valencia]=Una variació del tema basat en el tema Marcs Comment[en_GB]=A theme variation based Frames theme Comment[es]=Un tema basado en una variación del tema Marcos Comment[gl]=Unha variación de tema baseado no tema de marcos Comment[nl]=Een themavariatie gebaseerd op Frames-thema Comment[nn]=Temavariasjon av «Rammer»-temaet Comment[pl]=Wariacja wyglądu oparta na wyglądzie ramek Comment[pt]=Uma variação de temas, baseado no tema Molduras Comment[sv]=En temavariant baserad på temat Ramar Comment[uk]=Варіант теми на основі теми «Рамки» Comment[x-test]=xxA theme variation based Frames themexx [X-HTMLGallery Author] Name=Elizabeth Marmorstein Name[ca]=Elizabeth Marmorstein Name[ca@valencia]=Elizabeth Marmorstein Name[cs]=Elizabeth Marmorstein Name[de]=Elizabeth Marmorstein Name[el]=Elizabeth Marmorstein Name[en_GB]=Elizabeth Marmorstein Name[es]=Elizabeth Marmorstein Name[fr]=Elizabeth Marmorstein Name[gl]=Elizabeth Marmorstein Name[is]=Elizabeth Marmorstein Name[it]=Elizabeth Marmorstein Name[nb]=Elizabeth Marmorstein Name[nl]=Elizabeth Marmorstein Name[nn]=Elizabeth Marmorstein Name[pl]=Elizabeth Marmorstein Name[pt]=Elizabeth Marmorstein Name[ru]=Elizabeth Marmorstein Name[sv]=Elizabeth Marmorstein Name[tr]=Elizabeth Marmorstein Name[uk]=Elizabeth Marmorstein Name[x-test]=xxElizabeth Marmorsteinxx Url=mailto:purplegamba@cox.net [X-HTMLGallery Preview] Name=Blue Frames Name[ca]=Marcs blaus Name[ca@valencia]=Marcs blaus +Name[cs]=Modré rámce Name[en_GB]=Blue Frames Name[es]=Marcos azules Name[gl]=Marcos azuis Name[nl]=Blauwe frames Name[nn]=Blå rammer Name[pl]=Niebieskie ramki Name[pt]=Molduras Azuis Name[sv]=Blåa ramar Name[uk]=Сині рамки Name[x-test]=xxBlue Framesxx Url=preview.png diff --git a/core/data/htmlgallery/themes/simpleslides/simpleslides.desktop b/core/data/htmlgallery/themes/simpleslides/simpleslides.desktop index 31b40c1434..da76bff06d 100644 --- a/core/data/htmlgallery/themes/simpleslides/simpleslides.desktop +++ b/core/data/htmlgallery/themes/simpleslides/simpleslides.desktop @@ -1,84 +1,95 @@ [Desktop Entry] Type=Theme Name=Simple Slideshow Name[ca]=Passi de diapositives senzill Name[ca@valencia]=Passe de diapositives senzill +Name[cs]=Jednoduché promítání +Name[en_GB]=Simple Slideshow Name[es]=Presentación sencilla Name[gl]=Presentación simple Name[nl]=Eenvoudige diavoorstelling Name[pt]=Apresentação Simples Name[sv]=Enkelt bildspel Name[uk]=Простий показ слайдів Name[x-test]=xxSimple Slideshowxx Comment=A slideshow designed for a kiosk-mode display. Generated from a simple template with javascript and css embedded into a single index.html file. Comment[ca]=Un passi de diapositives dissenyat per a una pantalla en mode kiosk. Generat des d'una plantilla senzilla amb Javascript i CSS incrustat en un únic fitxer «index.html». Comment[ca@valencia]=Un passe de diapositives dissenyat per a una pantalla en mode kiosk. Generat des d'una plantilla senzilla amb Javascript i CSS incrustat en un únic fitxer «index.html». +Comment[en_GB]=A slideshow designed for a kiosk-mode display. Generated from a simple template with javascript and CSS embedded into a single index.html file. Comment[es]=Una presentación diseñada para una pantalla en modo kiosko. Generada a partir de una sencilla plantilla con JavaScript y CSS integrados en un único archivo index.html. Comment[gl]=Unha presentación deseñada para unha pantalla en modo de quiosco. Xerada a partir dun modelo simple con JavaScript e CSS incrustado nun único ficheiro index.html. Comment[nl]=Een diavoorstelling ontworpen voor een weergave in modus kiosk. Gegenereerd uit een eenvoudig sjabloon met javascript en css, ingebed in een enkel index.html bestand. Comment[pt]=Uma apresentação desenhada para uma visualização em modo quiosque. Gerada a partir de um modelo simples com JavaScript e CSS incorporados num único ficheiro 'index.html'. Comment[sv]=Ett bildspel konstruerat för en kioskskärm. Skapad från en enkel mall med Javascript och CSS inbäddade i en enda index.html-fil. Comment[uk]=Показ слайдів, який створено для перегляду у режимі кіоску. Засновано на простому шаблоні із javascript та css, які вбудовано до єдиного файла index.html. Comment[x-test]=xxA slideshow designed for a kiosk-mode display. Generated from a simple template with javascript and css embedded into a single index.html file.xx [X-HTMLGallery Author] Name=Simon Kuss Name[ca]=Simon Kuss Name[ca@valencia]=Simon Kuss +Name[cs]=Simon Kuss +Name[en_GB]=Simon Kuss Name[es]=Simon Kuss Name[gl]=Simon Kuss Name[nl]=Simon Kuss Name[pt]=Simon Kuss Name[sv]=Simon Kuss Name[uk]=Simon Kuss Name[x-test]=xxSimon Kussxx Url=mailto:sjk281@iinet.net.au [X-HTMLGallery Parameter delay] Name=Time between slides (seconds) Name[ca]=Temps entre diapositives (segons) Name[ca@valencia]=Temps entre diapositives (segons) +Name[cs]=Interval mezi obrázky (v sekundách) +Name[en_GB]=Time between slides (seconds) Name[es]=Tiempo entre diapositivas (segundos) Name[gl]=Tempo entre diapositivas (segundos) Name[nl]=Tijd tussen dia's (seconden) Name[pt]=Tempo entre 'slides' (segundos) Name[sv]=Tid mellan bilder (sekunder) Name[uk]=Інтервал між слайдами (у секундах) Name[x-test]=xxTime between slides (seconds)xx Type=int Default=5 Min=1 Max=300 [X-HTMLGallery Parameter controls] Name=Location of controls Name[ca]=Ubicació dels controls Name[ca@valencia]=Ubicació dels controls +Name[cs]=Umístění ovládání +Name[en_GB]=Location of controls Name[es]=Ubicación de los controles Name[gl]=Situación dos controis Name[nl]=Locatie van besturing Name[pt]=Localização dos controlos Name[sv]=Plats för kontroller Name[uk]=Розташування засобів керування Name[x-test]=xxLocation of controlsxx Type=list Default=top Value-0=top Caption-0=Top Value-1=bottom Caption-1=Bottom Value-2=none Caption-2=None [X-HTMLGallery Preview] Name=Simple Slideshow Name[ca]=Passi de diapositives senzill Name[ca@valencia]=Passe de diapositives senzill +Name[cs]=Jednoduché promítání +Name[en_GB]=Simple Slideshow Name[es]=Presentación sencilla Name[gl]=Presentación simple Name[nl]=Eenvoudige diavoorstelling Name[pt]=Apresentação Simples Name[sv]=Enkelt bildspel Name[uk]=Простий показ слайдів Name[x-test]=xxSimple Slideshowxx Url=preview.png diff --git a/core/data/htmlgallery/themes/vanilla/vanilla.desktop b/core/data/htmlgallery/themes/vanilla/vanilla.desktop index 0437d72b26..7b75d24c3a 100644 --- a/core/data/htmlgallery/themes/vanilla/vanilla.desktop +++ b/core/data/htmlgallery/themes/vanilla/vanilla.desktop @@ -1,311 +1,317 @@ [Desktop Entry] Name=Vanilla Name[ca]=Vainilla Name[ca@valencia]=Vainilla Name[en_GB]=Vanilla Name[es]=Vainilla Name[gl]=Estándar Name[nl]=Vanilla Name[nn]=Vanilje Name[pl]=Niezmieniony Name[pt]=Limpo Name[sv]=Ordinär Name[uk]=Ванілла Name[x-test]=xxVanillaxx Type=Theme Comment=This theme is based on the look of Adobe Lightroom's HTML export. It has a simple but usable grid layout and comes with various color presets. Comment[ca]=Aquest tema està basat en l'aparença de l'exportació HTML del Lightroom de l'Adobe. Té una disposició de quadrícula senzilla però útil i es distribueix amb diverses predefinicions de colors. Comment[ca@valencia]=Aquest tema està basat en l'aparença de l'exportació HTML del Lightroom de l'Adobe. Té una disposició de quadrícula senzilla però útil i es distribueix amb diverses predefinicions de colors. Comment[en_GB]=This theme is based on the look of Adobe Lightroom's HTML export. It has a simple but usable grid layout and comes with various colour presets. Comment[es]=Este tema está basado en el aspecto de la exportación HTML de Adobe Lightroom. Contiene una simple aunque usable distribución en rejilla y dispone de varios preajustes de color. Comment[gl]=Este tema está baseado na aparencia da exportación a HTML de Adobe Lightroom. Ten unha disposición de grade simple pero fácil de usar e inclúe varias predefinicións de cores. Comment[nl]=Dit thema is gebaseerd oo het uiterlijk van Adobe Lightroom's HTML export. Het heeft een eenvoudige maar bruikbare rasterindeling en komt met van te voren ingestelde verschillende kleuren. Comment[nn]=Inspirert av HTML-eksporten til Adobe Lightroom. Det har ei enkel men praktisk rutenettbasert utforming og ulike fargetema. Comment[pl]=Ten wygląd opiera się na wyglądzie eksportu HTML Adobe Lightroom. Ma prosty lecz użyteczny układ siatki i jest dostępny w wielu wariantach kolorystycznych Comment[pt]=Este tema é baseado na aparência da exportação para HTML do Adobe Lightroom. Tem uma disposição em grelha simples mas útil e vem com diversas predefinições de cores. Comment[sv]=Temat är baserat på utseendet hos HTML-export i Adobe Lightroom. Det har en enkelt men användbar rutlayout och levereras med diverse förinställningar av färger. Comment[uk]=Цю тему засновано на вигляді експортованих даних Adobe Lightroom у форматі HTML. Вона проста, але має зручне табличне компонування і містить різноманітні попередньо визначені набори кольорів. Comment[x-test]=xxThis theme is based on the look of Adobe Lightroom's HTML export. It has a simple but usable grid layout and comes with various color presets.xx [X-HTMLGallery Author] Name=Wojciech Jarosz Name[ca]=Wojciech Jarosz Name[ca@valencia]=Wojciech Jarosz Name[cs]=Wojciech Jarosz Name[de]=Wojciech Jarosz Name[el]=Wojciech Jarosz Name[en_GB]=Wojciech Jarosz Name[es]=Wojciech Jarosz Name[fr]=Wojciech Jarosz Name[gl]=Wojciech Jarosz Name[is]=Wojciech Jarosz Name[it]=Wojciech Jarosz Name[nl]=Wojciech Jarosz Name[nn]=Wojciech Jarosz Name[pl]=Wojciech Jarosz Name[pt]=Wojciech Jarosz Name[ru]=Wojciech Jarosz Name[sv]=Wojciech Jarosz Name[tr]=Wojciech Jarosz Name[uk]=Wojciech Jarosz Name[x-test]=xxWojciech Jaroszxx Url=mailto:wjarosz@ucsd.edu [X-HTMLGallery Parameter author] Name=Author Name[ca]=Autor Name[ca@valencia]=Autor Name[cs]=Autor Name[de]=Autor Name[el]=Συγγραφέας Name[en_GB]=Author Name[es]=Autor Name[fr]=Auteur Name[gl]=Autor Name[is]=Höfundur Name[it]=Autore Name[nl]=Auteur Name[nn]=Opphavsperson Name[pl]=Autor Name[pt]=Autor Name[ru]=Автор Name[sv]=Upphovsman Name[tr]=Yazar Name[uk]=Автор Name[x-test]=xxAuthorxx Name[zh_CN]=作者 Type=string Default= [X-HTMLGallery Parameter authorEmail] Name=Author Email Name[ca]=Correu de l'autor Name[ca@valencia]=Correu de l'autor +Name[cs]=E-mail autora Name[en_GB]=Author Email Name[es]=Correo electrónico del autor Name[gl]=Correo electrónico do autor Name[nl]=E-mailadres van de auteur Name[nn]=E-postadresse til fotograf Name[pl]=Adres pocztowy autora Name[pt]=E-Mail do Autor Name[sv]=Upphovsmannens e-postadress Name[uk]=Адреса ел. пошти автора Name[x-test]=xxAuthor Emailxx Type=string Default= [X-HTMLGallery Parameter numRows] Name=Rows per page Name[ca]=Files per pàgina Name[ca@valencia]=Files per pàgina +Name[cs]=Řádků na stranu Name[en_GB]=Rows per page Name[es]=Filas por página Name[gl]=Filas por páxina Name[nl]=Rijen per pagina Name[nn]=Rader per side Name[pl]=Wierszy na stronę Name[pt]=Linhas por página Name[sv]=Rader per sida Name[uk]=Кількість рядків на сторінку Name[x-test]=xxRows per pagexx Type=int Default=3 [X-HTMLGallery Parameter numCols] Name=Columns per page Name[ca]=Columnes per pàgina Name[ca@valencia]=Columnes per pàgina +Name[cs]=Sloupců na stranu Name[en_GB]=Columns per page Name[es]=Columnas por página Name[gl]=Columnas por páxina Name[nl]=Kolommen per pagina Name[nn]=Kolonnar per side Name[pl]=Kolumn na stronę Name[pt]=Colunas por página Name[sv]=Kolumner per sida Name[uk]=Кількість стовпчиків на сторінку Name[x-test]=xxColumns per pagexx Type=int Default=3 [X-HTMLGallery Parameter style] Name=Style Name[ca]=Estil Name[ca@valencia]=Estil Name[cs]=Styl Name[de]=Stil Name[el]=Στιλ Name[en_GB]=Style Name[es]=Estilo Name[fr]=Style Name[gl]=Estilo Name[is]=Stíll Name[it]=Stile Name[nb]=Stil Name[nl]=Stijl Name[nn]=Stil Name[pl]=Wygląd Name[pt]=Estilo Name[ru]=Цветовая гамма Name[se]=Stiila Name[sv]=Stil Name[tr]=Stil Name[uk]=Стиль Name[x-test]=xxStylexx Name[zh_CN]=风格 Type=list Default=graphite.css Value-0=charcoal.css Caption-0=Charcoal Value-1=dusk.css Caption-1=Dusk Value-2=graphite.css Caption-2=Graphite Value-3=ice_blue.css Caption-3=Ice Blue Value-4=ivory.css Caption-4=Ivory Value-5=light_grey.css Caption-5=Light Grey Value-6=midnight.css Caption-6=Midnight Value-7=white_on_black.css Caption-7=White on Black [X-HTMLGallery Parameter displayNumbers] Name=Show image numbers Name[ca]=Mostra els números de les imatges Name[ca@valencia]=Mostra els números de les imatges +Name[cs]=Zobrazit čísla obrázků Name[en_GB]=Show image numbers Name[es]=Mostrar números de las imágenes Name[gl]=Mostrar os números das imaxes Name[nl]=Afbeeldingsnummers tonen Name[nn]=Vis bilettal Name[pl]=Pokaż numery obrazów Name[pt]=Mostrar os números das imagens Name[sv]=Visa bildnummer Name[uk]=Показувати номери зображень Name[x-test]=xxShow image numbersxx Type=list Default=inherit Value-0=inherit Caption-0=Yes Value-1=none Caption-1=No [X-HTMLGallery Parameter dropShadow] Name=Add drop shadow Name[ca]=Afegeix una ombra difusa Name[ca@valencia]=Afig una ombra difusa +Name[cs]=Přidat vrhání stínu Name[en_GB]=Add drop shadow Name[es]=Añadir sombra arrojada Name[gl]=Engadir sombreado de caída Name[nl]=Slagschaduw toevoegen Name[nn]=Legg til skuggeeffekt Name[pl]=Dodaj cień Name[pt]=Adicionar um sombreado Name[sv]=Lägg till fallskugga Name[uk]=Додати відкидання тіні Name[x-test]=xxAdd drop shadowxx Type=list Default=true Value-0=true Caption-0=Yes Value-1=false Caption-1=No [X-HTMLGallery Parameter thumbMargin] Name=Thumbnail margin Name[ca]=Marge de les miniatures Name[ca@valencia]=Marge de les miniatures +Name[cs]=Okraj náhledu Name[en_GB]=Thumbnail margin Name[es]=Margen de las miniaturas Name[gl]=Marxe das miniaturas Name[nl]=Marge van miniaturen Name[nn]=Marg rundt miniatyrar Name[pl]=Margines na miniatury Name[pt]=Margem das miniaturas Name[sv]=Miniatyrbildsmarginal Name[uk]=Поле мініатюр Name[x-test]=xxThumbnail marginxx Type=int Default=25 [X-HTMLGallery Parameter useLyteBox] Name=Use LyteBox Name[ca]=Usa el LyteBox Name[ca@valencia]=Usa el LyteBox Name[cs]=Použít LyteBox Name[el]=Χρήση LyteBox Name[en_GB]=Use LyteBox Name[es]=Usar LyteBox Name[fr]=Utiliser LyteBox Name[gl]=Usar LyteBox Name[is]=Nota LyteBox Name[it]=Usa LyteBox Name[nl]=LyteBox gebruiken Name[nn]=Bruk LyteBox Name[pl]=Użyj LyteBox Name[pt]=Usar o LyteBox Name[ru]=Использовать LyteBox Name[sv]=Använd LyteBox Name[tr]=LyteBox Kullan Name[uk]=Використання LyteBox Name[x-test]=xxUse LyteBoxxx Name[zh_CN]=使用 LyteBox Type=list Default=true Value-0=true Caption-0=Yes Value-1=false Caption-1=No [X-HTMLGallery Parameter slideInterval] Name=LyteBox Slideshow Interval Name[ca]=Interval entre diapositives del LyteBox Name[ca@valencia]=Interval entre diapositives del LyteBox Name[el]=Χρονικό διάστημα προβολής διαφανειών LyteBox Name[en_GB]=LyteBox Slideshow Interval Name[es]=Intervalo de presentación de LyteBox Name[fr]=Intervalle du diaporama LyteBox Name[gl]=Intervalo de presentación de LyteBox Name[is]=Bið milli mynda í LyteBox-skyggnusýningu Name[it]=Intervallo presentazione LyteBox Name[nl]=Interval LyteBox diavoorstelling Name[nn]=Oppdateringsintervall for LyteBox-framvising Name[pl]=Okres pokazu slajdów LyteBox Name[pt]=Intervalo da Apresentação do LyteBox Name[ru]=Интервал слайд-шоу (LyteBox): Name[sv]=Intervall för LyteBox-bildspel Name[tr]=LyteBox Slayt Gösterisi Aralığı Name[uk]=Інтервал показу слайдів LyteBox Name[x-test]=xxLyteBox Slideshow Intervalxx Name[zh_CN]=LyteBox 幻灯片间隔 Type=list Default=6000 Value-0=2000 Caption-0=2 seconds Value-1=4000 Caption-1=4 seconds Value-2=6000 Caption-2=6 seconds Value-3=8000 Caption-3=8 seconds Value-4=10000 Caption-4=10 seconds [X-HTMLGallery Preview] Name=Vanilla Name[ca]=Vainilla Name[ca@valencia]=Vainilla Name[en_GB]=Vanilla Name[es]=Vainilla Name[gl]=Estándar Name[nl]=Vanilla Name[nn]=Vanilje Name[pl]=Niezmieniony Name[pt]=Limpo Name[sv]=Ordinär Name[uk]=Ванілла Name[x-test]=xxVanillaxx Url=preview.png diff --git a/core/libs/album/albummanager.cpp b/core/libs/album/albummanager.cpp index 04025a3639..3b1cc32a5c 100644 --- a/core/libs/album/albummanager.cpp +++ b/core/libs/album/albummanager.cpp @@ -1,3759 +1,3760 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2004-06-15 * Description : Albums manager interface. * * Copyright (C) 2004 by Renchi Raju * Copyright (C) 2006-2018 by Gilles Caulier * Copyright (C) 2006-2011 by Marcel Wiesweg * Copyright (C) 2015 by Mohamed_Anwer * * 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, 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. * * ============================================================ */ #include "albummanager.h" // C ANSI includes extern "C" { #include #include #include } // C++ includes #include #include #include #include // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE includes #include // Local includes #include "digikam_debug.h" #include "coredb.h" #include "album.h" #include "applicationsettings.h" #include "metadatasettings.h" #include "metadatasynchronizer.h" #include "albumwatch.h" #include "imageattributeswatch.h" #include "collectionlocation.h" #include "collectionmanager.h" #include "digikam_config.h" #include "coredbaccess.h" #include "coredboperationgroup.h" #include "dbengineguierrorhandler.h" #include "dbengineparameters.h" #include "databaseserverstarter.h" #include "coredbthumbinfoprovider.h" #include "coredburl.h" #include "coredbsearchxml.h" #include "coredbwatch.h" #include "dio.h" #include "facetags.h" #include "facetagseditor.h" #include "imagelister.h" #include "scancontroller.h" #include "setupcollections.h" #include "setup.h" #include "tagscache.h" #include "thumbsdbaccess.h" #include "thumbnailloadthread.h" #include "dnotificationwrapper.h" #include "dbjobinfo.h" #include "dbjobsmanager.h" #include "dbjobsthread.h" #include "similaritydb.h" #include "similaritydbaccess.h" namespace Digikam { class PAlbumPath { public: PAlbumPath() : albumRootId(-1) { } PAlbumPath(int albumRootId, const QString& albumPath) : albumRootId(albumRootId), albumPath(albumPath) { } PAlbumPath(PAlbum* const album) { if (album->isRoot()) { albumRootId = -1; } else { albumRootId = album->albumRootId(); albumPath = album->albumPath(); } } bool operator==(const PAlbumPath& other) const { return (other.albumRootId == albumRootId && other.albumPath == albumPath); } public: int albumRootId; QString albumPath; }; // ----------------------------------------------------------------------------------- uint qHash(const PAlbumPath& id) { return ( ::qHash(id.albumRootId) ^ ::qHash(id.albumPath) ); } // ----------------------------------------------------------------------------------- class AlbumManager::Private { public: explicit Private() : changed(false), hasPriorizedDbPath(false), dbFakeConnection(false), showOnlyAvailableAlbums(false), albumListJob(0), dateListJob(0), tagListJob(0), personListJob(0), albumWatch(0), rootPAlbum(0), rootTAlbum(0), rootDAlbum(0), rootSAlbum(0), currentlyMovingAlbum(0), changingDB(false), scanPAlbumsTimer(0), scanTAlbumsTimer(0), scanSAlbumsTimer(0), scanDAlbumsTimer(0), updatePAlbumsTimer(0), albumItemCountTimer(0), tagItemCountTimer(0) { } bool changed; bool hasPriorizedDbPath; bool dbFakeConnection; bool showOnlyAvailableAlbums; AlbumsDBJobsThread* albumListJob; DatesDBJobsThread* dateListJob; TagsDBJobsThread* tagListJob; TagsDBJobsThread* personListJob; AlbumWatch* albumWatch; PAlbum* rootPAlbum; TAlbum* rootTAlbum; DAlbum* rootDAlbum; SAlbum* rootSAlbum; QHash allAlbumsIdHash; QHash albumPathHash; QHash albumRootAlbumHash; Album* currentlyMovingAlbum; QMultiHash guardedPointers; /** For multiple selection support **/ QList currentAlbums; bool changingDB; QTimer* scanPAlbumsTimer; QTimer* scanTAlbumsTimer; QTimer* scanSAlbumsTimer; QTimer* scanDAlbumsTimer; QTimer* updatePAlbumsTimer; QTimer* albumItemCountTimer; QTimer* tagItemCountTimer; QSet changedPAlbums; QMap pAlbumsCount; QMap tAlbumsCount; QMap dAlbumsCount; QMap fAlbumsCount; public: QString labelForAlbumRootAlbum(const CollectionLocation& location) { QString label = location.label(); if (label.isEmpty()) { label = location.albumRootPath(); } return label; } }; // ----------------------------------------------------------------------------------- class ChangingDB { public: explicit ChangingDB(AlbumManager::Private* const d) : d(d) { d->changingDB = true; } ~ChangingDB() { d->changingDB = false; } AlbumManager::Private* const d; }; // ----------------------------------------------------------------------------------- class AlbumManagerCreator { public: AlbumManager object; }; Q_GLOBAL_STATIC(AlbumManagerCreator, creator) // ----------------------------------------------------------------------------------- // A friend-class shortcut to circumvent accessing this from within the destructor AlbumManager* AlbumManager::internalInstance = 0; AlbumManager* AlbumManager::instance() { return &creator->object; } AlbumManager::AlbumManager() : d(new Private) { qRegisterMetaType>("QMap"); qRegisterMetaType>("QMap"); qRegisterMetaType >>("QMap >"); internalInstance = this; d->albumWatch = new AlbumWatch(this); // these operations are pretty fast, no need for long queuing d->scanPAlbumsTimer = new QTimer(this); d->scanPAlbumsTimer->setInterval(50); d->scanPAlbumsTimer->setSingleShot(true); connect(d->scanPAlbumsTimer, SIGNAL(timeout()), this, SLOT(scanPAlbums())); d->scanTAlbumsTimer = new QTimer(this); d->scanTAlbumsTimer->setInterval(50); d->scanTAlbumsTimer->setSingleShot(true); connect(d->scanTAlbumsTimer, SIGNAL(timeout()), this, SLOT(scanTAlbums())); d->scanSAlbumsTimer = new QTimer(this); d->scanSAlbumsTimer->setInterval(50); d->scanSAlbumsTimer->setSingleShot(true); connect(d->scanSAlbumsTimer, SIGNAL(timeout()), this, SLOT(scanSAlbums())); d->updatePAlbumsTimer = new QTimer(this); d->updatePAlbumsTimer->setInterval(50); d->updatePAlbumsTimer->setSingleShot(true); connect(d->updatePAlbumsTimer, SIGNAL(timeout()), this, SLOT(updateChangedPAlbums())); // this operation is much more expensive than the other scan methods d->scanDAlbumsTimer = new QTimer(this); d->scanDAlbumsTimer->setInterval(30 * 1000); d->scanDAlbumsTimer->setSingleShot(true); connect(d->scanDAlbumsTimer, SIGNAL(timeout()), this, SLOT(scanDAlbumsScheduled())); // moderately expensive d->albumItemCountTimer = new QTimer(this); d->albumItemCountTimer->setInterval(1000); d->albumItemCountTimer->setSingleShot(true); connect(d->albumItemCountTimer, SIGNAL(timeout()), this, SLOT(getAlbumItemsCount())); // more expensive d->tagItemCountTimer = new QTimer(this); d->tagItemCountTimer->setInterval(2500); d->tagItemCountTimer->setSingleShot(true); connect(d->tagItemCountTimer, SIGNAL(timeout()), this, SLOT(getTagItemsCount())); } AlbumManager::~AlbumManager() { delete d->rootPAlbum; delete d->rootTAlbum; delete d->rootDAlbum; delete d->rootSAlbum; internalInstance = 0; delete d; } void AlbumManager::cleanUp() { // This is what we prefer to do before Application destruction if (d->dateListJob) { d->dateListJob->cancel(); d->dateListJob = 0; } if (d->albumListJob) { d->albumListJob->cancel(); d->albumListJob = 0; } if (d->tagListJob) { d->tagListJob->cancel(); d->tagListJob = 0; } if (d->personListJob) { d->personListJob->cancel(); d->personListJob = 0; } } bool AlbumManager::databaseEqual(const DbEngineParameters& parameters) const { DbEngineParameters params = CoreDbAccess::parameters(); return (params == parameters); } static bool moveToBackup(const QFileInfo& info) { if (info.exists()) { QFileInfo backup(info.dir(), info.fileName() + QLatin1String("-backup-") + QDateTime::currentDateTime().toString(Qt::ISODate)); bool ret = QDir().rename(info.filePath(), backup.filePath()); if (!ret) { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), i18n("Failed to backup the existing database file (\"%1\"). " "Refusing to replace file without backup, using the existing file.", QDir::toNativeSeparators(info.filePath()))); return false; } } return true; } static bool copyToNewLocation(const QFileInfo& oldFile, const QFileInfo& newFile, const QString otherMessage = QString()) { QString message = otherMessage; if (message.isNull()) { message = i18n("Failed to copy the old database file (\"%1\") " "to its new location (\"%2\"). " "Starting with an empty database.", QDir::toNativeSeparators(oldFile.filePath()), QDir::toNativeSeparators(newFile.filePath())); } bool ret = QFile::copy(oldFile.filePath(), newFile.filePath()); if (!ret) { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), message); return false; } return true; } void AlbumManager::checkDatabaseDirsAfterFirstRun(const QString& dbPath, const QString& albumPath) { // for bug #193522 QDir newDir(dbPath); QDir albumDir(albumPath); DbEngineParameters newParams = DbEngineParameters::parametersForSQLiteDefaultFile(newDir.path()); QFileInfo digikam4DB(newParams.SQLiteDatabaseFile()); if (!digikam4DB.exists()) { QFileInfo digikam3DB(newDir, QLatin1String("digikam3.db")); QFileInfo digikamVeryOldDB(newDir, QLatin1String("digikam.db")); if (digikam3DB.exists() || digikamVeryOldDB.exists()) { QPointer msgBox = new QMessageBox(QMessageBox::Warning, i18n("Database Folder"), i18n("

You have chosen the folder \"%1\" as the place to store the database. " "A database file from an older version of digiKam is found in this folder.

" "

Would you like to upgrade the old database file - confirming " "that this database file was indeed created for the pictures located in the folder \"%2\" - " "or ignore the old file and start with a new database?

", QDir::toNativeSeparators(newDir.path()), QDir::toNativeSeparators(albumDir.path())), QMessageBox::Yes | QMessageBox::No, qApp->activeWindow()); msgBox->button(QMessageBox::Yes)->setText(i18n("Upgrade Database")); msgBox->button(QMessageBox::Yes)->setIcon(QIcon::fromTheme(QLatin1String("view-refresh"))); msgBox->button(QMessageBox::No)->setText(i18n("Create New Database")); msgBox->button(QMessageBox::No)->setIcon(QIcon::fromTheme(QLatin1String("document-new"))); msgBox->setDefaultButton(QMessageBox::Yes); int result = msgBox->exec(); if (result == QMessageBox::Yes) { // CoreDbSchemaUpdater expects Album Path to point to the album root of the 0.9 db file. // Restore this situation. KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup group = config->group("Album Settings"); group.writeEntry("Album Path", albumDir.path()); group.sync(); } else if (result == QMessageBox::No) { moveToBackup(digikam3DB); moveToBackup(digikamVeryOldDB); } delete msgBox; } } } void AlbumManager::changeDatabase(const DbEngineParameters& newParams) { // if there is no file at the new place, copy old one DbEngineParameters params = CoreDbAccess::parameters(); // New database type SQLITE if (newParams.isSQLite()) { DatabaseServerStarter::instance()->stopServerManagerProcess(); QDir newDir(newParams.getCoreDatabaseNameOrDir()); QFileInfo newFile(newDir, QLatin1String("digikam4.db")); if (!newFile.exists()) { QFileInfo digikam3DB(newDir, QLatin1String("digikam3.db")); QFileInfo digikamVeryOldDB(newDir, QLatin1String("digikam.db")); if (digikam3DB.exists() || digikamVeryOldDB.exists()) { int result = -1; if (params.isSQLite()) { QPointer msgBox = new QMessageBox(QMessageBox::Warning, i18n("New database folder"), i18n("

You have chosen the folder \"%1\" as the new place to store the database. " "A database file from an older version of digiKam is found in this folder.

" "

Would you like to upgrade the old database file, start with a new database, " "or copy the current database to this location and continue using it?

", QDir::toNativeSeparators(newDir.path())), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, qApp->activeWindow()); msgBox->button(QMessageBox::Yes)->setText(i18n("Upgrade Database")); msgBox->button(QMessageBox::Yes)->setIcon(QIcon::fromTheme(QLatin1String("view-refresh"))); msgBox->button(QMessageBox::No)->setText(i18n("Create New Database")); msgBox->button(QMessageBox::No)->setIcon(QIcon::fromTheme(QLatin1String("document-new"))); msgBox->button(QMessageBox::Cancel)->setText(i18n("Copy Current Database")); msgBox->button(QMessageBox::Cancel)->setIcon(QIcon::fromTheme(QLatin1String("edit-copy"))); msgBox->setDefaultButton(QMessageBox::Yes); result = msgBox->exec(); delete msgBox; } else { QPointer msgBox = new QMessageBox(QMessageBox::Warning, i18n("New database folder"), i18n("

You have chosen the folder \"%1\" as the new place to store the database. " "A database file from an older version of digiKam is found in this folder.

" "

Would you like to upgrade the old database file or start with a new database?

", QDir::toNativeSeparators(newDir.path())), QMessageBox::Yes | QMessageBox::No, qApp->activeWindow()); msgBox->button(QMessageBox::Yes)->setText(i18n("Upgrade Database")); msgBox->button(QMessageBox::Yes)->setIcon(QIcon::fromTheme(QLatin1String("view-refresh"))); msgBox->button(QMessageBox::No)->setText(i18n("Create New Database")); msgBox->button(QMessageBox::No)->setIcon(QIcon::fromTheme(QLatin1String("document-new"))); msgBox->setDefaultButton(QMessageBox::Yes); result = msgBox->exec(); delete msgBox; } if (result == QMessageBox::Yes) { // CoreDbSchemaUpdater expects Album Path to point to the album root of the 0.9 db file. // Restore this situation. KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(QLatin1String("Album Settings")); group.writeEntry(QLatin1String("Album Path"), newDir.path()); group.sync(); } else if (result == QMessageBox::No) { moveToBackup(digikam3DB); moveToBackup(digikamVeryOldDB); } else if (result == QMessageBox::Cancel) { QFileInfo oldFile(params.SQLiteDatabaseFile()); copyToNewLocation(oldFile, newFile, i18n("Failed to copy the old database file (\"%1\") " "to its new location (\"%2\"). " "Trying to upgrade old databases.", QDir::toNativeSeparators(oldFile.filePath()), QDir::toNativeSeparators(newFile.filePath()))); } } else { int result = QMessageBox::Yes; if (params.isSQLite()) { QPointer msgBox = new QMessageBox(QMessageBox::Warning, i18n("New database folder"), i18n("

You have chosen the folder \"%1\" as the new place to store the database.

" "

Would you like to copy the current database to this location " "and continue using it, or start with a new database?

", QDir::toNativeSeparators(newDir.path())), QMessageBox::Yes | QMessageBox::No, qApp->activeWindow()); msgBox->button(QMessageBox::Yes)->setText(i18n("Create New Database")); msgBox->button(QMessageBox::Yes)->setIcon(QIcon::fromTheme(QLatin1String("document-new"))); msgBox->button(QMessageBox::No)->setText(i18n("Copy Current Database")); msgBox->button(QMessageBox::No)->setIcon(QIcon::fromTheme(QLatin1String("edit-copy"))); msgBox->setDefaultButton(QMessageBox::Yes); result = msgBox->exec(); delete msgBox; } if (result == QMessageBox::No) { QFileInfo oldFile(params.SQLiteDatabaseFile()); copyToNewLocation(oldFile, newFile); } } } else { int result = QMessageBox::No; if (params.isSQLite()) { QPointer msgBox = new QMessageBox(QMessageBox::Warning, i18n("New database folder"), i18n("

You have chosen the folder \"%1\" as the new place to store the database. " "There is already a database file in this location.

" "

Would you like to use this existing file as the new database, or remove it " "and copy the current database to this place?

", QDir::toNativeSeparators(newDir.path())), QMessageBox::Yes | QMessageBox::No, qApp->activeWindow()); msgBox->button(QMessageBox::Yes)->setText(i18n("Copy Current Database")); msgBox->button(QMessageBox::Yes)->setIcon(QIcon::fromTheme(QLatin1String("edit-copy"))); msgBox->button(QMessageBox::No)->setText(i18n("Use Existing File")); msgBox->button(QMessageBox::No)->setIcon(QIcon::fromTheme(QLatin1String("document-open"))); msgBox->setDefaultButton(QMessageBox::Yes); result = msgBox->exec(); delete msgBox; } if (result == QMessageBox::Yes) { // first backup if (moveToBackup(newFile)) { QFileInfo oldFile(params.SQLiteDatabaseFile()); // then copy copyToNewLocation(oldFile, newFile); } } } } if (setDatabase(newParams, false)) { QApplication::setOverrideCursor(Qt::WaitCursor); startScan(); QApplication::restoreOverrideCursor(); ScanController::instance()->completeCollectionScan(); } } bool AlbumManager::setDatabase(const DbEngineParameters& params, bool priority, const QString& suggestedAlbumRoot) { // This is to ensure that the setup does not overrule the command line. // TODO: there is a bug that setup is showing something different here. if (priority) { d->hasPriorizedDbPath = true; } else if (d->hasPriorizedDbPath) { // ignore change without priority return true; } // shutdown possibly running collection scans. Must call resumeCollectionScan further down. ScanController::instance()->cancelAllAndSuspendCollectionScan(); QApplication::setOverrideCursor(Qt::WaitCursor); d->changed = true; disconnect(CollectionManager::instance(), 0, this, 0); CollectionManager::instance()->setWatchDisabled(); if (CoreDbAccess::databaseWatch()) { disconnect(CoreDbAccess::databaseWatch(), 0, this, 0); } DatabaseServerStarter::instance()->stopServerManagerProcess(); d->albumWatch->clear(); cleanUp(); d->currentAlbums.clear(); emit signalAlbumCurrentChanged(d->currentAlbums); emit signalAlbumsCleared(); d->albumPathHash.clear(); d->allAlbumsIdHash.clear(); d->albumRootAlbumHash.clear(); // deletes all child albums as well delete d->rootPAlbum; delete d->rootTAlbum; delete d->rootDAlbum; delete d->rootSAlbum; d->rootPAlbum = 0; d->rootTAlbum = 0; d->rootDAlbum = 0; d->rootSAlbum = 0; // -- Database initialization ------------------------------------------------- // ensure, embedded database is loaded qCDebug(DIGIKAM_GENERAL_LOG) << params; // workaround for the problem mariaDB >= 10.2 and QTBUG-63108 if (params.isMySQL()) { addFakeConnection(); } if (params.internalServer) { DatabaseServerError result = DatabaseServerStarter::instance()->startServerManagerProcess(params); if (result.getErrorType() != DatabaseServerError::NoErrors) { QWidget* const parent = QWidget::find(0); QString message = i18n("

An error occurred during the internal server start.

" "Details:\n %1", result.getErrorText()); QApplication::changeOverrideCursor(Qt::ArrowCursor); QMessageBox::critical(parent, qApp->applicationName(), message); QApplication::changeOverrideCursor(Qt::WaitCursor); } } CoreDbAccess::setParameters(params, CoreDbAccess::MainApplication); DbEngineGuiErrorHandler* const handler = new DbEngineGuiErrorHandler(CoreDbAccess::parameters()); CoreDbAccess::initDbEngineErrorHandler(handler); if (!handler->checkDatabaseConnection()) { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), i18n("

Failed to open the database. " "

You cannot use digiKam without a working database. " "digiKam will attempt to start now, but it will not be functional. " "Please check the database settings in the configuration menu.

" )); CoreDbAccess::setParameters(DbEngineParameters(), CoreDbAccess::DatabaseSlave); QApplication::restoreOverrideCursor(); return true; } d->albumWatch->setDbEngineParameters(params); // still suspended from above ScanController::instance()->resumeCollectionScan(); ScanController::Advice advice = ScanController::instance()->databaseInitialization(); QApplication::restoreOverrideCursor(); switch (advice) { case ScanController::Success: break; case ScanController::ContinueWithoutDatabase: { QString errorMsg = CoreDbAccess().lastError(); if (errorMsg.isEmpty()) { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), i18n("

Failed to open the database. " "

You cannot use digiKam without a working database. " "digiKam will attempt to start now, but it will not be functional. " "Please check the database settings in the configuration menu.

" )); } else { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), i18n("

Failed to open the database. Error message from database:

" "

%1

" "

You cannot use digiKam without a working database. " "digiKam will attempt to start now, but it will not be functional. " "Please check the database settings in the configuration menu.

", errorMsg)); } return true; } case ScanController::AbortImmediately: return false; } // -- Locale Checking --------------------------------------------------------- QString currLocale = QString::fromUtf8((QTextCodec::codecForLocale()->name())); QString dbLocale = CoreDbAccess().db()->getSetting(QLatin1String("Locale")); // guilty until proven innocent bool localeChanged = true; if (dbLocale.isNull()) { qCDebug(DIGIKAM_GENERAL_LOG) << "No locale found in database"; // Copy an existing locale from the settings file (used < 0.8) // to the database. KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(QLatin1String("General Settings")); if (group.hasKey(QLatin1String("Locale"))) { qCDebug(DIGIKAM_GENERAL_LOG) << "Locale found in configfile"; dbLocale = group.readEntry(QLatin1String("Locale"), QString()); // this hack is necessary, as we used to store the entire // locale info LC_ALL (for eg: en_US.UTF-8) earlier, // we now save only the encoding (UTF-8) QString oldConfigLocale = QString::fromUtf8(::setlocale(0, 0)); if (oldConfigLocale == dbLocale) { dbLocale = currLocale; localeChanged = false; CoreDbAccess().db()->setSetting(QLatin1String("Locale"), dbLocale); } } else { qCDebug(DIGIKAM_GENERAL_LOG) << "No locale found in config file"; dbLocale = currLocale; localeChanged = false; CoreDbAccess().db()->setSetting(QLatin1String("Locale"), dbLocale); } } else { if (dbLocale == currLocale) { localeChanged = false; } } if (localeChanged) { // TODO it would be better to replace all yes/no confirmation dialogs with ones that has custom // buttons that denote the actions directly, i.e.: ["Ignore and Continue"] ["Adjust locale"] int result = QMessageBox::warning(qApp->activeWindow(), qApp->applicationName(), i18n("Your locale has changed since this " "album was last opened.\n" "Old locale: %1, new locale: %2\n" "If you have recently changed your locale, you need not be concerned.\n" "Please note that if you switched to a locale " "that does not support some of the filenames in your collection, " "these files may no longer be found in the collection. " "If you are sure that you want to " "continue, click 'Yes'. " "Otherwise, click 'No' and correct your " "locale setting before restarting digiKam.", dbLocale, currLocale), QMessageBox::Yes | QMessageBox::No); if (result != QMessageBox::Yes) { return false; } CoreDbAccess().db()->setSetting(QLatin1String("Locale"), currLocale); } // -- UUID Checking --------------------------------------------------------- QList disappearedLocations = CollectionManager::instance()->checkHardWiredLocations(); foreach(const CollectionLocation& loc, disappearedLocations) { QString locDescription; QStringList candidateIds, candidateDescriptions; CollectionManager::instance()->migrationCandidates(loc, &locDescription, &candidateIds, &candidateDescriptions); qCDebug(DIGIKAM_GENERAL_LOG) << "Migration candidates for" << locDescription << ":" << candidateIds << candidateDescriptions; QDialog* const dialog = new QDialog; QWidget* const widget = new QWidget(dialog); QGridLayout* const mainLayout = new QGridLayout; mainLayout->setColumnStretch(1, 1); QLabel* const deviceIconLabel = new QLabel; deviceIconLabel->setPixmap(QIcon::fromTheme(QLatin1String("drive-harddisk")).pixmap(64)); mainLayout->addWidget(deviceIconLabel, 0, 0); QLabel* const mainLabel = new QLabel(i18n("

The collection

%1
(%2)

is currently not found on your system.
" "Please choose the most appropriate option to handle this situation:

", loc.label(), QDir::toNativeSeparators(locDescription))); mainLabel->setWordWrap(true); mainLayout->addWidget(mainLabel, 0, 1); QGroupBox* const groupBox = new QGroupBox; mainLayout->addWidget(groupBox, 1, 0, 1, 2); QGridLayout* const layout = new QGridLayout; layout->setColumnStretch(1, 1); QRadioButton* migrateButton = 0; QComboBox* migrateChoices = 0; if (!candidateIds.isEmpty()) { migrateButton = new QRadioButton; QLabel* const migrateLabel = new QLabel(i18n("

The collection is still available, but the identifier changed.
" "This can be caused by restoring a backup, changing the partition layout " "or the file system settings.
" "The collection is now located at this place:

")); migrateLabel->setWordWrap(true); migrateChoices = new QComboBox; for (int i = 0 ; i < candidateIds.size() ; ++i) { migrateChoices->addItem(QDir::toNativeSeparators(candidateDescriptions.at(i)), candidateIds.at(i)); } layout->addWidget(migrateButton, 0, 0, Qt::AlignTop); layout->addWidget(migrateLabel, 0, 1); layout->addWidget(migrateChoices, 1, 1); } QRadioButton* const isRemovableButton = new QRadioButton; QLabel* const isRemovableLabel = new QLabel(i18n("The collection is located on a storage device which is not always attached. " "Mark the collection as a removable collection.")); isRemovableLabel->setWordWrap(true); layout->addWidget(isRemovableButton, 2, 0, Qt::AlignTop); layout->addWidget(isRemovableLabel, 2, 1); QRadioButton* const solveManuallyButton = new QRadioButton; QLabel* const solveManuallyLabel = new QLabel(i18n("Take no action now. I would like to solve the problem " "later using the setup dialog")); solveManuallyLabel->setWordWrap(true); layout->addWidget(solveManuallyButton, 3, 0, Qt::AlignTop); layout->addWidget(solveManuallyLabel, 3, 1); groupBox->setLayout(layout); widget->setLayout(mainLayout); QVBoxLayout* const vbx = new QVBoxLayout(dialog); QDialogButtonBox* const buttons = new QDialogButtonBox(QDialogButtonBox::Ok, dialog); vbx->addWidget(widget); vbx->addWidget(buttons); dialog->setLayout(vbx); dialog->setWindowTitle(i18n("Collection not found")); connect(buttons->button(QDialogButtonBox::Ok), SIGNAL(clicked()), dialog, SLOT(accept())); // Default option: If there is only one candidate, default to migration. // Otherwise default to do nothing now. if (migrateButton && candidateIds.size() == 1) { migrateButton->setChecked(true); } else { solveManuallyButton->setChecked(true); } if (dialog->exec()) { if (migrateButton && migrateButton->isChecked()) { CollectionManager::instance()->migrateToVolume(loc, migrateChoices->itemData(migrateChoices->currentIndex()).toString()); } else if (isRemovableButton->isChecked()) { CollectionManager::instance()->changeType(loc, CollectionLocation::TypeVolumeRemovable); } } delete dialog; } // -- --------------------------------------------------------- // check that we have one album root if (CollectionManager::instance()->allLocations().isEmpty()) { if (suggestedAlbumRoot.isEmpty()) { Setup::execSinglePage(Setup::CollectionsPage); } else { QUrl albumRoot(QUrl::fromLocalFile(suggestedAlbumRoot)); CollectionManager::instance()->addLocation(albumRoot, albumRoot.fileName()); // Not needed? See bug #188959 //ScanController::instance()->completeCollectionScan(); } } // -- --------------------------------------------------------- QApplication::setOverrideCursor(Qt::WaitCursor); ThumbnailLoadThread::initializeThumbnailDatabase(CoreDbAccess::parameters().thumbnailParameters(), new ThumbsDbInfoProvider()); DbEngineGuiErrorHandler* const thumbnailsDBHandler = new DbEngineGuiErrorHandler(ThumbsDbAccess::parameters()); ThumbsDbAccess::initDbEngineErrorHandler(thumbnailsDBHandler); // Activate the similarity database. SimilarityDbAccess::setParameters(params.similarityParameters()); DbEngineGuiErrorHandler* const similarityHandler = new DbEngineGuiErrorHandler(SimilarityDbAccess::parameters()); SimilarityDbAccess::initDbEngineErrorHandler(similarityHandler); if (SimilarityDbAccess::checkReadyForUse(0)) { qCDebug(DIGIKAM_SIMILARITYDB_LOG) << "Similarity database ready for use"; } else { qCDebug(DIGIKAM_SIMILARITYDB_LOG) << "Failed to initialize similarity database"; } QApplication::restoreOverrideCursor(); return true; } void AlbumManager::startScan() { if (!d->changed) { return; } d->changed = false; // create root albums d->rootPAlbum = new PAlbum(i18n("Albums")); insertPAlbum(d->rootPAlbum, 0); d->rootTAlbum = new TAlbum(i18n("Tags"), 0, true); insertTAlbum(d->rootTAlbum, 0); d->rootSAlbum = new SAlbum(i18n("Searches"), 0, true); emit signalAlbumAboutToBeAdded(d->rootSAlbum, 0, 0); d->allAlbumsIdHash[d->rootSAlbum->globalID()] = d->rootSAlbum; emit signalAlbumAdded(d->rootSAlbum); d->rootDAlbum = new DAlbum(QDate(), true); emit signalAlbumAboutToBeAdded(d->rootDAlbum, 0, 0); d->allAlbumsIdHash[d->rootDAlbum->globalID()] = d->rootDAlbum; emit signalAlbumAdded(d->rootDAlbum); // Create albums for album roots. Reuse logic implemented in the method foreach(const CollectionLocation& location, CollectionManager::instance()->allLocations()) { handleCollectionStatusChange(location, CollectionLocation::LocationNull); } // listen to location status changes connect(CollectionManager::instance(), SIGNAL(locationStatusChanged(CollectionLocation,int)), this, SLOT(slotCollectionLocationStatusChanged(CollectionLocation,int))); connect(CollectionManager::instance(), SIGNAL(locationPropertiesChanged(CollectionLocation)), this, SLOT(slotCollectionLocationPropertiesChanged(CollectionLocation))); // reload albums refresh(); // listen to album database changes connect(CoreDbAccess::databaseWatch(), SIGNAL(albumChange(AlbumChangeset)), this, SLOT(slotAlbumChange(AlbumChangeset))); connect(CoreDbAccess::databaseWatch(), SIGNAL(tagChange(TagChangeset)), this, SLOT(slotTagChange(TagChangeset))); connect(CoreDbAccess::databaseWatch(), SIGNAL(searchChange(SearchChangeset)), this, SLOT(slotSearchChange(SearchChangeset))); // listen to collection image changes connect(CoreDbAccess::databaseWatch(), SIGNAL(collectionImageChange(CollectionImageChangeset)), this, SLOT(slotCollectionImageChange(CollectionImageChangeset))); connect(CoreDbAccess::databaseWatch(), SIGNAL(imageTagChange(ImageTagChangeset)), this, SLOT(slotImageTagChange(ImageTagChangeset))); // listen to image attribute changes connect(ImageAttributesWatch::instance(), SIGNAL(signalImageDateChanged(qlonglong)), d->scanDAlbumsTimer, SLOT(start())); emit signalAllAlbumsLoaded(); } void AlbumManager::slotCollectionLocationStatusChanged(const CollectionLocation& location, int oldStatus) { // not before initialization if (!d->rootPAlbum) { return; } if (handleCollectionStatusChange(location, oldStatus)) { // a change occurred. Possibly albums have appeared or disappeared scanPAlbums(); } } /// Returns true if it added or removed an album bool AlbumManager::handleCollectionStatusChange(const CollectionLocation& location, int oldStatus) { enum Action { Add, Remove, DoNothing }; Action action = DoNothing; switch (oldStatus) { case CollectionLocation::LocationNull: case CollectionLocation::LocationHidden: case CollectionLocation::LocationUnavailable: { switch (location.status()) { case CollectionLocation::LocationNull: // not possible break; case CollectionLocation::LocationHidden: action = Remove; break; case CollectionLocation::LocationAvailable: action = Add; break; case CollectionLocation::LocationUnavailable: if (d->showOnlyAvailableAlbums) { action = Remove; } else { action = Add; } break; case CollectionLocation::LocationDeleted: action = Remove; break; } break; } case CollectionLocation::LocationAvailable: { switch (location.status()) { case CollectionLocation::LocationNull: case CollectionLocation::LocationHidden: case CollectionLocation::LocationDeleted: action = Remove; break; case CollectionLocation::LocationUnavailable: if (d->showOnlyAvailableAlbums) { action = Remove; } break; case CollectionLocation::LocationAvailable: // not possible break; } break; } case CollectionLocation::LocationDeleted: // not possible break; } if (action == Add && !d->albumRootAlbumHash.value(location.id())) { // This is the only place where album root albums are added addAlbumRoot(location); return true; } else if (action == Remove && d->albumRootAlbumHash.value(location.id())) { removeAlbumRoot(location); return true; } return false; } void AlbumManager::slotCollectionLocationPropertiesChanged(const CollectionLocation& location) { PAlbum* const album = d->albumRootAlbumHash.value(location.id()); if (album) { QString newLabel = d->labelForAlbumRootAlbum(location); if (album->title() != newLabel) { album->setTitle(newLabel); emit signalAlbumRenamed(album); } } } void AlbumManager::addAlbumRoot(const CollectionLocation& location) { PAlbum* album = d->albumRootAlbumHash.value(location.id()); if (!album) { // Create a PAlbum for the Album Root. QString label = d->labelForAlbumRootAlbum(location); album = new PAlbum(location.id(), label); qCDebug(DIGIKAM_GENERAL_LOG) << "Added root album called: " << album->title(); // insert album root created into hash d->albumRootAlbumHash.insert(location.id(), album); } } void AlbumManager::removeAlbumRoot(const CollectionLocation& location) { // retrieve and remove from hash PAlbum* const album = d->albumRootAlbumHash.take(location.id()); if (album) { // delete album and all its children removePAlbum(album); } } bool AlbumManager::isShowingOnlyAvailableAlbums() const { return d->showOnlyAvailableAlbums; } void AlbumManager::setShowOnlyAvailableAlbums(bool onlyAvailable) { if (d->showOnlyAvailableAlbums == onlyAvailable) { return; } d->showOnlyAvailableAlbums = onlyAvailable; emit signalShowOnlyAvailableAlbumsChanged(d->showOnlyAvailableAlbums); // We need to update the unavailable locations. // We assume the handleCollectionStatusChange does the right thing (even though old status == current status) foreach (const CollectionLocation& location, CollectionManager::instance()->allLocations()) { if (location.status() == CollectionLocation::LocationUnavailable) { handleCollectionStatusChange(location, CollectionLocation::LocationUnavailable); } } } void AlbumManager::refresh() { scanPAlbums(); scanTAlbums(); scanSAlbums(); scanDAlbums(); } void AlbumManager::prepareItemCounts() { // There is no way to find out if any data we had collected // previously is still valid - recompute scanDAlbums(); getAlbumItemsCount(); getTagItemsCount(); } void AlbumManager::scanPAlbums() { d->scanPAlbumsTimer->stop(); // first insert all the current normal PAlbums into a map for quick lookup QHash oldAlbums; AlbumIterator it(d->rootPAlbum); while (it.current()) { PAlbum* const a = (PAlbum*)(*it); oldAlbums[a->id()] = a; ++it; } // scan db and get a list of all albums QList currentAlbums = CoreDbAccess().db()->scanAlbums(); // sort by relative path so that parents are created before children std::sort(currentAlbums.begin(), currentAlbums.end()); QList newAlbums; // go through all the Albums and see which ones are already present foreach(const AlbumInfo& info, currentAlbums) { // check that location of album is available if (d->showOnlyAvailableAlbums && !CollectionManager::instance()->locationForAlbumRootId(info.albumRootId).isAvailable()) { continue; } if (oldAlbums.contains(info.id)) { oldAlbums.remove(info.id); } else { newAlbums << info; } } // now oldAlbums contains all the deleted albums and // newAlbums contains all the new albums // delete old albums, informing all frontends // The albums have to be removed with children being removed first, // removePAlbum takes care of that. // So we only feed it the albums from oldAlbums topmost in hierarchy. QSet topMostOldAlbums; foreach(PAlbum* const album, oldAlbums) { if (album->isTrashAlbum()) { continue; } if (!album->parent() || !oldAlbums.contains(album->parent()->id())) { topMostOldAlbums << album; } } foreach(PAlbum* const album, topMostOldAlbums) { // recursively removes all children and the album removePAlbum(album); } // sort by relative path so that parents are created before children std::sort(newAlbums.begin(), newAlbums.end()); // create all new albums foreach(const AlbumInfo& info, newAlbums) { if (info.relativePath.isEmpty()) { continue; } PAlbum* album = 0, *parent = 0; if (info.relativePath == QLatin1String("/")) { // Albums that represent the root directory of an album root // We have them as here new albums first time after their creation parent = d->rootPAlbum; album = d->albumRootAlbumHash.value(info.albumRootId); if (!album) { qCDebug(DIGIKAM_GENERAL_LOG) << "Did not find album root album in hash"; continue; } // it has been created from the collection location // with album root id, parentPath "/" and a name, but no album id yet. album->m_id = info.id; } else { // last section, no slash QString name = info.relativePath.section(QLatin1Char('/'), -1, -1); // all but last sections, leading slash, no trailing slash QString parentPath = info.relativePath.section(QLatin1Char('/'), 0, -2); if (parentPath.isEmpty()) { parent = d->albumRootAlbumHash.value(info.albumRootId); } else { parent = d->albumPathHash.value(PAlbumPath(info.albumRootId, parentPath)); } if (!parent) { qCDebug(DIGIKAM_GENERAL_LOG) << "Could not find parent with url: " << parentPath << " for: " << info.relativePath; continue; } // Create the new album album = new PAlbum(info.albumRootId, parentPath, name, info.id); } album->m_caption = info.caption; album->m_category = info.category; album->m_date = info.date; album->m_iconId = info.iconId; insertPAlbum(album, parent); if (album->isAlbumRoot()) { // Inserting virtual Trash PAlbum for AlbumsRootAlbum using special constructor PAlbum* trashAlbum = new PAlbum(album->title(), album->id()); insertPAlbum(trashAlbum, album); } } if (!topMostOldAlbums.isEmpty() || !newAlbums.isEmpty()) { emit signalAlbumsUpdated(Album::PHYSICAL); } getAlbumItemsCount(); } void AlbumManager::updateChangedPAlbums() { d->updatePAlbumsTimer->stop(); // scan db and get a list of all albums QList currentAlbums = CoreDbAccess().db()->scanAlbums(); bool needScanPAlbums = false; // Find the AlbumInfo for each id in changedPAlbums foreach(int id, d->changedPAlbums) { foreach(const AlbumInfo& info, currentAlbums) { if (info.id == id) { d->changedPAlbums.remove(info.id); PAlbum* album = findPAlbum(info.id); if (album) { // Renamed? if (info.relativePath != QLatin1String("/")) { // Handle rename of album name // last section, no slash QString name = info.relativePath.section(QLatin1Char('/'), -1, -1); QString parentPath = info.relativePath; parentPath.chop(name.length()); if (parentPath != album->m_parentPath || info.albumRootId != album->albumRootId()) { // Handle actual move operations: trigger ScanPAlbums needScanPAlbums = true; removePAlbum(album); break; } else if (name != album->title()) { album->setTitle(name); updateAlbumPathHash(); emit signalAlbumRenamed(album); } } // Update caption, collection, date album->m_caption = info.caption; album->m_category = info.category; album->m_date = info.date; // Icon changed? if (album->m_iconId != info.iconId) { album->m_iconId = info.iconId; emit signalAlbumIconChanged(album); } } } } } if (needScanPAlbums) { scanPAlbums(); } } void AlbumManager::getAlbumItemsCount() { d->albumItemCountTimer->stop(); if (!ApplicationSettings::instance()->getShowFolderTreeViewItemsCount()) { return; } if (d->albumListJob) { d->albumListJob->cancel(); d->albumListJob = 0; } AlbumsDBJobInfo jInfo; jInfo.setFoldersJob(); d->albumListJob = DBJobsManager::instance()->startAlbumsJobThread(jInfo); connect(d->albumListJob, SIGNAL(finished()), this, SLOT(slotAlbumsJobResult())); connect(d->albumListJob, SIGNAL(foldersData(QMap)), this, SLOT(slotAlbumsJobData(QMap))); } void AlbumManager::scanTAlbums() { d->scanTAlbumsTimer->stop(); // list TAlbums directly from the db // first insert all the current TAlbums into a map for quick lookup typedef QMap TagMap; TagMap tmap; tmap.insert(0, d->rootTAlbum); AlbumIterator it(d->rootTAlbum); while (it.current()) { TAlbum* t = (TAlbum*)(*it); tmap.insert(t->id(), t); ++it; } // Retrieve the list of tags from the database TagInfo::List tList = CoreDbAccess().db()->scanTags(); // sort the list. needed because we want the tags can be read in any order, // but we want to make sure that we are ensure to find the parent TAlbum // for a new TAlbum { QHash tagHash; // insert items into a dict for quick lookup for (TagInfo::List::const_iterator iter = tList.constBegin() ; iter != tList.constEnd() ; ++iter) { TagInfo info = *iter; TAlbum* const album = new TAlbum(info.name, info.id); album->m_icon = info.icon; album->m_iconId = info.iconId; album->m_pid = info.pid; tagHash.insert(info.id, album); } tList.clear(); // also add root tag TAlbum* const rootTag = new TAlbum(QLatin1String("root"), 0, true); tagHash.insert(0, rootTag); // build tree for (QHash::const_iterator iter = tagHash.constBegin() ; iter != tagHash.constEnd() ; ++iter) { TAlbum* album = *iter; if (album->m_id == 0) { continue; } TAlbum* const parent = tagHash.value(album->m_pid); if (parent) { album->setParent(parent); } else { qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to find parent tag for tag " << album->m_title << " with pid " << album->m_pid; } } tagHash.clear(); // now insert the items into the list. becomes sorted AlbumIterator it(rootTag); while (it.current()) { TagInfo info; TAlbum* const album = static_cast(it.current()); if (album) { info.id = album->m_id; info.pid = album->m_pid; info.name = album->m_title; info.icon = album->m_icon; info.iconId = album->m_iconId; } tList.append(info); ++it; } // this will also delete all child albums delete rootTag; } for (TagInfo::List::const_iterator it = tList.constBegin() ; it != tList.constEnd() ; ++it) { TagInfo info = *it; // check if we have already added this tag if (tmap.contains(info.id)) { continue; } // Its a new album. Find the parent of the album TagMap::const_iterator iter = tmap.constFind(info.pid); if (iter == tmap.constEnd()) { qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to find parent tag for tag " << info.name << " with pid " << info.pid; continue; } TAlbum* const parent = iter.value(); // Create the new TAlbum TAlbum* const album = new TAlbum(info.name, info.id, false); album->m_icon = info.icon; album->m_iconId = info.iconId; insertTAlbum(album, parent); // also insert it in the map we are doing lookup of parent tags tmap.insert(info.id, album); } if (!tList.isEmpty()) { emit signalAlbumsUpdated(Album::TAG); } getTagItemsCount(); } void AlbumManager::getTagItemsCount() { d->tagItemCountTimer->stop(); if (!ApplicationSettings::instance()->getShowFolderTreeViewItemsCount()) { return; } tagItemsCount(); personItemsCount(); } void AlbumManager::tagItemsCount() { if (d->tagListJob) { d->tagListJob->cancel(); d->tagListJob = 0; } TagsDBJobInfo jInfo; jInfo.setFoldersJob(); d->tagListJob = DBJobsManager::instance()->startTagsJobThread(jInfo); connect(d->tagListJob, SIGNAL(finished()), this, SLOT(slotTagsJobResult())); connect(d->tagListJob, SIGNAL(foldersData(QMap)), this, SLOT(slotTagsJobData(QMap))); } void AlbumManager::personItemsCount() { if (d->personListJob) { d->personListJob->cancel(); d->personListJob = 0; } TagsDBJobInfo jInfo; jInfo.setFaceFoldersJob(); d->personListJob = DBJobsManager::instance()->startTagsJobThread(jInfo); connect(d->personListJob, SIGNAL(finished()), this, SLOT(slotPeopleJobResult())); connect(d->personListJob, SIGNAL(faceFoldersData(QMap >)), // krazy:exclude=normalize this, SLOT(slotPeopleJobData(QMap >))); // krazy:exclude=normalize } void AlbumManager::scanSAlbums() { d->scanSAlbumsTimer->stop(); // list SAlbums directly from the db // first insert all the current SAlbums into a map for quick lookup QMap oldSearches; AlbumIterator it(d->rootSAlbum); while (it.current()) { SAlbum* const search = (SAlbum*)(*it); oldSearches[search->id()] = search; ++it; } // scan db and get a list of all albums QList currentSearches = CoreDbAccess().db()->scanSearches(); QList newSearches; // go through all the Albums and see which ones are already present foreach(const SearchInfo& info, currentSearches) { if (oldSearches.contains(info.id)) { SAlbum* const album = oldSearches[info.id]; if (info.name != album->title() || info.type != album->searchType() || info.query != album->query()) { QString oldName = album->title(); album->setSearch(info.type, info.query); album->setTitle(info.name); if (oldName != album->title()) { emit signalAlbumRenamed(album); } emit signalSearchUpdated(album); } oldSearches.remove(info.id); } else { newSearches << info; } } // remove old albums that have been deleted foreach(SAlbum* const album, oldSearches) { emit signalAlbumAboutToBeDeleted(album); d->allAlbumsIdHash.remove(album->globalID()); emit signalAlbumDeleted(album); quintptr deletedAlbum = reinterpret_cast(album); delete album; emit signalAlbumHasBeenDeleted(deletedAlbum); } // add new albums foreach(const SearchInfo& info, newSearches) { SAlbum* const album = new SAlbum(info.name, info.id); album->setSearch(info.type, info.query); emit signalAlbumAboutToBeAdded(album, d->rootSAlbum, d->rootSAlbum->lastChild()); album->setParent(d->rootSAlbum); d->allAlbumsIdHash[album->globalID()] = album; emit signalAlbumAdded(album); } } void AlbumManager::scanDAlbumsScheduled() { // Avoid a cycle of killing a job which takes longer than the timer interval if (d->dateListJob) { d->scanDAlbumsTimer->start(); return; } scanDAlbums(); } void AlbumManager::scanDAlbums() { d->scanDAlbumsTimer->stop(); if (d->dateListJob) { d->dateListJob->cancel(); d->dateListJob = 0; } DatesDBJobInfo jInfo; jInfo.setFoldersJob(); d->dateListJob = DBJobsManager::instance()->startDatesJobThread(jInfo); connect(d->dateListJob, SIGNAL(finished()), this, SLOT(slotDatesJobResult())); connect(d->dateListJob, SIGNAL(foldersData(QMap)), this, SLOT(slotDatesJobData(QMap))); } AlbumList AlbumManager::allPAlbums() const { AlbumList list; if (d->rootPAlbum) { list.append(d->rootPAlbum); } AlbumIterator it(d->rootPAlbum); while (it.current()) { list.append(*it); ++it; } return list; } AlbumList AlbumManager::allTAlbums() const { AlbumList list; if (d->rootTAlbum) { list.append(d->rootTAlbum); } AlbumIterator it(d->rootTAlbum); while (it.current()) { list.append(*it); ++it; } return list; } AlbumList AlbumManager::allSAlbums() const { AlbumList list; if (d->rootSAlbum) { list.append(d->rootSAlbum); } AlbumIterator it(d->rootSAlbum); while (it.current()) { list.append(*it); ++it; } return list; } AlbumList AlbumManager::allDAlbums() const { AlbumList list; if (d->rootDAlbum) { list.append(d->rootDAlbum); } AlbumIterator it(d->rootDAlbum); while (it.current()) { list.append(*it); ++it; } return list; } void AlbumManager::setCurrentAlbums(QList albums) { if (albums.isEmpty()) return; QList filtered; /** * Filter out the null pointers */ foreach(Album* const album, albums) { if (album != 0) { filtered.append(album); } } albums = filtered; /** * Sort is needed to identify selection correctly, ex AlbumHistory */ std::sort(albums.begin(), albums.end()); d->currentAlbums.clear(); d->currentAlbums+=albums; emit signalAlbumCurrentChanged(d->currentAlbums); } AlbumList AlbumManager::currentAlbums() const { return d->currentAlbums; } PAlbum* AlbumManager::currentPAlbum() const { /** * Temporary fix, to return multiple items, * iterate and cast each element */ if (!d->currentAlbums.isEmpty()) return dynamic_cast(d->currentAlbums.first()); else return 0; } QList AlbumManager::currentTAlbums() const { /** * This method is not yet used */ QList talbums; QList::iterator it; for (it = d->currentAlbums.begin() ; it != d->currentAlbums.end() ; ++it) { TAlbum* const temp = dynamic_cast(*it); if (temp) talbums.append(temp); } return talbums; } PAlbum* AlbumManager::findPAlbum(const QUrl& url) const { CollectionLocation location = CollectionManager::instance()->locationForUrl(url); if (location.isNull()) { return 0; } return d->albumPathHash.value(PAlbumPath(location.id(), CollectionManager::instance()->album(location, url))); } PAlbum* AlbumManager::findPAlbum(int id) const { if (!d->rootPAlbum) { return 0; } int gid = d->rootPAlbum->globalID() + id; return static_cast((d->allAlbumsIdHash.value(gid))); } TAlbum* AlbumManager::findTAlbum(int id) const { if (!d->rootTAlbum) { return 0; } int gid = d->rootTAlbum->globalID() + id; return static_cast((d->allAlbumsIdHash.value(gid))); } SAlbum* AlbumManager::findSAlbum(int id) const { if (!d->rootSAlbum) { return 0; } int gid = d->rootSAlbum->globalID() + id; return static_cast((d->allAlbumsIdHash.value(gid))); } DAlbum* AlbumManager::findDAlbum(int id) const { if (!d->rootDAlbum) { return 0; } int gid = d->rootDAlbum->globalID() + id; return static_cast((d->allAlbumsIdHash.value(gid))); } Album* AlbumManager::findAlbum(int gid) const { return d->allAlbumsIdHash.value(gid); } Album* AlbumManager::findAlbum(Album::Type type, int id) const { return findAlbum(Album::globalID(type, id)); } TAlbum* AlbumManager::findTAlbum(const QString& tagPath) const { // handle gracefully with or without leading slash bool withLeadingSlash = tagPath.startsWith(QLatin1Char('/')); AlbumIterator it(d->rootTAlbum); while (it.current()) { TAlbum* const talbum = static_cast(*it); if (talbum->tagPath(withLeadingSlash) == tagPath) { return talbum; } ++it; } return 0; } SAlbum* AlbumManager::findSAlbum(const QString& name) const { for (Album* album = d->rootSAlbum->firstChild() ; album ; album = album->next()) { if (album->title() == name) { return dynamic_cast(album); } } return 0; } QList AlbumManager::findSAlbumsBySearchType(int searchType) const { QList albums; for (Album* album = d->rootSAlbum->firstChild() ; album ; album = album->next()) { if (album != 0) { SAlbum* sAlbum = dynamic_cast(album); if ((sAlbum != 0) && (sAlbum->searchType() == searchType)) { albums.append(sAlbum); } } } return albums; } void AlbumManager::addGuardedPointer(Album* album, Album** pointer) { if (album) { d->guardedPointers.insert(album, pointer); } } void AlbumManager::removeGuardedPointer(Album* album, Album** pointer) { if (album) { d->guardedPointers.remove(album, pointer); } } void AlbumManager::changeGuardedPointer(Album* oldAlbum, Album* album, Album** pointer) { if (oldAlbum) { d->guardedPointers.remove(oldAlbum, pointer); } if (album) { d->guardedPointers.insert(album, pointer); } } void AlbumManager::invalidateGuardedPointers(Album* album) { if (!album) { return; } QMultiHash::iterator it = d->guardedPointers.find(album); for (; it != d->guardedPointers.end() && it.key() == album; ++it) { if (it.value()) { *(it.value()) = 0; } } } PAlbum* AlbumManager::createPAlbum(const QString& albumRootPath, const QString& name, const QString& caption, const QDate& date, const QString& category, QString& errMsg) { CollectionLocation location = CollectionManager::instance()->locationForAlbumRootPath(albumRootPath); return createPAlbum(location, name, caption, date, category, errMsg); } PAlbum* AlbumManager::createPAlbum(const CollectionLocation& location, const QString& name, const QString& caption, const QDate& date, const QString& category, QString& errMsg) { if (location.isNull() || !location.isAvailable()) { errMsg = i18n("The collection location supplied is invalid or currently not available."); return 0; } PAlbum* album = d->albumRootAlbumHash.value(location.id()); if (!album) { errMsg = i18n("No album for collection location: Internal error"); return 0; } return createPAlbum(album, name, caption, date, category, errMsg); } PAlbum* AlbumManager::createPAlbum(PAlbum* parent, const QString& name, const QString& caption, const QDate& date, const QString& category, QString& errMsg) { if (!parent) { errMsg = i18n("No parent found for album."); return 0; } // sanity checks if (name.isEmpty()) { errMsg = i18n("Album name cannot be empty."); return 0; } if (name.contains(QLatin1String("/"))) { errMsg = i18n("Album name cannot contain '/'."); return 0; } if (parent->isRoot()) { errMsg = i18n("createPAlbum does not accept the root album as parent."); return 0; } QString albumPath = parent->isAlbumRoot() ? QString(QLatin1Char('/') + name) : QString(parent->albumPath() + QLatin1Char('/') + name); int albumRootId = parent->albumRootId(); // first check if we have a sibling album with the same name PAlbum* child = static_cast(parent->m_firstChild); while (child) { if (child->albumRootId() == albumRootId && child->albumPath() == albumPath) { errMsg = i18n("An existing album has the same name."); return 0; } child = static_cast(child->m_next); } CoreDbUrl url = parent->databaseUrl(); url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + QLatin1Char('/') + name); QUrl fileUrl = url.fileUrl(); bool ret = QDir().mkdir(fileUrl.toLocalFile()); if (!ret) { errMsg = i18n("Failed to create directory '%1'", fileUrl.toString()); // TODO add tags? return 0; } ChangingDB changing(d); int id = CoreDbAccess().db()->addAlbum(albumRootId, albumPath, caption, date, category); if (id == -1) { errMsg = i18n("Failed to add album to database"); return 0; } QString parentPath; if (!parent->isAlbumRoot()) { parentPath = parent->albumPath(); } PAlbum* const album = new PAlbum(albumRootId, parentPath, name, id); album->m_caption = caption; album->m_category = category; album->m_date = date; insertPAlbum(album, parent); emit signalAlbumsUpdated(Album::PHYSICAL); return album; } bool AlbumManager::renamePAlbum(PAlbum* album, const QString& newName, QString& errMsg) { if (!album) { errMsg = i18n("No such album"); return false; } if (album == d->rootPAlbum) { errMsg = i18n("Cannot rename root album"); return false; } if (album->isAlbumRoot()) { errMsg = i18n("Cannot rename album root album"); return false; } if (newName.contains(QLatin1String("/"))) { errMsg = i18n("Album name cannot contain '/'"); return false; } // first check if we have another sibling with the same name if (hasDirectChildAlbumWithTitle(album->m_parent, newName)) { errMsg = i18n("Another album with the same name already exists.\n" "Please choose another name."); return false; } d->albumWatch->removeWatchedPAlbums(album); QString oldAlbumPath = album->albumPath(); QUrl oldUrl = album->fileUrl(); album->setTitle(newName); album->m_path = newName; QUrl newUrl = album->fileUrl(); QString newAlbumPath = album->albumPath(); // We use a private shortcut around collection scanner noticing our changes, // we rename them directly. Faster. ScanController::instance()->suspendCollectionScan(); bool ret = QDir().rename(oldUrl.toLocalFile(), newUrl.toLocalFile()); if (!ret) { ScanController::instance()->resumeCollectionScan(); errMsg = i18n("Failed to rename Album"); return false; } // now rename the album and subalbums in the database { CoreDbAccess access; ChangingDB changing(d); access.db()->renameAlbum(album->id(), album->albumRootId(), album->albumPath()); PAlbum* subAlbum = 0; AlbumIterator it(album); while ((subAlbum = static_cast(it.current())) != 0) { subAlbum->m_parentPath = newAlbumPath + subAlbum->m_parentPath.mid(oldAlbumPath.length()); access.db()->renameAlbum(subAlbum->id(), album->albumRootId(), subAlbum->albumPath()); emit signalAlbumNewPath(subAlbum); ++it; } } updateAlbumPathHash(); emit signalAlbumRenamed(album); ScanController::instance()->resumeCollectionScan(); return true; } void AlbumManager::updateAlbumPathHash() { // Update AlbumDict. basically clear it and rebuild from scratch d->albumPathHash.clear(); AlbumIterator it(d->rootPAlbum); PAlbum* subAlbum = 0; while ((subAlbum = static_cast(it.current())) != 0) { d->albumPathHash[subAlbum] = subAlbum; ++it; } } bool AlbumManager::updatePAlbumIcon(PAlbum* album, qlonglong iconID, QString& errMsg) { if (!album) { errMsg = i18n("No such album"); return false; } if (album == d->rootPAlbum) { errMsg = i18n("Cannot edit root album"); return false; } { CoreDbAccess access; ChangingDB changing(d); access.db()->setAlbumIcon(album->id(), iconID); album->m_iconId = iconID; } emit signalAlbumIconChanged(album); return true; } qlonglong AlbumManager::getItemFromAlbum(PAlbum* album, const QString& fileName) { return CoreDbAccess().db()->getItemFromAlbum(album->id(), fileName); } TAlbum* AlbumManager::createTAlbum(TAlbum* parent, const QString& name, const QString& iconkde, QString& errMsg) { if (!parent) { errMsg = i18n("No parent found for tag"); return 0; } // sanity checks if (name.isEmpty()) { errMsg = i18n("Tag name cannot be empty"); return 0; } if (name.contains(QLatin1String("/"))) { errMsg = i18n("Tag name cannot contain '/'"); return 0; } // first check if we have another album with the same name if (hasDirectChildAlbumWithTitle(parent, name)) { errMsg = i18n("Tag name already exists"); return 0; } ChangingDB changing(d); int id = CoreDbAccess().db()->addTag(parent->id(), name, iconkde, 0); if (id == -1) { errMsg = i18n("Failed to add tag to database"); return 0; } TAlbum* const album = new TAlbum(name, id, false); album->m_icon = iconkde; insertTAlbum(album, parent); TAlbum* personParentTag = findTAlbum(FaceTags::personParentTag()); if (personParentTag && personParentTag->isAncestorOf(album)) { FaceTags::ensureIsPerson(album->id()); } emit signalAlbumsUpdated(Album::TAG); return album; } AlbumList AlbumManager::findOrCreateTAlbums(const QStringList& tagPaths) { // find tag ids for tag paths in list, create if they don't exist QList tagIDs = TagsCache::instance()->getOrCreateTags(tagPaths); // create TAlbum objects for the newly created tags scanTAlbums(); AlbumList resultList; for (QList::const_iterator it = tagIDs.constBegin() ; it != tagIDs.constEnd() ; ++it) { resultList.append(findTAlbum(*it)); } return resultList; } bool AlbumManager::deleteTAlbum(TAlbum* album, QString& errMsg, bool askUser) { if (!album) { errMsg = i18n("No such album"); return false; } if (album == d->rootTAlbum) { errMsg = i18n("Cannot delete Root Tag"); return false; } QList imageIds; if (askUser) { imageIds = CoreDbAccess().db()->getItemIDsInTag(album->id()); } { CoreDbAccess access; ChangingDB changing(d); access.db()->deleteTag(album->id()); Album* subAlbum = 0; AlbumIterator it(album); while ((subAlbum = it.current()) != 0) { access.db()->deleteTag(subAlbum->id()); ++it; } } removeTAlbum(album); emit signalAlbumsUpdated(Album::TAG); if (askUser) { askUserForWriteChangedTAlbumToFiles(imageIds); } return true; } bool AlbumManager::hasDirectChildAlbumWithTitle(Album* parent, const QString& title) { Album* sibling = parent->m_firstChild; while (sibling) { if (sibling->title() == title) { return true; } sibling = sibling->m_next; } return false; } bool AlbumManager::renameTAlbum(TAlbum* album, const QString& name, QString& errMsg) { if (!album) { errMsg = i18n("No such album"); return false; } if (album == d->rootTAlbum) { errMsg = i18n("Cannot edit root tag"); return false; } if (name.contains(QLatin1String("/"))) { errMsg = i18n("Tag name cannot contain '/'"); return false; } // first check if we have another sibling with the same name if (hasDirectChildAlbumWithTitle(album->m_parent, name)) { errMsg = i18n("Another tag with the same name already exists.\n" "Please choose another name."); return false; } ChangingDB changing(d); CoreDbAccess().db()->setTagName(album->id(), name); album->setTitle(name); emit signalAlbumRenamed(album); askUserForWriteChangedTAlbumToFiles(album); return true; } bool AlbumManager::moveTAlbum(TAlbum* album, TAlbum* newParent, QString& errMsg) { if (!album) { errMsg = i18n("No such album"); return false; } if (!newParent) { errMsg = i18n("Attempt to move TAlbum to nowhere"); return false; } if (album == d->rootTAlbum) { errMsg = i18n("Cannot move root tag"); return false; } if (hasDirectChildAlbumWithTitle(newParent, album->title())) { QPointer msgBox = new QMessageBox(QMessageBox::Warning, qApp->applicationName(), i18n("Another tag with the same name already exists.\n" "Do you want to merge the tags?"), QMessageBox::Yes | QMessageBox::No, qApp->activeWindow()); int result = msgBox->exec(); delete msgBox; if (result == QMessageBox::Yes) { TAlbum* const destAlbum = findTAlbum(newParent->tagPath() + QLatin1Char('/') + album->title()); return mergeTAlbum(album, destAlbum, false, errMsg); } else { return true; } } d->currentlyMovingAlbum = album; emit signalAlbumAboutToBeMoved(album); emit signalAlbumAboutToBeDeleted(album); if (album->parent()) { album->parent()->removeChild(album); } album->setParent(0); emit signalAlbumDeleted(album); emit signalAlbumHasBeenDeleted(reinterpret_cast(album)); emit signalAlbumAboutToBeAdded(album, newParent, newParent->lastChild()); ChangingDB changing(d); CoreDbAccess().db()->setTagParentID(album->id(), newParent->id()); album->setParent(newParent); emit signalAlbumAdded(album); emit signalAlbumMoved(album); emit signalAlbumsUpdated(Album::TAG); d->currentlyMovingAlbum = 0; TAlbum* personParentTag = findTAlbum(FaceTags::personParentTag()); if (personParentTag && personParentTag->isAncestorOf(album)) { FaceTags::ensureIsPerson(album->id()); } askUserForWriteChangedTAlbumToFiles(album); return true; } bool AlbumManager::mergeTAlbum(TAlbum* album, TAlbum* destAlbum, bool dialog, QString& errMsg) { if (!album || !destAlbum) { errMsg = i18n("No such album"); return false; } if (album == d->rootTAlbum || destAlbum == d->rootTAlbum) { errMsg = i18n("Cannot merge root tag"); return false; } if (album->m_firstChild) { errMsg = i18n("Only a tag without children can be merged!"); return false; } if (dialog) { QPointer msgBox = new QMessageBox(QMessageBox::Warning, qApp->applicationName(), i18n("Do you want to merge tag '%1' into tag '%2'?", album->title(), destAlbum->title()), QMessageBox::Yes | QMessageBox::No, qApp->activeWindow()); int result = msgBox->exec(); delete msgBox; if (result == QMessageBox::No) { return true; } } int oldId = album->id(); int mergeId = destAlbum->id(); if (oldId == mergeId) { return true; } QApplication::setOverrideCursor(Qt::WaitCursor); QList imageIds = CoreDbAccess().db()->getItemIDsInTag(oldId); CoreDbOperationGroup group; group.setMaximumTime(200); foreach(const qlonglong& imageId, imageIds) { QList facesList = FaceTagsEditor().databaseFaces(imageId); bool foundFace = false; foreach(const FaceTagsIface& face, facesList) { if (face.tagId() == oldId) { foundFace = true; FaceTagsEditor().removeFace(face); FaceTagsEditor().add(imageId, mergeId, face.region(), false); } } if (!foundFace) { ImageInfo info(imageId); info.removeTag(oldId); info.setTag(mergeId); group.allowLift(); } } QApplication::restoreOverrideCursor(); if (!deleteTAlbum(album, errMsg, false)) { return false; } askUserForWriteChangedTAlbumToFiles(imageIds); return true; } bool AlbumManager::updateTAlbumIcon(TAlbum* album, const QString& iconKDE, qlonglong iconID, QString& errMsg) { if (!album) { errMsg = i18n("No such tag"); return false; } if (album == d->rootTAlbum) { errMsg = i18n("Cannot edit root tag"); return false; } { CoreDbAccess access; ChangingDB changing(d); access.db()->setTagIcon(album->id(), iconKDE, iconID); album->m_icon = iconKDE; album->m_iconId = iconID; } emit signalAlbumIconChanged(album); return true; } AlbumList AlbumManager::getRecentlyAssignedTags(bool includeInternal) const { QList tagIDs = CoreDbAccess().db()->getRecentlyAssignedTags(); AlbumList resultList; for (QList::const_iterator it = tagIDs.constBegin() ; it != tagIDs.constEnd() ; ++it) { TAlbum* const album = findTAlbum(*it); if (album) { if (!includeInternal && album->isInternalTag()) { continue; } resultList.append(album); } } return resultList; } QStringList AlbumManager::tagPaths(const QList& tagIDs, bool leadingSlash, bool includeInternal) const { QStringList tagPaths; for (QList::const_iterator it = tagIDs.constBegin() ; it != tagIDs.constEnd() ; ++it) { TAlbum* album = findTAlbum(*it); if (album) { if (!includeInternal && album->isInternalTag()) { continue; } tagPaths.append(album->tagPath(leadingSlash)); } } return tagPaths; } QStringList AlbumManager::tagNames(const QList& tagIDs, bool includeInternal) const { QStringList tagNames; foreach(int id, tagIDs) { TAlbum* const album = findTAlbum(id); if (album) { if (!includeInternal && album->isInternalTag()) { continue; } tagNames << album->title(); } } return tagNames; } QHash AlbumManager::tagPaths(bool leadingSlash, bool includeInternal) const { QHash hash; AlbumIterator it(d->rootTAlbum); while (it.current()) { TAlbum* const t = (TAlbum*)(*it); if (includeInternal || !t->isInternalTag()) { hash.insert(t->id(), t->tagPath(leadingSlash)); } ++it; } return hash; } QHash AlbumManager::tagNames(bool includeInternal) const { QHash hash; AlbumIterator it(d->rootTAlbum); while (it.current()) { TAlbum* const t = (TAlbum*)(*it); if (includeInternal || !t->isInternalTag()) { hash.insert(t->id(), t->title()); } ++it; } return hash; } QList< int > AlbumManager::subTags(int tagId, bool recursive) { TAlbum* const album = this->findTAlbum(tagId); return album->childAlbumIds(recursive); } AlbumList AlbumManager::findTagsWithProperty(const QString& property) { AlbumList list; QList ids = TagsCache::instance()->tagsWithProperty(property); foreach(int id, ids) { TAlbum* const album = findTAlbum(id); if (album) { list << album; } } return list; } AlbumList AlbumManager::findTagsWithProperty(const QString& property, const QString& value) { AlbumList list; AlbumIterator it(d->rootTAlbum); while (it.current()) { if (static_cast(*it)->property(property) == value) { list << *it; } ++it; } return list; } QHash AlbumManager::albumTitles() const { QHash hash; AlbumIterator it(d->rootPAlbum); while (it.current()) { PAlbum* const a = (PAlbum*)(*it); hash.insert(a->id(), a->title()); ++it; } return hash; } SAlbum* AlbumManager::createSAlbum(const QString& name, DatabaseSearch::Type type, const QString& query) { // first iterate through all the search albums and see if there's an existing // SAlbum with same name. (Remember, SAlbums are arranged in a flat list) SAlbum* album = findSAlbum(name); ChangingDB changing(d); if (album) { updateSAlbum(album, query, name, type); return album; } int id = CoreDbAccess().db()->addSearch(type, name, query); if (id == -1) { return 0; } album = new SAlbum(name, id); emit signalAlbumAboutToBeAdded(album, d->rootSAlbum, d->rootSAlbum->lastChild()); album->setSearch(type, query); album->setParent(d->rootSAlbum); d->allAlbumsIdHash.insert(album->globalID(), album); emit signalAlbumAdded(album); return album; } bool AlbumManager::updateSAlbum(SAlbum* album, const QString& changedQuery, const QString& changedName, DatabaseSearch::Type type) { if (!album) { return false; } QString newName = changedName.isNull() ? album->title() : changedName; DatabaseSearch::Type newType = (type == DatabaseSearch::UndefinedType) ? album->searchType() : type; ChangingDB changing(d); CoreDbAccess().db()->updateSearch(album->id(), newType, newName, changedQuery); QString oldName = album->title(); album->setSearch(newType, changedQuery); album->setTitle(newName); if (oldName != album->title()) { emit signalAlbumRenamed(album); } if (!d->currentAlbums.isEmpty()) { if (d->currentAlbums.first() == album) { emit signalAlbumCurrentChanged(d->currentAlbums); } } return true; } bool AlbumManager::deleteSAlbum(SAlbum* album) { if (!album) { return false; } emit signalAlbumAboutToBeDeleted(album); ChangingDB changing(d); CoreDbAccess().db()->deleteSearch(album->id()); d->allAlbumsIdHash.remove(album->globalID()); emit signalAlbumDeleted(album); quintptr deletedAlbum = reinterpret_cast(album); delete album; emit signalAlbumHasBeenDeleted(deletedAlbum); return true; } QMap AlbumManager::getPAlbumsCount() const { return d->pAlbumsCount; } QMap AlbumManager::getTAlbumsCount() const { return d->tAlbumsCount; } QMap AlbumManager::getDAlbumsCount() const { return d->dAlbumsCount; } QMap AlbumManager::getFaceCount() const { return d->fAlbumsCount; } bool AlbumManager::isMovingAlbum(Album* album) const { return d->currentlyMovingAlbum == album; } void AlbumManager::insertPAlbum(PAlbum* album, PAlbum* parent) { if (!album) { return; } emit signalAlbumAboutToBeAdded(album, parent, parent ? parent->lastChild() : 0); if (parent) { album->setParent(parent); } d->albumPathHash[album] = album; d->allAlbumsIdHash[album->globalID()] = album; emit signalAlbumAdded(album); } void AlbumManager::removePAlbum(PAlbum* album) { if (!album) { return; } // remove all children of this album Album* child = album->m_firstChild; PAlbum* toBeRemoved = 0; while (child) { Album* next = child->m_next; toBeRemoved = static_cast(child); if (toBeRemoved) { removePAlbum(toBeRemoved); toBeRemoved = 0; } child = next; } emit signalAlbumAboutToBeDeleted(album); d->albumPathHash.remove(album); d->allAlbumsIdHash.remove(album->globalID()); CoreDbUrl url = album->databaseUrl(); if (!d->currentAlbums.isEmpty()) { if (album == d->currentAlbums.first()) { d->currentAlbums.clear(); emit signalAlbumCurrentChanged(d->currentAlbums); } } if (album->isAlbumRoot()) { d->albumRootAlbumHash.remove(album->albumRootId()); } emit signalAlbumDeleted(album); quintptr deletedAlbum = reinterpret_cast(album); delete album; emit signalAlbumHasBeenDeleted(deletedAlbum); } void AlbumManager::insertTAlbum(TAlbum* album, TAlbum* parent) { if (!album) { return; } emit signalAlbumAboutToBeAdded(album, parent, parent ? parent->lastChild() : 0); if (parent) { album->setParent(parent); } d->allAlbumsIdHash.insert(album->globalID(), album); emit signalAlbumAdded(album); } void AlbumManager::removeTAlbum(TAlbum* album) { if (!album) { return; } // remove all children of this album Album* child = album->m_firstChild; TAlbum* toBeRemoved = 0; while (child) { Album* next = child->m_next; toBeRemoved = static_cast(child); if (toBeRemoved) { removeTAlbum(toBeRemoved); toBeRemoved = 0; } child = next; } emit signalAlbumAboutToBeDeleted(album); d->allAlbumsIdHash.remove(album->globalID()); if (!d->currentAlbums.isEmpty()) { if (album == d->currentAlbums.first()) { d->currentAlbums.clear(); emit signalAlbumCurrentChanged(d->currentAlbums); } } emit signalAlbumDeleted(album); quintptr deletedAlbum = reinterpret_cast(album); delete album; emit signalAlbumHasBeenDeleted(deletedAlbum); } void AlbumManager::notifyAlbumDeletion(Album* album) { invalidateGuardedPointers(album); } void AlbumManager::slotAlbumsJobResult() { if (!d->albumListJob) { return; } if (d->albumListJob->hasErrors()) { qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to list albums"; // Pop-up a message about the error. DNotificationWrapper(QString(), d->albumListJob->errorsList().first(), 0, i18n("digiKam")); } d->albumListJob = 0; } void AlbumManager::slotAlbumsJobData(const QMap &albumsStatMap) { if (albumsStatMap.isEmpty()) { return; } d->pAlbumsCount = albumsStatMap; emit signalPAlbumsDirty(albumsStatMap); } void AlbumManager::slotPeopleJobResult() { if (!d->personListJob) { return; } if (d->personListJob->hasErrors()) { qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to list face tags"; // Pop-up a message about the error. DNotificationWrapper(QString(), d->personListJob->errorsList().first(), 0, i18n("digiKam")); } d->personListJob = 0; } void AlbumManager::slotPeopleJobData(const QMap >& facesStatMap) { if (facesStatMap.isEmpty()) { return; } // For now, we only use the sum of confirmed and unconfirmed faces d->fAlbumsCount.clear(); typedef QMap IntIntMap; foreach(const IntIntMap& counts, facesStatMap) { QMap::const_iterator it; for (it = counts.begin() ; it != counts.end() ; ++it) { d->fAlbumsCount[it.key()] += it.value(); } } emit signalFaceCountsDirty(d->fAlbumsCount); } void AlbumManager::slotTagsJobResult() { if (!d->tagListJob) { return; } if (d->tagListJob->hasErrors()) { qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to list face tags"; // Pop-up a message about the error. DNotificationWrapper(QString(), d->personListJob->errorsList().first(), 0, i18n("digiKam")); } d->tagListJob = 0; } void AlbumManager::slotTagsJobData(const QMap& tagsStatMap) { if (tagsStatMap.isEmpty()) { return; } d->tAlbumsCount = tagsStatMap; emit signalTAlbumsDirty(tagsStatMap); } void AlbumManager::slotDatesJobResult() { if (!d->dateListJob) { return; } if (d->dateListJob->hasErrors()) { qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to list dates"; // Pop-up a message about the error. DNotificationWrapper(QString(), d->dateListJob->errorsList().first(), 0, i18n("digiKam")); } d->dateListJob = 0; emit signalAllDAlbumsLoaded(); } void AlbumManager::slotDatesJobData(const QMap& datesStatMap) { if (datesStatMap.isEmpty() || !d->rootDAlbum) { return; } // insert all the DAlbums into a qmap for quick access QMap mAlbumMap; QMap yAlbumMap; AlbumIterator it(d->rootDAlbum); while (it.current()) { DAlbum* const a = (DAlbum*)(*it); if (a->range() == DAlbum::Month) { mAlbumMap.insert(a->date(), a); } else { yAlbumMap.insert(a->date().year(), a); } ++it; } QMap yearMonthMap; for (QMap::const_iterator it = datesStatMap.constBegin() ; it != datesStatMap.constEnd() ; ++it) { YearMonth yearMonth = YearMonth(it.key().date().year(), it.key().date().month()); QMap::iterator it2 = yearMonthMap.find(yearMonth); if (it2 == yearMonthMap.end()) { yearMonthMap.insert(yearMonth, *it); } else { *it2 += *it; } } int year, month; for (QMap::const_iterator iter = yearMonthMap.constBegin() ; iter != yearMonthMap.constEnd() ; ++iter) { year = iter.key().first; month = iter.key().second; QDate md(year, month, 1); // Do we already have this Month album if (mAlbumMap.contains(md)) { // already there. remove Month album from map mAlbumMap.remove(md); if (yAlbumMap.contains(year)) { // already there. remove from map yAlbumMap.remove(year); } continue; } // Check if Year Album already exist. DAlbum* yAlbum = 0; AlbumIterator it(d->rootDAlbum); while (it.current()) { DAlbum* const a = (DAlbum*)(*it); if (a->date() == QDate(year, 1, 1) && a->range() == DAlbum::Year) { yAlbum = a; break; } ++it; } // If no, create Year album. if (!yAlbum) { yAlbum = new DAlbum(QDate(year, 1, 1), false, DAlbum::Year); emit signalAlbumAboutToBeAdded(yAlbum, d->rootDAlbum, d->rootDAlbum->lastChild()); yAlbum->setParent(d->rootDAlbum); d->allAlbumsIdHash.insert(yAlbum->globalID(), yAlbum); emit signalAlbumAdded(yAlbum); } // Create Month album DAlbum* mAlbum = new DAlbum(md); emit signalAlbumAboutToBeAdded(mAlbum, yAlbum, yAlbum->lastChild()); mAlbum->setParent(yAlbum); d->allAlbumsIdHash.insert(mAlbum->globalID(), mAlbum); emit signalAlbumAdded(mAlbum); } // Now the items contained in the maps are the ones which // have been deleted. for (QMap::const_iterator it = mAlbumMap.constBegin() ; it != mAlbumMap.constEnd() ; ++it) { DAlbum* const album = it.value(); emit signalAlbumAboutToBeDeleted(album); d->allAlbumsIdHash.remove(album->globalID()); emit signalAlbumDeleted(album); quintptr deletedAlbum = reinterpret_cast(album); delete album; emit signalAlbumHasBeenDeleted(deletedAlbum); } for (QMap::const_iterator it = yAlbumMap.constBegin() ; it != yAlbumMap.constEnd() ; ++it) { DAlbum* const album = it.value(); emit signalAlbumAboutToBeDeleted(album); d->allAlbumsIdHash.remove(album->globalID()); emit signalAlbumDeleted(album); quintptr deletedAlbum = reinterpret_cast(album); delete album; emit signalAlbumHasBeenDeleted(deletedAlbum); } d->dAlbumsCount = yearMonthMap; emit signalDAlbumsDirty(yearMonthMap); emit signalDatesMapDirty(datesStatMap); } void AlbumManager::slotAlbumChange(const AlbumChangeset& changeset) { if (d->changingDB || !d->rootPAlbum) { return; } switch (changeset.operation()) { case AlbumChangeset::Added: case AlbumChangeset::Deleted: if (!d->scanPAlbumsTimer->isActive()) { d->scanPAlbumsTimer->start(); } break; case AlbumChangeset::Renamed: case AlbumChangeset::PropertiesChanged: // mark for rescan d->changedPAlbums << changeset.albumId(); if (!d->updatePAlbumsTimer->isActive()) { d->updatePAlbumsTimer->start(); } break; case AlbumChangeset::Unknown: break; } } void AlbumManager::slotTagChange(const TagChangeset& changeset) { if (d->changingDB || !d->rootTAlbum) { return; } switch (changeset.operation()) { case TagChangeset::Added: case TagChangeset::Moved: case TagChangeset::Deleted: case TagChangeset::Reparented: if (!d->scanTAlbumsTimer->isActive()) { d->scanTAlbumsTimer->start(); } break; case TagChangeset::Renamed: case TagChangeset::IconChanged: /** * @todo what happens here? */ break; case TagChangeset::PropertiesChanged: { TAlbum* tag = findTAlbum(changeset.tagId()); if (tag) { emit signalTagPropertiesChanged(tag); } break; } case TagChangeset::Unknown: break; } } void AlbumManager::slotSearchChange(const SearchChangeset& changeset) { if (d->changingDB || !d->rootSAlbum) { return; } switch (changeset.operation()) { case SearchChangeset::Added: case SearchChangeset::Deleted: if (!d->scanSAlbumsTimer->isActive()) { d->scanSAlbumsTimer->start(); } break; case SearchChangeset::Changed: if (!d->currentAlbums.isEmpty()) { Album* currentAlbum = d->currentAlbums.first(); if (currentAlbum && currentAlbum->type() == Album::SEARCH && currentAlbum->id() == changeset.searchId()) { // the pointer is the same, but the contents changed emit signalAlbumCurrentChanged(d->currentAlbums); } } break; case SearchChangeset::Unknown: break; } } void AlbumManager::slotCollectionImageChange(const CollectionImageChangeset& changeset) { if (!d->rootDAlbum) { return; } switch (changeset.operation()) { case CollectionImageChangeset::Added: + case CollectionImageChangeset::Deleted: case CollectionImageChangeset::Removed: case CollectionImageChangeset::RemovedAll: if (!d->scanDAlbumsTimer->isActive()) { d->scanDAlbumsTimer->start(); } if (!d->albumItemCountTimer->isActive()) { d->albumItemCountTimer->start(); } break; default: break; } } void AlbumManager::slotImageTagChange(const ImageTagChangeset& changeset) { if (!d->rootTAlbum) { return; } switch (changeset.operation()) { case ImageTagChangeset::Added: case ImageTagChangeset::Removed: case ImageTagChangeset::RemovedAll: // Add properties changed. // Reason: in people sidebar, the images are not // connected with the ImageTag table but by // ImageTagProperties entries. // Thus, the count of entries in face tags are not // updated. This adoption should fix the problem. case ImageTagChangeset::PropertiesChanged: if (!d->tagItemCountTimer->isActive()) { d->tagItemCountTimer->start(); } break; default: break; } } void AlbumManager::slotImagesDeleted(const QList& imageIds) { qCDebug(DIGIKAM_GENERAL_LOG) << "Got image deletion notification from ImageViewUtilities for " << imageIds.size() << " images."; QSet sAlbumsToUpdate; QSet deletedImages = imageIds.toSet(); QList sAlbums = findSAlbumsBySearchType(DatabaseSearch::DuplicatesSearch); foreach(SAlbum* const sAlbum, sAlbums) { // Read the search query XML and save the image ids SearchXmlReader reader(sAlbum->query()); SearchXml::Element element; QSet images; while ((element = reader.readNext()) != SearchXml::End) { if ((element == SearchXml::Field) && (reader.fieldName().compare(QLatin1String("imageid")) == 0)) { images = reader.valueToLongLongList().toSet(); } } // If the deleted images are part of the SAlbum, // mark the album as ready for deletion and the images as ready for rescan. #if QT_VERSION >= 0x050600 if (images.intersects(deletedImages)) #else if (images.intersect(deletedImages).isEmpty()) #endif { sAlbumsToUpdate.insert(sAlbum); } } if (!sAlbumsToUpdate.isEmpty()) { emit signalUpdateDuplicatesAlbums(sAlbumsToUpdate.toList(), deletedImages.toList()); } } void AlbumManager::askUserForWriteChangedTAlbumToFiles(TAlbum* const album) { QList imageIds = CoreDbAccess().db()->getItemIDsInTag(album->id()); askUserForWriteChangedTAlbumToFiles(imageIds); } void AlbumManager::askUserForWriteChangedTAlbumToFiles(const QList& imageIds) { MetadataSettings* const settings = MetadataSettings::instance(); if ((!settings->settings().saveTags && !settings->settings().saveFaceTags) || imageIds.isEmpty()) { return; } if (imageIds.count() > 100) { QPointer msgBox = new QMessageBox(QMessageBox::Warning, qApp->applicationName(), i18n("This operation can take a long time in the background.\n" "Do you want to write the metadata to %1 files now?", imageIds.count()), QMessageBox::Yes | QMessageBox::No, qApp->activeWindow()); int result = msgBox->exec(); delete msgBox; if (result != QMessageBox::Yes) { return; } } ImageInfoList infos(imageIds); MetadataSynchronizer* const tool = new MetadataSynchronizer(infos, MetadataSynchronizer::WriteFromDatabaseToFile); tool->start(); } void AlbumManager::removeWatchedPAlbums(const PAlbum* const album) { d->albumWatch->removeWatchedPAlbums(album); } void AlbumManager::addFakeConnection() { if (!d->dbFakeConnection) { // workaround for the problem mariaDB >= 10.2 and QTBUG-63108 QSqlDatabase::addDatabase(QLatin1String("QMYSQL"), QLatin1String("FakeConnection")); d->dbFakeConnection = true; } } void AlbumManager::removeFakeConnection() { if (d->dbFakeConnection) { QSqlDatabase::removeDatabase(QLatin1String("FakeConnection")); } } } // namespace Digikam diff --git a/core/libs/database/collection/collectionmanager.cpp b/core/libs/database/collection/collectionmanager.cpp index dcf34e8644..46bb787399 100644 --- a/core/libs/database/collection/collectionmanager.cpp +++ b/core/libs/database/collection/collectionmanager.cpp @@ -1,1796 +1,1803 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2007-04-09 * Description : Collection location management * * Copyright (C) 2007-2009 by Marcel Wiesweg * * 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, 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. * * ============================================================ */ #include "collectionmanager.h" // Qt includes #include #include #include #include #include // KDE includes #include #include #include #include #include #include #include #include #include // Local includes #include "digikam_debug.h" #include "coredbaccess.h" #include "coredbchangesets.h" #include "coredbtransaction.h" #include "coredb.h" #include "collectionscanner.h" #include "collectionlocation.h" namespace Digikam { class AlbumRootLocation : public CollectionLocation { public: AlbumRootLocation() : available(false), hidden(false) { } explicit AlbumRootLocation(const AlbumRootInfo& info) { qCDebug(DIGIKAM_DATABASE_LOG) << "Creating new Location " << info.specificPath << " uuid " << info.identifier; m_id = info.id; m_type = (Type)info.type; specificPath = info.specificPath; identifier = info.identifier; m_label = info.label; m_path.clear(); setStatus((CollectionLocation::Status)info.status); } void setStatusFromFlags() { if (hidden) { m_status = CollectionLocation::LocationHidden; } else { if (available) { m_status = CollectionLocation::LocationAvailable; } else { m_status = CollectionLocation::LocationUnavailable; } } } void setStatus(CollectionLocation::Status s) { m_status = s; // status is exclusive, and Hidden wins // but really both states are independent // - a hidden location might or might not be available if (m_status == CollectionLocation::LocationAvailable) { available = true; hidden = false; } else if (m_status == CollectionLocation::LocationHidden) { available = false; hidden = true; } else // Unavailable, Null, Deleted { available = false; hidden = false; } } void setId(int id) { m_id = id; } void setAbsolutePath(const QString& path) { m_path = path; } void setType(Type type) { m_type = type; } void setLabel(const QString& label) { m_label = label; } public: QString identifier; QString specificPath; bool available; bool hidden; }; // ------------------------------------------------- class SolidVolumeInfo { public: SolidVolumeInfo() : isRemovable(false), isOpticalDisc(false), isMounted(false) { } bool isNull() const { return path.isNull(); } public: QString udi; // Solid device UDI of the StorageAccess device QString path; // mount path of volume, with trailing slash QString uuid; // UUID as from Solid QString label; // volume label (think of CDs) bool isRemovable; // may be removed bool isOpticalDisc; // is an optical disk device as CD/DVD/BR bool isMounted; // is mounted on File System. }; // ------------------------------------------------- class CollectionManagerPrivate { public: explicit CollectionManagerPrivate(CollectionManager* s); // hack for Solid's threading problems QList actuallyListVolumes(); void slotTriggerUpdateVolumesList(); QList volumesListCache; /// Access Solid and return a list of storage volumes QList listVolumes(); /** * Find from a given list (usually the result of listVolumes) the volume * corresponding to the location */ SolidVolumeInfo findVolumeForLocation(const AlbumRootLocation* location, const QList volumes); /** * Find from a given list (usually the result of listVolumes) the volume * on which the file path specified by the url is located. */ SolidVolumeInfo findVolumeForUrl(const QUrl& fileUrl, const QList volumes); /// Create the volume identifier for the given volume info static QString volumeIdentifier(const SolidVolumeInfo& info); /// Create a volume identifier based on the path only QString volumeIdentifier(const QString& path); /// Create a network share identifier based on the mountpath QString networkShareIdentifier(const QString& path); /// Return the path, if location has a path-only identifier. Else returns a null string. QString pathFromIdentifier(const AlbumRootLocation* location); /// Return the path, if location has a path-only identifier. Else returns a null string. QStringList networkShareMountPathsFromIdentifier(const AlbumRootLocation* location); /// Create an MD5 hash of the top-level entries (file names, not file content) of the given path static QString directoryHash(const QString& path); /// Check if a location for specified path exists, assuming the given list of locations was deleted bool checkIfExists(const QString& path, QList assumeDeleted); /// Make a user presentable description, regardless of current location status QString technicalDescription(const AlbumRootLocation* location); public: QMap locations; bool changingDB; QStringList udisToWatch; bool watchEnabled; CollectionManager* s; }; // ------------------------------------------------- class ChangingDB { public: explicit ChangingDB(CollectionManagerPrivate* d) : d(d) { d->changingDB = true; } ~ChangingDB() { d->changingDB = false; } public: CollectionManagerPrivate* const d; }; } // namespace Digikam // This is because of the private slot; we'd want a collectionmanager_p.h #include "collectionmanager.h" // krazy:exclude=includes namespace Digikam { CollectionManagerPrivate::CollectionManagerPrivate(CollectionManager* s) : changingDB(false), watchEnabled(false), s(s) { QObject::connect(s, SIGNAL(triggerUpdateVolumesList()), s, SLOT(slotTriggerUpdateVolumesList()), Qt::BlockingQueuedConnection); } QList CollectionManagerPrivate::listVolumes() { // Move calls to Solid to the main thread. // Solid was meant to be thread-safe, but it is not (KDE4.0), // calling from a non-UI thread leads to a reversible // lock-up of variable length. if (QThread::currentThread() == QCoreApplication::instance()->thread()) { return actuallyListVolumes(); } else { // emit a blocking queued signal to move call to main thread emit(s->triggerUpdateVolumesList()); return volumesListCache; } } void CollectionManagerPrivate::slotTriggerUpdateVolumesList() { volumesListCache = actuallyListVolumes(); } QList CollectionManagerPrivate::actuallyListVolumes() { QList volumes; //qCDebug(DIGIKAM_DATABASE_LOG) << "listFromType"; QList devices = Solid::Device::listFromType(Solid::DeviceInterface::StorageAccess); //qCDebug(DIGIKAM_DATABASE_LOG) << "got listFromType"; udisToWatch.clear(); foreach(const Solid::Device& accessDevice, devices) { // check for StorageAccess if (!accessDevice.is()) { continue; } // mark as a device of principal interest udisToWatch << accessDevice.udi(); const Solid::StorageAccess* access = accessDevice.as(); // watch mount status (remove previous connections) QObject::disconnect(access, SIGNAL(accessibilityChanged(bool,QString)), s, SLOT(accessibilityChanged(bool,QString))); QObject::connect(access, SIGNAL(accessibilityChanged(bool,QString)), s, SLOT(accessibilityChanged(bool,QString))); if (!access->isAccessible()) { continue; } // check for StorageDrive Solid::Device driveDevice; for (Solid::Device currentDevice = accessDevice; currentDevice.isValid(); currentDevice = currentDevice.parent()) { if (currentDevice.is()) { driveDevice = currentDevice; break; } } /* * We cannot require a drive, some logical volumes may not have "one" drive as parent * See bug 273369 if (!driveDevice.isValid()) { continue; } */ Solid::StorageDrive* drive = driveDevice.as(); // check for StorageVolume Solid::Device volumeDevice; for (Solid::Device currentDevice = accessDevice; currentDevice.isValid(); currentDevice = currentDevice.parent()) { if (currentDevice.is()) { volumeDevice = currentDevice; break; } } if (!volumeDevice.isValid()) { continue; } Solid::StorageVolume* const volume = volumeDevice.as(); SolidVolumeInfo info; info.udi = accessDevice.udi(); info.path = QDir::fromNativeSeparators(access->filePath()); info.isMounted = access->isAccessible(); if (!info.path.isEmpty() && !info.path.endsWith(QLatin1Char('/'))) { info.path += QLatin1Char('/'); } info.uuid = volume->uuid(); info.label = volume->label(); if (drive) { info.isRemovable = drive->isHotpluggable() || drive->isRemovable(); } else { // impossible to know, but probably not hotpluggable (see comment above) info.isRemovable = false; } info.isOpticalDisc = volumeDevice.is(); volumes << info; } // This is the central place where the watch is enabled watchEnabled = true; return volumes; } QString CollectionManagerPrivate::volumeIdentifier(const SolidVolumeInfo& volume) { QUrl url; url.setScheme(QLatin1String("volumeid")); // On changing these, please update the checkLocation() code bool identifyByUUID = !volume.uuid.isEmpty(); bool identifyByLabel = !identifyByUUID && !volume.label.isEmpty() && (volume.isOpticalDisc || volume.isRemovable); bool addDirectoryHash = identifyByLabel && volume.isOpticalDisc; bool identifyByMountPath = !identifyByUUID && !identifyByLabel; if (identifyByUUID) { QUrlQuery q(url); q.addQueryItem(QLatin1String("uuid"), volume.uuid); url.setQuery(q); } if (identifyByLabel) { QUrlQuery q(url); q.addQueryItem(QLatin1String("label"), volume.label); url.setQuery(q); } if (addDirectoryHash) { // for CDs, we store a hash of the root directory. May be useful. QString dirHash = directoryHash(volume.path); if (!dirHash.isNull()) { QUrlQuery q(url); q.addQueryItem(QLatin1String("directoryhash"), dirHash); url.setQuery(q); } } if (identifyByMountPath) { QUrlQuery q(url); q.addQueryItem(QLatin1String("mountpath"), volume.path); url.setQuery(q); } return url.url(); } QString CollectionManagerPrivate::volumeIdentifier(const QString& path) { QUrl url; url.setScheme(QLatin1String("volumeid")); QUrlQuery q(url); q.addQueryItem(QLatin1String("path"), path); url.setQuery(q); return url.url(); } QString CollectionManagerPrivate::networkShareIdentifier(const QString& path) { QUrl url; url.setScheme(QLatin1String("networkshareid")); QUrlQuery q(url); q.addQueryItem(QLatin1String("mountpath"), path); url.setQuery(q); return url.url(); } QString CollectionManagerPrivate::pathFromIdentifier(const AlbumRootLocation* location) { QUrl url(location->identifier); if (url.scheme() != QLatin1String("volumeid")) { return QString(); } return QUrlQuery(url).queryItemValue(QLatin1String("path")); } QStringList CollectionManagerPrivate::networkShareMountPathsFromIdentifier(const AlbumRootLocation* location) { // using a QUrl because QUrl cannot handle duplicate query items QUrl url(location->identifier); if (url.scheme() != QLatin1String("networkshareid")) { return QStringList(); } return QUrlQuery(url).allQueryItemValues(QLatin1String("mountpath")); } QString CollectionManagerPrivate::directoryHash(const QString& path) { QDir dir(path); if (dir.isReadable()) { QStringList entries = dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); QCryptographicHash md5(QCryptographicHash::Md5); foreach(const QString& entry, entries) { md5.addData(entry.toUtf8()); } return QString::fromUtf8(md5.result().toHex()); } return QString(); } SolidVolumeInfo CollectionManagerPrivate::findVolumeForLocation(const AlbumRootLocation* location, const QList volumes) { QUrl url(location->identifier); QString queryItem; if (url.scheme() != QLatin1String("volumeid")) { return SolidVolumeInfo(); } if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("uuid"))).isNull()) { foreach(const SolidVolumeInfo& volume, volumes) { if (volume.uuid.compare(queryItem, Qt::CaseInsensitive) == 0) { return volume; } } return SolidVolumeInfo(); } else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("label"))).isNull()) { // This one is a bit more difficult, as we take into account the possibility // that the label is not unique, and we take some care to make it work anyway. // find all available volumes with the given label (usually one) QList candidateVolumes; foreach(const SolidVolumeInfo& volume, volumes) { if (volume.label == queryItem) { candidateVolumes << volume; } } if (candidateVolumes.isEmpty()) { return SolidVolumeInfo(); } // find out of there is another location with the same label (usually not) bool hasOtherLocation = false; foreach(AlbumRootLocation* const otherLocation, locations) { if (otherLocation == location) { continue; } QUrl otherUrl(otherLocation->identifier); if (otherUrl.scheme() == QLatin1String("volumeid") && QUrlQuery(otherUrl).queryItemValue(QLatin1String("label")) == queryItem) { hasOtherLocation = true; break; } } // the usual, easy case if (candidateVolumes.size() == 1 && !hasOtherLocation) { return candidateVolumes.first(); } else { // not unique: try to use the directoryhash QString dirHash = QUrlQuery(url).queryItemValue(QLatin1String("directoryhash")); // bail out if not provided if (dirHash.isNull()) { qCDebug(DIGIKAM_DATABASE_LOG) << "No directory hash specified for the non-unique Label" << queryItem << "Resorting to returning the first match."; return candidateVolumes.first(); } // match against directory hash foreach(const SolidVolumeInfo& volume, candidateVolumes) { QString volumeDirHash = directoryHash(volume.path); if (volumeDirHash == dirHash) { return volume; } } } return SolidVolumeInfo(); } else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("mountpath"))).isNull()) { foreach(const SolidVolumeInfo& volume, volumes) { if (volume.isMounted && volume.path == queryItem) { return volume; } } return SolidVolumeInfo(); } return SolidVolumeInfo(); } QString CollectionManagerPrivate::technicalDescription(const AlbumRootLocation* albumLoc) { QUrl url(albumLoc->identifier); QString queryItem; if (url.scheme() == QLatin1String("volumeid")) { if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("uuid"))).isNull()) { return i18nc("\"relative path\" on harddisk partition with \"UUID\"", "Folder \"%1\" on the volume with the id \"%2\"", QDir::toNativeSeparators(albumLoc->specificPath), queryItem); } else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("label"))).isNull()) { return i18nc("\"relative path\" on harddisk partition with \"label\"", "Folder \"%1\" on the volume labeled \"%2\"", QDir::toNativeSeparators(albumLoc->specificPath), queryItem); } else if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("mountpath"))).isNull()) { return QString::fromUtf8("\"%1\"").arg(queryItem); } } else if (url.scheme() == QLatin1String("networkshareid")) { if (!(queryItem = QUrlQuery(url).queryItemValue(QLatin1String("mountpath"))).isNull()) { return i18nc("@info", "Shared directory mounted at %1", QDir::toNativeSeparators(queryItem)); } } return QString(); } SolidVolumeInfo CollectionManagerPrivate::findVolumeForUrl(const QUrl& fileUrl, const QList volumes) { SolidVolumeInfo volume; // v.path is specified to have a trailing slash. path needs one as well. QString path = fileUrl.toLocalFile() + QLatin1String("/"); int volumeMatch = 0; //FIXME: Network shares! Here we get only the volume of the mount path... // This is probably not really clean. But Solid does not help us. foreach(const SolidVolumeInfo& v, volumes) { if (v.isMounted && !v.path.isEmpty() && path.startsWith(v.path)) { int length = v.path.length(); if (length > volumeMatch) { volumeMatch = v.path.length(); volume = v; } } } if (!volumeMatch) { qCDebug(DIGIKAM_DATABASE_LOG) << "Failed to detect a storage volume for path " << path << " with Solid"; } return volume; } bool CollectionManagerPrivate::checkIfExists(const QString& filePath, QList assumeDeleted) { const QUrl filePathUrl = QUrl::fromLocalFile(filePath); CoreDbAccess access; foreach(AlbumRootLocation* const location, locations) { const QUrl locationPathUrl = QUrl::fromLocalFile(location->albumRootPath()); //qCDebug(DIGIKAM_DATABASE_LOG) << filePathUrl << locationPathUrl; // make sure filePathUrl is neither a child nor a parent // of an existing collection if (!locationPathUrl.isEmpty() && (filePathUrl.isParentOf(locationPathUrl) || locationPathUrl.isParentOf(filePathUrl)) ) { bool isDeleted = false; foreach(const CollectionLocation& deletedLoc, assumeDeleted) { if (deletedLoc.id() == location->id()) { isDeleted = true; break; } } if (!isDeleted) { return true; } } } return false; } // ------------------------------------------------- CollectionManager* CollectionManager::m_instance = 0; CollectionManager* CollectionManager::instance() { if (!m_instance) { m_instance = new CollectionManager; } return m_instance; } void CollectionManager::cleanUp() { delete m_instance; m_instance = 0; } CollectionManager::CollectionManager() : d(new CollectionManagerPrivate(this)) { qRegisterMetaType("CollectionLocation"); connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceAdded(QString)), this, SLOT(deviceAdded(QString))); connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceRemoved(QString)), this, SLOT(deviceRemoved(QString))); // CoreDbWatch slot is connected at construction of CoreDbWatch, which may be later. } CollectionManager::~CollectionManager() { qDeleteAll(d->locations.values()); delete d; } void CollectionManager::refresh() { { // if called from the CoreDbAccess constructor itself, it will // hold a flag to prevent endless recursion CoreDbAccess access; clear_locked(); } updateLocations(); } void CollectionManager::setWatchDisabled() { d->watchEnabled = false; } CollectionLocation CollectionManager::addLocation(const QUrl& fileUrl, const QString& label) { qCDebug(DIGIKAM_DATABASE_LOG) << "addLocation " << fileUrl; QString path = fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile(); if (!locationForPath(path).isNull()) { return CollectionLocation(); } QList volumes = d->listVolumes(); SolidVolumeInfo volume = d->findVolumeForUrl(fileUrl, volumes); if (!volume.isNull()) { CoreDbAccess access; // volume.path has a trailing slash. We want to split in front of this. QString specificPath = path.mid(volume.path.length() - 1); AlbumRoot::Type type; if (volume.isRemovable) { type = AlbumRoot::VolumeRemovable; } else { type = AlbumRoot::VolumeHardWired; } ChangingDB changing(d); access.db()->addAlbumRoot(type, d->volumeIdentifier(volume), specificPath, label); } else { // Empty volumes indicates that Solid is not working correctly. if (volumes.isEmpty()) { qCDebug(DIGIKAM_DATABASE_LOG) << "Solid did not return any storage volumes on your system."; qCDebug(DIGIKAM_DATABASE_LOG) << "This indicates a missing implementation or a problem with your installation"; qCDebug(DIGIKAM_DATABASE_LOG) << "On Linux, check that Solid and HAL are working correctly." "Problems with RAID partitions have been reported, if you have RAID this error may be normal."; qCDebug(DIGIKAM_DATABASE_LOG) << "On Windows, Solid may not be fully implemented, if you are running Windows this error may be normal."; } // fall back qCWarning(DIGIKAM_DATABASE_LOG) << "Unable to identify a path with Solid. Adding the location with path only."; ChangingDB changing(d); CoreDbAccess().db()->addAlbumRoot(AlbumRoot::VolumeHardWired, d->volumeIdentifier(path), QLatin1String("/"), label); } // Do not emit the locationAdded signal here, it is done in updateLocations() updateLocations(); return locationForPath(path); } CollectionLocation CollectionManager::addNetworkLocation(const QUrl& fileUrl, const QString& label) { qCDebug(DIGIKAM_DATABASE_LOG) << "addLocation " << fileUrl; QString path = fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile(); if (!locationForPath(path).isNull()) { return CollectionLocation(); } ChangingDB changing(d); CoreDbAccess().db()->addAlbumRoot(AlbumRoot::Network, d->networkShareIdentifier(path), QLatin1String("/"), label); // Do not emit the locationAdded signal here, it is done in updateLocations() updateLocations(); return locationForPath(path); } CollectionManager::LocationCheckResult CollectionManager::checkLocation(const QUrl& fileUrl, QList assumeDeleted, QString* message, QString* iconName) { if (!fileUrl.isLocalFile()) { if (message) { *message = i18n("Sorry, digiKam does not support remote URLs as collections."); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } QString path = fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile(); QDir dir(path); if (!dir.isReadable()) { if (message) { *message = i18n("The selected folder does not exist or is not readable"); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } if (d->checkIfExists(path, assumeDeleted)) { if (message) { *message = i18n("There is already a collection containing the folder \"%1\"", QDir::toNativeSeparators(path)); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } QList volumes = d->listVolumes(); SolidVolumeInfo volume = d->findVolumeForUrl(fileUrl, volumes); if (!volume.isNull()) { if (!volume.uuid.isEmpty()) { if (volume.isRemovable) { if (message) { *message = i18n("The storage media can be uniquely identified."); } if (iconName) { *iconName = QLatin1String("drive-removable-media"); } } else { if (message) { *message = i18n("The collection is located on your harddisk"); } if (iconName) { *iconName = QLatin1String("drive-harddisk"); } } return LocationAllRight; } else if (!volume.label.isEmpty() && (volume.isOpticalDisc || volume.isRemovable)) { if (volume.isOpticalDisc) { bool hasOtherLocation = false; foreach(AlbumRootLocation* const otherLocation, d->locations) { QUrl otherUrl(otherLocation->identifier); if (otherUrl.scheme() == QLatin1String("volumeid") && QUrlQuery(otherUrl).queryItemValue(QLatin1String("label")) == volume.label) { hasOtherLocation = true; break; } } if (iconName) { *iconName = QLatin1String("media-optical"); } if (hasOtherLocation) { if (message) *message = i18n("This is a CD/DVD, which is identified by the label " "that you can set in your CD burning application. " "There is already another entry with the same label. " "The two will be distinguished by the files in the top directory, " "so please do not append files to the CD, or it will not be recognized. " "In the future, please set a unique label on your CDs and DVDs " "if you intend to use them with digiKam."); return LocationHasProblems; } else { if (message) *message = i18n("This is a CD/DVD. It will be identified by the label (\"%1\")" "that you have set in your CD burning application. " "If you create further CDs for use with digikam in the future, " "please remember to give them a unique label as well.", volume.label); return LocationAllRight; } } else { // Which situation? HasProblems or AllRight? if (message) *message = i18n("This is a removable storage medium that will be identified by its label (\"%1\")", volume.label); if (iconName) { *iconName = QLatin1String("drive-removable-media"); } return LocationAllRight; } } else { if (message) *message = i18n("This entry will only be identified by the path where it is found on your system (\"%1\"). " "No more specific means of identification (UUID, label) is available.", QDir::toNativeSeparators(volume.path)); if (iconName) { *iconName = QLatin1String("drive-removale-media"); } return LocationHasProblems; } } else { if (message) *message = i18n("It is not possible on your system to identify the storage medium of this path. " "It will be added using the file path as the only identifier. " "This will work well for your local hard disk."); if (iconName) { *iconName = QLatin1String("folder-important"); } return LocationHasProblems; } } CollectionManager::LocationCheckResult CollectionManager::checkNetworkLocation(const QUrl& fileUrl, QList assumeDeleted, QString* message, QString* iconName) { if (!fileUrl.isLocalFile()) { if (message) { if (fileUrl.scheme() == QLatin1String("smb")) *message = i18n("You need to locally mount your Samba share. " "Sorry, digiKam does currently not support smb:// URLs. "); else *message = i18n("Your network storage must be set up to be accessible " "as files and folders through the operating system. " "DigiKam does not support remote URLs."); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } QString path = fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile(); QDir dir(path); if (!dir.isReadable()) { if (message) { *message = i18n("The selected folder does not exist or is not readable"); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } if (d->checkIfExists(path, assumeDeleted)) { if (message) { *message = i18n("There is already a collection for a network share with the same path."); } if (iconName) { *iconName = QLatin1String("dialog-error"); } return LocationNotAllowed; } if (message) *message = i18n("The network share will be identified by the path you selected. " "If the path is empty, the share will be considered unavailable."); if (iconName) { *iconName = QLatin1String("network-wired-activated"); } return LocationAllRight; } void CollectionManager::removeLocation(const CollectionLocation& location) { { CoreDbAccess access; AlbumRootLocation* const albumLoc = d->locations.value(location.id()); if (!albumLoc) { return; } // Ensure that all albums are set to orphan and no images will be permanently deleted, // as would do only calling deleteAlbumRoot by a Trigger QList albumIds = access.db()->getAlbumsOnAlbumRoot(albumLoc->id()); ChangingDB changing(d); CollectionScanner scanner; CoreDbTransaction transaction(&access); scanner.safelyRemoveAlbums(albumIds); access.db()->deleteAlbumRoot(albumLoc->id()); } // Do not emit the locationRemoved signal here, it is done in updateLocations() updateLocations(); } QList CollectionManager::checkHardWiredLocations() { QList disappearedLocations; QList volumes = d->listVolumes(); CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { // Hardwired and unavailable? if (location->type() == CollectionLocation::TypeVolumeHardWired && location->status() == CollectionLocation::LocationUnavailable) { disappearedLocations << *location; } } return disappearedLocations; } void CollectionManager::migrationCandidates(const CollectionLocation& location, QString* const description, QStringList* const candidateIdentifiers, QStringList* const candidateDescriptions) { description->clear(); candidateIdentifiers->clear(); candidateDescriptions->clear(); QList volumes = d->listVolumes(); CoreDbAccess access; AlbumRootLocation* const albumLoc = d->locations.value(location.id()); if (!albumLoc) { return; } *description = d->technicalDescription(albumLoc); // Find possible new volumes where the specific path is found. foreach(const SolidVolumeInfo& info, volumes) { if (info.isMounted && !info.path.isEmpty()) { QDir dir(info.path + albumLoc->specificPath); if (dir.exists()) { *candidateIdentifiers << d->volumeIdentifier(info); *candidateDescriptions << dir.absolutePath(); } } } } void CollectionManager::migrateToVolume(const CollectionLocation& location, const QString& identifier) { CoreDbAccess access; AlbumRootLocation* const albumLoc = d->locations.value(location.id()); if (!albumLoc) { return; } // update db ChangingDB db(d); access.db()->migrateAlbumRoot(albumLoc->id(), identifier); albumLoc->identifier = identifier; updateLocations(); } void CollectionManager::setLabel(const CollectionLocation& location, const QString& label) { CoreDbAccess access; AlbumRootLocation* const albumLoc = d->locations.value(location.id()); if (!albumLoc) { return; } // update db ChangingDB db(d); access.db()->setAlbumRootLabel(albumLoc->id(), label); // update local structure albumLoc->setLabel(label); emit locationPropertiesChanged(*albumLoc); } void CollectionManager::changeType(const CollectionLocation& location, int type) { CoreDbAccess access; AlbumRootLocation* const albumLoc = d->locations.value(location.id()); if (!albumLoc) { return; } // update db ChangingDB db(d); access.db()->changeAlbumRootType(albumLoc->id(), (AlbumRoot::Type)type); // update local structure albumLoc->setType((CollectionLocation::Type)type); emit locationPropertiesChanged(*albumLoc); } QList CollectionManager::allLocations() { CoreDbAccess access; QList list; foreach(AlbumRootLocation* const location, d->locations) { list << *location; } return list; } QList CollectionManager::allAvailableLocations() { CoreDbAccess access; QList list; foreach(AlbumRootLocation* const location, d->locations) { if (location->status() == CollectionLocation::LocationAvailable) { list << *location; } } return list; } QStringList CollectionManager::allAvailableAlbumRootPaths() { CoreDbAccess access; QStringList list; foreach(AlbumRootLocation* const location, d->locations) { if (location->status() == CollectionLocation::LocationAvailable) { list << location->albumRootPath(); } } return list; } CollectionLocation CollectionManager::locationForAlbumRootId(int id) { CoreDbAccess access; AlbumRootLocation* const location = d->locations.value(id); if (location) { return *location; } else { return CollectionLocation(); } } CollectionLocation CollectionManager::locationForAlbumRoot(const QUrl& fileUrl) { return locationForAlbumRootPath(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } CollectionLocation CollectionManager::locationForAlbumRootPath(const QString& albumRootPath) { + if (!QFileInfo::exists(albumRootPath)) + { + qCWarning(DIGIKAM_DATABASE_LOG) << "Album root path not exist" << albumRootPath; + qCWarning(DIGIKAM_DATABASE_LOG) << "Drive or network connection broken?"; + + updateLocations(); + } + CoreDbAccess access; - QString path = albumRootPath; foreach(AlbumRootLocation* const location, d->locations) { - if (location->albumRootPath() == path) + if (location->albumRootPath() == albumRootPath) { return *location; } } return CollectionLocation(); } CollectionLocation CollectionManager::locationForUrl(const QUrl& fileUrl) { return locationForPath(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } CollectionLocation CollectionManager::locationForPath(const QString& givenPath) { CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { QString rootPath = location->albumRootPath(); QString filePath = QDir::fromNativeSeparators(givenPath); if (!rootPath.isEmpty() && filePath.startsWith(rootPath)) { // see also bug #221155 for extra checks if (filePath == rootPath || filePath.startsWith(rootPath + QLatin1Char('/'))) { return *location; } } } return CollectionLocation(); } QString CollectionManager::albumRootPath(int id) { CoreDbAccess access; CollectionLocation* const location = d->locations.value(id); if (location && location->status() == CollectionLocation::LocationAvailable) { return location->albumRootPath(); } return QString(); } QString CollectionManager::albumRootLabel(int id) { CoreDbAccess access; CollectionLocation* const location = d->locations.value(id); if (location && location->status() == CollectionLocation::LocationAvailable) { return location->label(); } return QString(); } QUrl CollectionManager::albumRoot(const QUrl& fileUrl) { return QUrl::fromLocalFile(albumRootPath(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile())); } QString CollectionManager::albumRootPath(const QUrl& fileUrl) { return albumRootPath(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } QString CollectionManager::albumRootPath(const QString& givenPath) { CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { QString rootPath = location->albumRootPath(); QString filePath = QDir::fromNativeSeparators(givenPath); if (!rootPath.isEmpty() && filePath.startsWith(rootPath)) { // see also bug #221155 for extra checks if (filePath == rootPath || filePath.startsWith(rootPath + QLatin1Char('/'))) { return location->albumRootPath(); } } } return QString(); } bool CollectionManager::isAlbumRoot(const QUrl& fileUrl) { return isAlbumRoot(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } bool CollectionManager::isAlbumRoot(const QString& filePath) { CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { if (filePath == location->albumRootPath()) { return true; } } return false; } QString CollectionManager::album(const QUrl& fileUrl) { return album(fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } QString CollectionManager::album(const QString& filePath) { CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { QString absolutePath = location->albumRootPath(); if (absolutePath.isEmpty()) { continue; } QString firstPart = filePath.left(absolutePath.length()); if (firstPart == absolutePath) { if (filePath == absolutePath || (filePath.length() == absolutePath.length() + 1 && filePath.right(1) == QLatin1String("/"))) { return QLatin1String("/"); } else { QString album = filePath.mid(absolutePath.length()); if (album.endsWith(QLatin1Char('/'))) { album.chop(1); } return album; } } } return QString(); } QString CollectionManager::album(const CollectionLocation& location, const QUrl& fileUrl) { return album(location, fileUrl.adjusted(QUrl::StripTrailingSlash).toLocalFile()); } QString CollectionManager::album(const CollectionLocation& location, const QString& filePath) { if (location.isNull()) { return QString(); } QString absolutePath = location.albumRootPath(); if (filePath == absolutePath) { return QLatin1String("/"); } else { QString album = filePath.mid(absolutePath.length()); if (album.endsWith(QLatin1Char('/'))) { album.chop(1); } return album; } } QUrl CollectionManager::oneAlbumRoot() { return QUrl::fromLocalFile(oneAlbumRootPath()); } QString CollectionManager::oneAlbumRootPath() { CoreDbAccess access; foreach(AlbumRootLocation* const location, d->locations) { if (location->status() == CollectionLocation::LocationAvailable) { return location->albumRootPath(); } } return QString(); } void CollectionManager::deviceAdded(const QString& udi) { if (!d->watchEnabled) { return; } Solid::Device device(udi); if (device.is()) { updateLocations(); } } void CollectionManager::deviceRemoved(const QString& udi) { if (!d->watchEnabled) { return; } // we can't access the Solid::Device to check because it is removed CoreDbAccess access; if (!d->udisToWatch.contains(udi)) { return; } updateLocations(); } void CollectionManager::accessibilityChanged(bool accessible, const QString& udi) { Q_UNUSED(accessible); Q_UNUSED(udi); updateLocations(); } void CollectionManager::updateLocations() { // get information from Solid QList volumes; { // Absolutely ensure that the db mutex is not held when emitting the blocking queued signal! Deadlock! CoreDbAccessUnlock unlock; volumes = d->listVolumes(); } { CoreDbAccess access; // read information from database QList infos = access.db()->getAlbumRoots(); // synchronize map with database QMap locs = d->locations; d->locations.clear(); foreach(const AlbumRootInfo& info, infos) { if (locs.contains(info.id)) { d->locations[info.id] = locs.value(info.id); locs.remove(info.id); } else { d->locations[info.id] = new AlbumRootLocation(info); } } // delete old locations foreach(AlbumRootLocation* const location, locs) { CollectionLocation::Status oldStatus = location->status(); location->setStatus(CollectionLocation::LocationDeleted); emit locationStatusChanged(*location, oldStatus); delete location; } // update status with current access state, store old status in list QList oldStatus; foreach(AlbumRootLocation* const location, d->locations) { oldStatus << location->status(); bool available = false; QString absolutePath; if (location->type() == CollectionLocation::TypeNetwork) { foreach(const QString& path, d->networkShareMountPathsFromIdentifier(location)) { QDir dir(path); available = dir.isReadable() && dir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot).count() > 0; absolutePath = path; if (available) { break; } } } else { SolidVolumeInfo info = d->findVolumeForLocation(location, volumes); if (!info.isNull()) { available = info.isMounted; QString volumePath = info.path; // volume.path has a trailing slash (and this is good) // but specific path has a leading slash, so remove it volumePath.chop(1); // volumePath is the mount point of the volume; // specific path is the path on the file system of the volume. absolutePath = volumePath + location->specificPath; } else { QString path = d->pathFromIdentifier(location); if (!path.isNull()) { available = true; // Here we have the absolute path as definition of the volume. // specificPath is "/" as per convention, but ignored, // absolute path shall not have a trailing slash. absolutePath = path; } } } // set values in location // Don't touch location->status, do not interfere with "hidden" setting location->available = available; location->setAbsolutePath(absolutePath); qCDebug(DIGIKAM_DATABASE_LOG) << "location for " << absolutePath << " is available " << available; // set the status depending on "hidden" and "available" location->setStatusFromFlags(); } // emit status changes (and new locations) int i = 0; foreach(AlbumRootLocation* const location, d->locations) { if (oldStatus.at(i) != location->status()) { emit locationStatusChanged(*location, oldStatus.at(i)); } ++i; } } } void CollectionManager::clear_locked() { // Internal method: Called with mutex locked // Cave: Difficult recursions with CoreDbAccess constructor and setParameters foreach(AlbumRootLocation* const location, d->locations) { CollectionLocation::Status oldStatus = location->status(); location->setStatus(CollectionLocation::LocationDeleted); emit locationStatusChanged(*location, oldStatus); delete location; } d->locations.clear(); } void CollectionManager::slotAlbumRootChange(const AlbumRootChangeset& changeset) { if (d->changingDB) { return; } switch (changeset.operation()) { case AlbumRootChangeset::Added: case AlbumRootChangeset::Deleted: updateLocations(); break; case AlbumRootChangeset::PropertiesChanged: // label has changed { CollectionLocation toBeEmitted; { CoreDbAccess access; AlbumRootLocation* const location = d->locations.value(changeset.albumRootId()); if (location) { QList infos = access.db()->getAlbumRoots(); foreach(const AlbumRootInfo& info, infos) { if (info.id == location->id()) { location->setLabel(info.label); toBeEmitted = *location; break; } } } } if (!toBeEmitted.isNull()) { emit locationPropertiesChanged(toBeEmitted); } } break; case AlbumRootChangeset::Unknown: break; } } } // namespace Digikam #include "moc_collectionmanager.cpp" diff --git a/core/libs/database/coredb/coredb.cpp b/core/libs/database/coredb/coredb.cpp index 0bea92bb36..ea3fddd58e 100644 --- a/core/libs/database/coredb/coredb.cpp +++ b/core/libs/database/coredb/coredb.cpp @@ -1,4980 +1,4987 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2004-06-18 * Description : Core database interface. * * Copyright (C) 2004-2005 by Renchi Raju * Copyright (C) 2006-2018 by Gilles Caulier * Copyright (C) 2006-2012 by Marcel Wiesweg * Copyright (C) 2012 by Andi Clemens * * 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, 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. * * ============================================================ */ #include "coredb.h" // C ANSI includes extern "C" { #include } // C++ includes #include #include #include // Qt includes #include #include #include #include // KDE includes #include #include // Local includes #include "digikam_debug.h" #include "coredbbackend.h" #include "collectionmanager.h" #include "collectionlocation.h" #include "dbengineactiontype.h" #include "tagscache.h" #include "album.h" namespace Digikam { class CoreDB::Private { public: explicit Private() : db(0), uniqueHashVersion(-1) { } static const QString configGroupName; static const QString configRecentlyUsedTags; CoreDbBackend* db; QList recentlyAssignedTags; int uniqueHashVersion; public: QString constructRelatedImagesSQL(bool fromOrTo, DatabaseRelation::Type type, bool boolean); QList execRelatedImagesQuery(DbEngineSqlQuery& query, qlonglong id, DatabaseRelation::Type type); }; const QString CoreDB::Private::configGroupName(QLatin1String("CoreDB Settings")); const QString CoreDB::Private::configRecentlyUsedTags(QLatin1String("Recently Used Tags")); QString CoreDB::Private::constructRelatedImagesSQL(bool fromOrTo, DatabaseRelation::Type type, bool boolean) { QString sql; if (fromOrTo) { sql = QString::fromUtf8("SELECT object FROM ImageRelations " "INNER JOIN Images ON ImageRelations.object=Images.id " "WHERE subject=? %1 AND status!=3 %2;"); } else { sql = QString::fromUtf8("SELECT subject FROM ImageRelations " "INNER JOIN Images ON ImageRelations.subject=Images.id " "WHERE object=? %1 AND status!=3 %2;"); } if (type != DatabaseRelation::UndefinedType) { sql = sql.arg(QString::fromUtf8("AND type=?")); } else { sql = sql.arg(QString()); } if (boolean) { sql = sql.arg(QString::fromUtf8("LIMIT 1")); } else { sql = sql.arg(QString()); } return sql; } QList CoreDB::Private::execRelatedImagesQuery(DbEngineSqlQuery& query, qlonglong id, DatabaseRelation::Type type) { QVariantList values; if (type == DatabaseRelation::UndefinedType) { db->execSql(query, id, &values); } else { db->execSql(query, id, type, &values); } QList imageIds; if (values.isEmpty()) { return imageIds; } for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { imageIds << (*it).toInt(); } return imageIds; } // -------------------------------------------------------- CoreDB::CoreDB(CoreDbBackend* const backend) : d(new Private) { d->db = backend; readSettings(); } CoreDB::~CoreDB() { writeSettings(); delete d; } QList CoreDB::getAlbumRoots() { QList list; QList values; d->db->execSql(QString::fromUtf8("SELECT id, label, status, type, identifier, specificPath FROM AlbumRoots;"), &values); for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { AlbumRootInfo info; info.id = (*it).toInt(); ++it; info.label = (*it).toString(); ++it; info.status = (*it).toInt(); ++it; info.type = (AlbumRoot::Type)(*it).toInt(); ++it; info.identifier = (*it).toString(); ++it; info.specificPath = (*it).toString(); ++it; list << info; } return list; } int CoreDB::addAlbumRoot(AlbumRoot::Type type, const QString& identifier, const QString& specificPath, const QString& label) { QVariant id; d->db->execSql(QString::fromUtf8("REPLACE INTO AlbumRoots (type, label, status, identifier, specificPath) " "VALUES(?, ?, 0, ?, ?);"), (int)type, label, identifier, specificPath, 0, &id); d->db->recordChangeset(AlbumRootChangeset(id.toInt(), AlbumRootChangeset::Added)); return id.toInt(); } void CoreDB::deleteAlbumRoot(int rootId) { d->db->execSql(QString::fromUtf8("DELETE FROM AlbumRoots WHERE id=?;"), rootId); QMap parameters; parameters.insert(QLatin1String(":albumRoot"), rootId); if (BdEngineBackend::NoErrors != d->db->execDBAction(d->db->getDBAction(QLatin1String("deleteAlbumRoot")), parameters)) { return; } d->db->recordChangeset(AlbumRootChangeset(rootId, AlbumRootChangeset::Deleted)); } void CoreDB::migrateAlbumRoot(int rootId, const QString& identifier) { d->db->execSql(QString::fromUtf8("UPDATE AlbumRoots SET identifier=? WHERE id=?;"), identifier, rootId); d->db->recordChangeset(AlbumRootChangeset(rootId, AlbumRootChangeset::PropertiesChanged)); } void CoreDB::setAlbumRootLabel(int rootId, const QString& newLabel) { d->db->execSql(QString::fromUtf8("UPDATE AlbumRoots SET label=? WHERE id=?;"), newLabel, rootId); d->db->recordChangeset(AlbumRootChangeset(rootId, AlbumRootChangeset::PropertiesChanged)); } void CoreDB::changeAlbumRootType(int rootId, AlbumRoot::Type newType) { d->db->execSql(QString::fromUtf8("UPDATE AlbumRoots SET type=? WHERE id=?;"), (int)newType, rootId); d->db->recordChangeset(AlbumRootChangeset(rootId, AlbumRootChangeset::PropertiesChanged)); } AlbumInfo::List CoreDB::scanAlbums() { AlbumInfo::List aList; QList values; d->db->execSql(QString::fromUtf8("SELECT albumRoot, id, relativePath, date, caption, collection, icon FROM Albums " " WHERE albumRoot != 0;"), // exclude stale albums &values); QString iconAlbumUrl, iconName; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { AlbumInfo info; info.albumRootId = (*it).toInt(); ++it; info.id = (*it).toInt(); ++it; info.relativePath = (*it).toString(); ++it; info.date = (*it).toDate(); ++it; info.caption = (*it).toString(); ++it; info.category = (*it).toString(); ++it; info.iconId = (*it).toLongLong(); ++it; aList.append(info); } return aList; } TagInfo::List CoreDB::scanTags() { TagInfo::List tList; QList values; d->db->execSql(QString::fromUtf8("SELECT id, pid, name, icon, iconkde FROM Tags;"), &values); for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { TagInfo info; info.id = (*it).toInt(); ++it; info.pid = (*it).toInt(); ++it; info.name = (*it).toString(); ++it; info.iconId = (*it).toLongLong(); ++it; info.icon = (*it).toString(); ++it; tList.append(info); } return tList; } TagInfo CoreDB::getTagInfo(int tagId) { QList values; d->db->execSql(QString::fromUtf8("SELECT id, pid, name, icon, iconkde WHERE id=? FROM Tags;"), tagId, &values); TagInfo info; if (!values.isEmpty() && values.size() == 5) { QList::const_iterator it = values.constBegin(); info.id = (*it).toInt(); ++it; info.pid = (*it).toInt(); ++it; info.name = (*it).toString(); ++it; info.iconId = (*it).toLongLong(); ++it; info.icon = (*it).toString(); ++it; } return info; } SearchInfo::List CoreDB::scanSearches() { SearchInfo::List searchList; QList values; d->db->execSql(QString::fromUtf8("SELECT id, type, name, query FROM Searches;"), &values); for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { SearchInfo info; info.id = (*it).toInt(); ++it; info.type = (DatabaseSearch::Type)(*it).toInt(); ++it; info.name = (*it).toString(); ++it; info.query = (*it).toString(); ++it; searchList.append(info); } return searchList; } QList CoreDB::getAlbumShortInfos() { QList values; d->db->execSql(QString::fromUtf8("SELECT Albums.id, Albums.relativePath, Albums.albumRoot from Albums ORDER BY Albums.id;"), &values); QList albumList; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { AlbumShortInfo info; info.id = (*it).toInt(); ++it; info.relativePath = (*it).toString(); ++it; info.albumRootId = (*it).toInt(); ++it; albumList << info; } return albumList; } QList CoreDB::getTagShortInfos() { QList values; d->db->execSql(QString::fromUtf8("SELECT id, pid, name FROM Tags ORDER BY id;"), &values); QList tagList; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { TagShortInfo info; info.id = (*it).toInt(); ++it; info.pid = (*it).toInt(); ++it; info.name = (*it).toString(); ++it; tagList << info; } return tagList; } /* QStringList CoreDB::getSubalbumsForPath(const QString& albumRoot, const QString& path, bool onlyDirectSubalbums) { CollectionLocation location = CollectionManager::instance()->locationForAlbumRootPath(albumRoot); if (location.isNull()) return QStringList(); QString subURL = path; if (!path.endsWith(QLatin1String("/"))) subURL += QLatin1Char('/'); subURL = (subURL); QList values; if (onlyDirectSubalbums) { d->db->execSql( QString::fromUtf8("SELECT relativePath FROM Albums WHERE albumRoot=? AND relativePath LIKE '") + subURL + QString::fromUtf8("%' ") + QString::fromUtf8("AND relativePath NOT LIKE '") + subURL + QString::fromUtf8("%/%'; "), location.id(), &values ); } else { d->db->execSql( QString::fromUtf8("SELECT relativePath FROM Albums WHERE albumRoot=? AND relativePath LIKE '") + subURL + QString::fromUtf8("%'; "), location.id(), &values ); } QStringList subalbums; QString albumRootPath = location.albumRootPath(); for (QList::iterator it = values.begin(); it != values.end(); ++it) subalbums << albumRootPath + it->toString(); return subalbums; } */ /* int CoreDB::addAlbum(const QString& albumRoot, const QString& relativePath, const QString& caption, const QDate& date, const QString& collection) { CollectionLocation location = CollectionManager::instance()->locationForAlbumRootPath(albumRoot); if (location.isNull()) return -1; return addAlbum(location.id(), relativePath, caption, date, collection); } */ int CoreDB::addAlbum(int albumRootId, const QString& relativePath, const QString& caption, const QDate& date, const QString& collection) { QVariant id; QList boundValues; boundValues << albumRootId << relativePath << date << caption << collection; d->db->execSql(QString::fromUtf8("REPLACE INTO Albums (albumRoot, relativePath, date, caption, collection) " "VALUES(?, ?, ?, ?, ?);"), boundValues, 0, &id); d->db->recordChangeset(AlbumChangeset(id.toInt(), AlbumChangeset::Added)); return id.toInt(); } void CoreDB::setAlbumCaption(int albumID, const QString& caption) { d->db->execSql(QString::fromUtf8("UPDATE Albums SET caption=? WHERE id=?;"), caption, albumID); d->db->recordChangeset(AlbumChangeset(albumID, AlbumChangeset::PropertiesChanged)); } void CoreDB::setAlbumCategory(int albumID, const QString& category) { // TODO : change "collection" propertie in DB ALbum table to "category" d->db->execSql(QString::fromUtf8("UPDATE Albums SET collection=? WHERE id=?;"), category, albumID); d->db->recordChangeset(AlbumChangeset(albumID, AlbumChangeset::PropertiesChanged)); } void CoreDB::setAlbumDate(int albumID, const QDate& date) { d->db->execSql(QString::fromUtf8("UPDATE Albums SET date=? WHERE id=?;"), date, albumID); d->db->recordChangeset(AlbumChangeset(albumID, AlbumChangeset::PropertiesChanged)); } void CoreDB::setAlbumIcon(int albumID, qlonglong iconID) { if (iconID == 0) { d->db->execSql(QString::fromUtf8("UPDATE Albums SET icon=NULL WHERE id=?;"), albumID); } else { d->db->execSql(QString::fromUtf8("UPDATE Albums SET icon=? WHERE id=?;"), iconID, albumID); } d->db->recordChangeset(AlbumChangeset(albumID, AlbumChangeset::PropertiesChanged)); } void CoreDB::deleteAlbum(int albumID) { QMap parameters; parameters.insert(QLatin1String(":albumId"), albumID); if (BdEngineBackend::NoErrors != d->db->execDBAction(d->db->getDBAction(QLatin1String("deleteAlbumID")), parameters)) { return; } d->db->recordChangeset(AlbumChangeset(albumID, AlbumChangeset::Deleted)); } void CoreDB::makeStaleAlbum(int albumID) { // We need to work around the table constraint, no we want to delete older stale albums with // the same relativePath, and adjust relativePaths depending on albumRoot. QList values; // retrieve information d->db->execSql(QString::fromUtf8("SELECT Albums.albumRoot, Albums.relativePath from Albums WHERE id=?;"), albumID, &values); if (values.isEmpty()) { return; } // prepend albumRootId to relativePath. relativePath is unused and officially undefined after this call. QString newRelativePath = values.at(0).toString() + QLatin1Char('-') + values.at(1).toString(); // delete older stale albums QMap parameters; parameters.insert(QLatin1String(":albumRoot"), 0); parameters.insert(QLatin1String(":relativePath"), newRelativePath); if (BdEngineBackend::NoErrors != d->db->execDBAction(d->db->getDBAction(QLatin1String("deleteAlbumRootPath")), parameters)) { return; } // now do our update d->db->setForeignKeyChecks(false); d->db->execSql(QString::fromUtf8("UPDATE Albums SET albumRoot=0, relativePath=? WHERE id=?;"), newRelativePath, albumID); // for now, we make no distinction to deleteAlbums wrt to changeset d->db->recordChangeset(AlbumChangeset(albumID, AlbumChangeset::Deleted)); d->db->setForeignKeyChecks(true); } void CoreDB::deleteStaleAlbums() { QMap parameters; parameters.insert(QLatin1String(":albumRoot"), 0); if (BdEngineBackend::NoErrors != d->db->execDBAction(d->db->getDBAction(QLatin1String("deleteAlbumRoot")), parameters)) { return; } // deliberately no changeset here, is done above } int CoreDB::addTag(int parentTagID, const QString& name, const QString& iconKDE, qlonglong iconID) { QVariant id; QMap parameters; parameters.insert(QLatin1String(":tagPID"), parentTagID); parameters.insert(QLatin1String(":tagname"), name); if (BdEngineBackend::NoErrors != d->db->execDBAction(d->db->getDBAction(QLatin1String("InsertTag")), parameters, 0 , &id)) { return -1; } if (!iconKDE.isEmpty()) { d->db->execSql(QString::fromUtf8("UPDATE Tags SET iconkde=? WHERE id=?;"), iconKDE, id.toInt()); } else if (iconID == 0) { d->db->execSql(QString::fromUtf8("UPDATE Tags SET icon=NULL WHERE id=?;"), id.toInt()); } else { d->db->execSql(QString::fromUtf8("UPDATE Tags SET icon=? WHERE id=?;"), iconID, id.toInt()); } d->db->recordChangeset(TagChangeset(id.toInt(), TagChangeset::Added)); return id.toInt(); } void CoreDB::deleteTag(int tagID) { /* QString("DELETE FROM Tags WHERE id=?;"), tagID */ QMap bindingMap; bindingMap.insert(QLatin1String(":tagID"), tagID); d->db->execDBAction(d->db->getDBAction(QLatin1String("DeleteTag")), bindingMap); d->db->recordChangeset(TagChangeset(tagID, TagChangeset::Deleted)); } void CoreDB::setTagIcon(int tagID, const QString& iconKDE, qlonglong iconID) { int _iconID = iconKDE.isEmpty() ? iconID : 0; QString _iconKDE = iconKDE; if (iconKDE.isEmpty() || iconKDE.toLower() == QLatin1String("tag")) { _iconKDE.clear(); } if (_iconID == 0) { d->db->execSql(QString::fromUtf8("UPDATE Tags SET iconkde=?, icon=NULL WHERE id=?;"), _iconKDE, tagID); } else { d->db->execSql(QString::fromUtf8("UPDATE Tags SET iconkde=?, icon=? WHERE id=?;"), _iconKDE, _iconID, tagID); } d->db->recordChangeset(TagChangeset(tagID, TagChangeset::IconChanged)); } void CoreDB::setTagParentID(int tagID, int newParentTagID) { if (d->db->databaseType() == BdEngineBackend::DbType::SQLite) { d->db->execSql(QString::fromUtf8("UPDATE OR REPLACE Tags SET pid=? WHERE id=?;"), newParentTagID, tagID); } else { d->db->execSql(QString::fromUtf8("UPDATE Tags SET pid=? WHERE id=?;"), newParentTagID, tagID); // NOTE: Update the Mysql TagsTree table which is used only in some search SQL queries (See lft/rgt tag ID properties). // In SQlite, it is nicely maintained by Triggers. // With MySQL, this did not work for some reason, and we patch a tree structure mimics in a different way. QMap bindingMap; bindingMap.insert(QLatin1String(":tagID"), tagID); bindingMap.insert(QLatin1String(":newTagPID"), newParentTagID); d->db->execDBAction(d->db->getDBAction(QLatin1String("MoveTagTree")), bindingMap); } d->db->recordChangeset(TagChangeset(tagID, TagChangeset::Reparented)); } QList CoreDB::getTagProperties(int tagId) { QList values; d->db->execSql(QString::fromUtf8("SELECT property, value FROM TagProperties WHERE tagid=?;"), tagId, &values); QList properties; if (values.isEmpty()) { return properties; } for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { TagProperty property; property.tagId = tagId; property.property = (*it).toString(); ++it; property.value = (*it).toString(); ++it; properties << property; } return properties; } QList CoreDB::getTagProperties(const QString& property) { QList values; d->db->execSql(QString::fromUtf8("SELECT tagid, property, value FROM TagProperties WHERE property=?;"), property, &values); QList properties; if (values.isEmpty()) { return properties; } for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { TagProperty property; property.tagId = (*it).toInt(); ++it; property.property = (*it).toString(); ++it; property.value = (*it).toString(); ++it; properties << property; } return properties; } QList CoreDB::getTagProperties() { QList values; d->db->execSql(QString::fromUtf8("SELECT tagid, property, value FROM TagProperties ORDER BY tagid, property;"), &values); QList properties; if (values.isEmpty()) { return properties; } for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { TagProperty property; property.tagId = (*it).toInt(); ++it; property.property = (*it).toString(); ++it; property.value = (*it).toString(); ++it; properties << property; } return properties; } QList< int > CoreDB::getTagsWithProperty(const QString& property) { QList values; d->db->execSql(QString::fromUtf8("SELECT DISTINCT tagid FROM TagProperties WHERE property=?;"), property, &values); QList tagIds; foreach(const QVariant& var, values) { tagIds << var.toInt(); } return tagIds; } void CoreDB::addTagProperty(int tagId, const QString& property, const QString& value) { d->db->execSql(QString::fromUtf8("INSERT INTO TagProperties (tagid, property, value) VALUES(?, ?, ?);"), tagId, property, value); d->db->recordChangeset(TagChangeset(tagId, TagChangeset::PropertiesChanged)); } void CoreDB::addTagProperty(const TagProperty& property) { addTagProperty(property.tagId, property.property, property.value); } void CoreDB::removeTagProperties(int tagId, const QString& property, const QString& value) { if (property.isNull()) { d->db->execSql(QString::fromUtf8("DELETE FROM TagProperties WHERE tagid=?;"), tagId); } else if (value.isNull()) { d->db->execSql(QString::fromUtf8("DELETE FROM TagProperties WHERE tagid=? AND property=?;"), tagId, property); } else { d->db->execSql(QString::fromUtf8("DELETE FROM TagProperties WHERE tagid=? AND property=? AND value=?;"), tagId, property, value); } d->db->recordChangeset(TagChangeset(tagId, TagChangeset::PropertiesChanged)); } int CoreDB::addSearch(DatabaseSearch::Type type, const QString& name, const QString& query) { QVariant id; if (!d->db->execSql(QString::fromUtf8("INSERT INTO Searches (type, name, query) VALUES(?, ?, ?);"), type, name, query, 0, &id)) { return -1; } d->db->recordChangeset(SearchChangeset(id.toInt(), SearchChangeset::Added)); return id.toInt(); } void CoreDB::updateSearch(int searchID, DatabaseSearch::Type type, const QString& name, const QString& query) { d->db->execSql(QString::fromUtf8("UPDATE Searches SET type=?, name=?, query=? WHERE id=?;"), type, name, query, searchID); d->db->recordChangeset(SearchChangeset(searchID, SearchChangeset::Changed)); } void CoreDB::deleteSearch(int searchID) { d->db->execSql(QString::fromUtf8("DELETE FROM Searches WHERE id=?;"), searchID); d->db->recordChangeset(SearchChangeset(searchID, SearchChangeset::Deleted)); } void CoreDB::deleteSearches(DatabaseSearch::Type type) { d->db->execSql(QString::fromUtf8("DELETE FROM Searches WHERE type=?;"), type); d->db->recordChangeset(SearchChangeset(0, SearchChangeset::Deleted)); } QString CoreDB::getSearchQuery(int searchId) { QList values; d->db->execSql(QString::fromUtf8("SELECT query FROM Searches WHERE id=?;"), searchId, &values); if (values.isEmpty()) { return QString(); } else { return values.first().toString(); } } SearchInfo CoreDB::getSearchInfo(int searchId) { SearchInfo info; QList values; d->db->execSql(QString::fromUtf8("SELECT id, type, name, query FROM Searches WHERE id=?;"), searchId, &values); if (values.size() == 4) { QList::const_iterator it = values.constBegin(); info.id = (*it).toInt(); ++it; info.type = (DatabaseSearch::Type)(*it).toInt(); ++it; info.name = (*it).toString(); ++it; info.query = (*it).toString(); ++it; } return info; } void CoreDB::setSetting(const QString& keyword, const QString& value) { d->db->execSql(QString::fromUtf8("REPLACE INTO Settings VALUES (?,?);"), keyword, value); } QString CoreDB::getSetting(const QString& keyword) { QList values; d->db->execSql(QString::fromUtf8("SELECT value FROM Settings " "WHERE keyword=?;"), keyword, &values); if (values.isEmpty()) { return QString(); } else { return values.first().toString(); } } // helper method static QStringList joinMainAndUserFilterString(const QChar& sep, const QString& filter, const QString& userFilter) { QSet filterSet; QStringList userFilterList; QStringList sortedList; filterSet = filter.split(sep, QString::SkipEmptyParts).toSet(); userFilterList = userFilter.split(sep, QString::SkipEmptyParts); foreach(const QString& userFormat, userFilterList) { if (userFormat.startsWith(QLatin1Char('-'))) { filterSet.remove(userFormat.mid(1)); } else { filterSet << userFormat; } } sortedList = filterSet.toList(); sortedList.sort(); return sortedList; } void CoreDB::getFilterSettings(QStringList* imageFilter, QStringList* videoFilter, QStringList* audioFilter) { QString imageFormats, videoFormats, audioFormats, userImageFormats, userVideoFormats, userAudioFormats; if (imageFilter) { imageFormats = getSetting(QLatin1String("databaseImageFormats")); userImageFormats = getSetting(QLatin1String("databaseUserImageFormats")); *imageFilter = joinMainAndUserFilterString(QLatin1Char(';'), imageFormats, userImageFormats); } if (videoFilter) { videoFormats = getSetting(QLatin1String("databaseVideoFormats")); userVideoFormats = getSetting(QLatin1String("databaseUserVideoFormats")); *videoFilter = joinMainAndUserFilterString(QLatin1Char(';'), videoFormats, userVideoFormats); } if (audioFilter) { audioFormats = getSetting(QLatin1String("databaseAudioFormats")); userAudioFormats = getSetting(QLatin1String("databaseUserAudioFormats")); *audioFilter = joinMainAndUserFilterString(QLatin1Char(';'), audioFormats, userAudioFormats); } } void CoreDB::getUserFilterSettings(QString* imageFilterString, QString* videoFilterString, QString* audioFilterString) { if (imageFilterString) { *imageFilterString = getSetting(QLatin1String("databaseUserImageFormats")); } if (videoFilterString) { *videoFilterString = getSetting(QLatin1String("databaseUserVideoFormats")); } if (audioFilterString) { *audioFilterString = getSetting(QLatin1String("databaseUserAudioFormats")); } } void CoreDB::getUserIgnoreDirectoryFilterSettings(QString* ignoreDirectoryFilterString) { *ignoreDirectoryFilterString = getSetting(QLatin1String("databaseUserIgnoreDirectoryFormats")); } void CoreDB::getIgnoreDirectoryFilterSettings(QStringList* ignoreDirectoryFilter) { QString ignoreDirectoryFormats, userIgnoreDirectoryFormats; ignoreDirectoryFormats = getSetting(QLatin1String("databaseIgnoreDirectoryFormats")); userIgnoreDirectoryFormats = getSetting(QLatin1String("databaseUserIgnoreDirectoryFormats")); *ignoreDirectoryFilter = joinMainAndUserFilterString(QLatin1Char(';'), ignoreDirectoryFormats, userIgnoreDirectoryFormats); } void CoreDB::setFilterSettings(const QStringList& imageFilter, const QStringList& videoFilter, const QStringList& audioFilter) { setSetting(QLatin1String("databaseImageFormats"), imageFilter.join(QLatin1Char(';'))); setSetting(QLatin1String("databaseVideoFormats"), videoFilter.join(QLatin1Char(';'))); setSetting(QLatin1String("databaseAudioFormats"), audioFilter.join(QLatin1Char(';'))); } void CoreDB::setIgnoreDirectoryFilterSettings(const QStringList& ignoreDirectoryFilter) { setSetting(QLatin1String("databaseIgnoreDirectoryFormats"), ignoreDirectoryFilter.join(QLatin1Char(';'))); } void CoreDB::setUserFilterSettings(const QStringList& imageFilter, const QStringList& videoFilter, const QStringList& audioFilter) { setSetting(QLatin1String("databaseUserImageFormats"), imageFilter.join(QLatin1Char(';'))); setSetting(QLatin1String("databaseUserVideoFormats"), videoFilter.join(QLatin1Char(';'))); setSetting(QLatin1String("databaseUserAudioFormats"), audioFilter.join(QLatin1Char(';'))); } void CoreDB::setUserIgnoreDirectoryFilterSettings(const QStringList& ignoreDirectoryFilters) { qCDebug(DIGIKAM_DATABASE_LOG) << "CoreDB::setUserIgnoreDirectoryFilterSettings. " "ignoreDirectoryFilterString: " << ignoreDirectoryFilters.join(QLatin1Char(';')); setSetting(QLatin1String("databaseUserIgnoreDirectoryFormats"), ignoreDirectoryFilters.join(QLatin1Char(';'))); } QUuid CoreDB::databaseUuid() { QString uuidString = getSetting(QLatin1String("databaseUUID")); QUuid uuid = QUuid(uuidString); if (uuidString.isNull() || uuid.isNull()) { uuid = QUuid::createUuid(); setSetting(QLatin1String("databaseUUID"), uuid.toString()); } return uuid; } int CoreDB::getUniqueHashVersion() { if (d->uniqueHashVersion == -1) { QString v = getSetting(QLatin1String("uniqueHashVersion")); if (v.isEmpty()) { d->uniqueHashVersion = 1; } else { d->uniqueHashVersion = v.toInt(); } } return d->uniqueHashVersion; } bool CoreDB::isUniqueHashV2() { return (getUniqueHashVersion() == 2); } void CoreDB::setUniqueHashVersion(int version) { d->uniqueHashVersion = version; setSetting(QLatin1String("uniqueHashVersion"), QString::number(d->uniqueHashVersion)); } /* QString CoreDB::getItemCaption(qlonglong imageID) { QList values; d->db->execSql( QString::fromUtf8("SELECT caption FROM Images " "WHERE id=?;"), imageID, &values ); if (!values.isEmpty()) return values.first().toString(); else return QString(); } QString CoreDB::getItemCaption(int albumID, const QString& name) { QList values; d->db->execSql( QString::fromUtf8("SELECT caption FROM Images " "WHERE dirid=? AND name=?;"), albumID, name, &values ); if (!values.isEmpty()) return values.first().toString(); else return QString(); } QDateTime CoreDB::getItemDate(qlonglong imageID) { QList values; d->db->execSql( QString::fromUtf8("SELECT datetime FROM Images " "WHERE id=?;"), imageID, &values ); if (!values.isEmpty()) return QDateTime::fromString(values.first().toString(), Qt::ISODate); else return QDateTime(); } QDateTime CoreDB::getItemDate(int albumID, const QString& name) { QList values; d->db->execSql( QString::fromUtf8("SELECT datetime FROM Images " "WHERE dirid=? AND name=?;"), albumID, name, &values ); if (values.isEmpty()) return QDateTime::fromString(values.first().toString(), Qt::ISODate); else return QDateTime(); } */ qlonglong CoreDB::getImageId(int albumID, const QString& name) { QList values; d->db->execSql(QString::fromUtf8("SELECT id FROM Images " "WHERE album=? AND name=?;"), albumID, name, &values); if (values.isEmpty()) { return -1; } else { return values.first().toLongLong(); } } QList CoreDB::getImageIds(int albumID, const QString& name, DatabaseItem::Status status) { QList values; if (albumID == -1) { d->db->execSql(QString::fromUtf8("SELECT id FROM Images " "WHERE album IS NULL AND name=? AND status=?;"), name, status, &values); } else { d->db->execSql(QString::fromUtf8("SELECT id FROM Images " "WHERE album=? AND name=? AND status=?;"), albumID, name, status, &values); } QList items; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { items << it->toLongLong(); } return items; } QList CoreDB::getImageIds(DatabaseItem::Status status) { QList values; d->db->execSql(QString::fromUtf8("SELECT id FROM Images " "WHERE status=?;"), status, &values); QList imageIds; foreach(const QVariant& object, values) { imageIds << object.toLongLong(); } return imageIds; } QList CoreDB::getImageIds(DatabaseItem::Status status, DatabaseItem::Category category) { QList values; d->db->execSql(QString::fromUtf8("SELECT id FROM Images " "WHERE status=? AND category=?;"), status, category, &values); QList imageIds; foreach(const QVariant& object, values) { imageIds << object.toLongLong(); } return imageIds; } qlonglong CoreDB::getImageId(int albumID, const QString& name, DatabaseItem::Status status, DatabaseItem::Category category, const QDateTime& modificationDate, qlonglong fileSize, const QString& uniqueHash) { QList values; QVariantList boundValues; // Add the standard bindings boundValues << name << (int)status << (int)category << modificationDate << fileSize << uniqueHash; // If the album id is -1, no album is assigned. Get all images with NULL album if (albumID == -1) { d->db->execSql(QString::fromUtf8("SELECT id FROM Images " "WHERE name=? AND status=? " "AND category=? AND modificationDate=? " "AND fileSize=? AND uniqueHash=? " "AND album IS NULL;"), boundValues, &values); } else { boundValues << albumID; d->db->execSql(QString::fromUtf8("SELECT id FROM Images " "WHERE name=? AND status=? " "AND category=? AND modificationDate=? " "AND fileSize=? AND uniqueHash=? " "AND album=?;"), boundValues, &values); } if (values.isEmpty() || ( values.size() > 1 )) { return -1; } else { return values.first().toLongLong(); } } QStringList CoreDB::getItemTagNames(qlonglong imageID) { QList values; d->db->execSql(QString::fromUtf8("SELECT name FROM Tags " "WHERE id IN (SELECT tagid FROM ImageTags " "WHERE imageid=?) " "ORDER BY name;"), imageID, &values); QStringList names; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { names << it->toString(); } return names; } QList CoreDB::getItemTagIDs(qlonglong imageID) { QList values; d->db->execSql(QString::fromUtf8("SELECT tagid FROM ImageTags WHERE imageID=?;"), imageID, &values); QList ids; if (values.isEmpty()) { return ids; } for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { ids << it->toInt(); } return ids; } QVector > CoreDB::getItemsTagIDs(const QList imageIds) { if (imageIds.isEmpty()) { return QVector >(); } QVector > results(imageIds.size()); DbEngineSqlQuery query = d->db->prepareQuery(QString::fromUtf8("SELECT tagid FROM ImageTags WHERE imageID=?;")); QVariantList values; for (int i = 0 ; i < imageIds.size() ; i++) { d->db->execSql(query, imageIds[i], &values); QList& tagIds = results[i]; foreach(const QVariant& v, values) { tagIds << v.toInt(); } } return results; } QList CoreDB::getImageTagProperties(qlonglong imageId, int tagId) { QList values; if (tagId == -1) { d->db->execSql(QString::fromUtf8("SELECT tagid, property, value FROM ImageTagProperties " "WHERE imageid=?;"), imageId, &values); } else { d->db->execSql(QString::fromUtf8("SELECT tagid, property, value FROM ImageTagProperties " "WHERE imageid=? AND tagid=?;"), imageId, tagId, &values); } QList properties; if (values.isEmpty()) { return properties; } for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { ImageTagProperty property; property.imageId = imageId; property.tagId = (*it).toInt(); ++it; property.property = (*it).toString(); ++it; property.value = (*it).toString(); ++it; properties << property; } return properties; } QList CoreDB::getTagIdsWithProperties(qlonglong imageId) { QList values; d->db->execSql(QString::fromUtf8("SELECT DISTINCT tagid FROM ImageTagProperties WHERE imageid=?;"), imageId, &values); QList tagIds; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { tagIds << (*it).toInt(); } return tagIds; } void CoreDB::addImageTagProperty(qlonglong imageId, int tagId, const QString& property, const QString& value) { d->db->execSql(QString::fromUtf8("INSERT INTO ImageTagProperties (imageid, tagid, property, value) VALUES(?, ?, ?, ?);"), imageId, tagId, property, value); d->db->recordChangeset(ImageTagChangeset(imageId, tagId, ImageTagChangeset::PropertiesChanged)); } void CoreDB::addImageTagProperty(const ImageTagProperty& property) { addImageTagProperty(property.imageId, property.tagId, property.property, property.value); } void CoreDB::removeImageTagProperties(qlonglong imageId, int tagId, const QString& property, const QString& value) { if (tagId == -1) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageTagProperties WHERE imageid=?;"), imageId); } else if (property.isNull()) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageTagProperties WHERE imageid=? AND tagid=?;"), imageId, tagId); } else if (value.isNull()) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageTagProperties WHERE imageid=? AND tagid=? AND property=?;"), imageId, tagId, property); } else { d->db->execSql(QString::fromUtf8("DELETE FROM ImageTagProperties WHERE imageid=? AND tagid=? AND property=? AND value=?;"), imageId, tagId, property, value); } d->db->recordChangeset(ImageTagChangeset(imageId, tagId, ImageTagChangeset::PropertiesChanged)); } ItemShortInfo CoreDB::getItemShortInfo(qlonglong imageID) { QList values; d->db->execSql(QString::fromUtf8("SELECT Images.name, Albums.albumRoot, Albums.relativePath, Albums.id " "FROM Images " " LEFT JOIN Albums ON Albums.id=Images.album " "WHERE Images.id=?;"), imageID, &values); ItemShortInfo info; if (!values.isEmpty()) { info.id = imageID; info.itemName = values.at(0).toString(); info.albumRootID = values.at(1).toInt(); info.album = values.at(2).toString(); info.albumID = values.at(3).toInt(); } return info; } ItemShortInfo CoreDB::getItemShortInfo(int albumRootId, const QString& relativePath, const QString& name) { QList values; d->db->execSql(QString::fromUtf8("SELECT Images.id, Albums.id " " FROM Images INNER JOIN Albums " " ON Images.album=Albums.id " " WHERE name=? AND albumRoot=? AND relativePath=?;"), name, albumRootId, relativePath, &values); ItemShortInfo info; if (!values.isEmpty()) { info.id = values.at(0).toLongLong(); info.itemName = name; info.albumRootID = albumRootId; info.album = relativePath; info.albumID = values.at(1).toInt(); } return info; } bool CoreDB::hasTags(const QList& imageIDList) { QList ids; if (imageIDList.isEmpty()) { return false; } QList values; QList boundValues; QString sql = QString::fromUtf8("SELECT count(tagid) FROM ImageTags " "WHERE imageid=? "); boundValues << imageIDList.first(); QList::const_iterator it = imageIDList.constBegin(); ++it; for (; it != imageIDList.constEnd() ; ++it) { sql += QString::fromUtf8(" OR imageid=? "); boundValues << (*it); } sql += QString::fromUtf8(";"); d->db->execSql(sql, boundValues, &values); if (values.isEmpty() || values.first().toInt() == 0) { return false; } else { return true; } } QList CoreDB::getItemCommonTagIDs(const QList& imageIDList) { QList ids; if (imageIDList.isEmpty()) { return ids; } QList values; QList boundValues; QString sql = QString::fromUtf8("SELECT DISTINCT tagid FROM ImageTags " "WHERE imageid=? "); boundValues << imageIDList.first(); QList::const_iterator it = imageIDList.constBegin(); ++it; for (; it != imageIDList.constEnd() ; ++it) { sql += QString::fromUtf8(" OR imageid=? "); boundValues << (*it); } sql += QString::fromUtf8(";"); d->db->execSql(sql, boundValues, &values); if (values.isEmpty()) { return ids; } for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { ids << it->toInt(); } return ids; } QVariantList CoreDB::getImagesFields(qlonglong imageID, DatabaseFields::Images fields) { QVariantList values; if (fields != DatabaseFields::ImagesNone) { QString query(QString::fromUtf8("SELECT ")); QStringList fieldNames = imagesFieldList(fields); query += fieldNames.join(QString::fromUtf8(", ")); query += QString::fromUtf8(" FROM Images WHERE id=?;"); d->db->execSql(query, imageID, &values); // Convert date times to QDateTime, they come as QString if ((fields & DatabaseFields::ModificationDate) && !values.isEmpty()) { int index = fieldNames.indexOf(QLatin1String("modificationDate")); values[index] = values.at(index).toDateTime(); } } return values; } QVariantList CoreDB::getImageInformation(qlonglong imageID, DatabaseFields::ImageInformation fields) { QVariantList values; if (fields != DatabaseFields::ImageInformationNone) { QString query(QString::fromUtf8("SELECT ")); QStringList fieldNames = imageInformationFieldList(fields); query += fieldNames.join(QString::fromUtf8(", ")); query += QString::fromUtf8(" FROM ImageInformation WHERE imageid=?;"); d->db->execSql(query, imageID, &values); // Convert date times to QDateTime, they come as QString if ((fields & DatabaseFields::CreationDate) && !values.isEmpty()) { int index = fieldNames.indexOf(QLatin1String("creationDate")); values[index] = values.at(index).toDateTime(); } if ((fields & DatabaseFields::DigitizationDate) && !values.isEmpty()) { int index = fieldNames.indexOf(QLatin1String("digitizationDate")); values[index] = values.at(index).toDateTime(); } } return values; } QVariantList CoreDB::getImageMetadata(qlonglong imageID, DatabaseFields::ImageMetadata fields) { QVariantList values; if (fields != DatabaseFields::ImageMetadataNone) { QString query(QString::fromUtf8("SELECT ")); QStringList fieldNames = imageMetadataFieldList(fields); query += fieldNames.join(QString::fromUtf8(", ")); query += QString::fromUtf8(" FROM ImageMetadata WHERE imageid=?;"); d->db->execSql(query, imageID, &values); // For some reason, if REAL values may be required from variables stored as QString QVariants. Convert code will come here. } return values; } QVariantList CoreDB::getVideoMetadata(qlonglong imageID, DatabaseFields::VideoMetadata fields) { QVariantList values; if (fields != DatabaseFields::VideoMetadataNone) { QString query(QString::fromUtf8("SELECT ")); QStringList fieldNames = videoMetadataFieldList(fields); query += fieldNames.join(QString::fromUtf8(", ")); query += QString::fromUtf8(" FROM VideoMetadata WHERE imageid=?;"); d->db->execSql(query, imageID, &values); // For some reason REAL values may come as QString QVariants. Convert here. if (values.size() == fieldNames.size() && ((fields & DatabaseFields::Aperture) || (fields & DatabaseFields::FocalLength) || (fields & DatabaseFields::FocalLength35) || (fields & DatabaseFields::ExposureTime) || (fields & DatabaseFields::SubjectDistance)) ) { for (int i = 0 ; i < values.size() ; ++i) { if (values.at(i).type() == QVariant::String && (fieldNames.at(i) == QLatin1String("aperture") || fieldNames.at(i) == QLatin1String("focalLength") || fieldNames.at(i) == QLatin1String("focalLength35") || fieldNames.at(i) == QLatin1String("exposureTime") || fieldNames.at(i) == QLatin1String("subjectDistance")) ) { values[i] = values.at(i).toDouble(); } } } } return values; } QVariantList CoreDB::getImagePosition(qlonglong imageID, DatabaseFields::ImagePositions fields) { QVariantList values; if (fields != DatabaseFields::ImagePositionsNone) { QString query(QString::fromUtf8("SELECT ")); QStringList fieldNames = imagePositionsFieldList(fields); query += fieldNames.join(QString::fromUtf8(", ")); query += QString::fromUtf8(" FROM ImagePositions WHERE imageid=?;"); d->db->execSql(query, imageID, &values); // For some reason REAL values may come as QString QVariants. Convert here. if (values.size() == fieldNames.size() && ((fields & DatabaseFields::LatitudeNumber) || (fields & DatabaseFields::LongitudeNumber) || (fields & DatabaseFields::Altitude) || (fields & DatabaseFields::PositionOrientation) || (fields & DatabaseFields::PositionTilt) || (fields & DatabaseFields::PositionRoll) || (fields & DatabaseFields::PositionAccuracy)) ) { for (int i = 0 ; i < values.size() ; ++i) { if (values.at(i).type() == QVariant::String && (fieldNames.at(i) == QLatin1String("latitudeNumber") || fieldNames.at(i) == QLatin1String("longitudeNumber") || fieldNames.at(i) == QLatin1String("altitude") || fieldNames.at(i) == QLatin1String("orientation") || fieldNames.at(i) == QLatin1String("tilt") || fieldNames.at(i) == QLatin1String("roll") || fieldNames.at(i) == QLatin1String("accuracy")) ) { if (!values.at(i).isNull()) values[i] = values.at(i).toDouble(); } } } } return values; } QVariantList CoreDB::getImagePositions(QList imageIDs, DatabaseFields::ImagePositions fields) { QVariantList values; if (fields != DatabaseFields::ImagePositionsNone) { QString sql(QString::fromUtf8("SELECT ")); QStringList fieldNames = imagePositionsFieldList(fields); sql += fieldNames.join(QString::fromUtf8(", ")); sql += QString::fromUtf8(" FROM ImagePositions WHERE imageid=?;"); DbEngineSqlQuery query = d->db->prepareQuery(sql); foreach(const qlonglong& imageid, imageIDs) { QVariantList singleValueList; d->db->execSql(query, imageid, &singleValueList); values << singleValueList; } // For some reason REAL values may come as QString QVariants. Convert here. if (values.size() == fieldNames.size() && (fields & DatabaseFields::LatitudeNumber || fields & DatabaseFields::LongitudeNumber || fields & DatabaseFields::Altitude || fields & DatabaseFields::PositionOrientation || fields & DatabaseFields::PositionTilt || fields & DatabaseFields::PositionRoll || fields & DatabaseFields::PositionAccuracy) ) { for (int i = 0 ; i < values.size() ; ++i) { if (values.at(i).type() == QVariant::String && (fieldNames.at(i) == QLatin1String("latitudeNumber") || fieldNames.at(i) == QLatin1String("longitudeNumber") || fieldNames.at(i) == QLatin1String("altitude") || fieldNames.at(i) == QLatin1String("orientation") || fieldNames.at(i) == QLatin1String("tilt") || fieldNames.at(i) == QLatin1String("roll") || fieldNames.at(i) == QLatin1String("accuracy")) ) { if (!values.at(i).isNull()) values[i] = values.at(i).toDouble(); } } } } return values; } void CoreDB::addImageInformation(qlonglong imageID, const QVariantList& infos, DatabaseFields::ImageInformation fields) { if (fields == DatabaseFields::ImageInformationNone) { return; } QString query(QString::fromUtf8("REPLACE INTO ImageInformation ( imageid, ")); QStringList fieldNames = imageInformationFieldList(fields); Q_ASSERT(fieldNames.size() == infos.size()); query += fieldNames.join(QLatin1String(", ")); query += QString::fromUtf8(" ) VALUES ("); addBoundValuePlaceholders(query, infos.size() + 1); query += QString::fromUtf8(");"); QVariantList boundValues; boundValues << imageID; boundValues << infos; d->db->execSql(query, boundValues); d->db->recordChangeset(ImageChangeset(imageID, fields)); } void CoreDB::changeImageInformation(qlonglong imageId, const QVariantList& infos, DatabaseFields::ImageInformation fields) { if (fields == DatabaseFields::ImageInformationNone) { return; } QStringList fieldNames = imageInformationFieldList(fields); d->db->execUpsertDBAction(QLatin1String("changeImageInformation"), imageId, fieldNames, infos); d->db->recordChangeset(ImageChangeset(imageId, fields)); } void CoreDB::changeImages(qlonglong imageId, const QVariantList& infos, DatabaseFields::Images fields) { qCDebug(DIGIKAM_DATABASE_LOG) << "---lyj--- change images " << imageId; if (fields == DatabaseFields::ImagesNone) { return; } QStringList fieldNames = imagesFieldList(fields); d->db->execUpsertDBAction(QLatin1String("changeImages"), imageId, fieldNames, infos); d->db->recordChangeset(ImageChangeset(imageId, fields)); } void CoreDB::addImageMetadata(qlonglong imageID, const QVariantList& infos, DatabaseFields::ImageMetadata fields) { if (fields == DatabaseFields::ImageMetadataNone) { return; } QString query(QString::fromUtf8("REPLACE INTO ImageMetadata ( imageid, ")); QStringList fieldNames = imageMetadataFieldList(fields); Q_ASSERT(fieldNames.size() == infos.size()); query += fieldNames.join(QLatin1String(", ")); query += QString::fromUtf8(" ) VALUES ("); addBoundValuePlaceholders(query, infos.size() + 1); query += QString::fromUtf8(");"); QVariantList boundValues; boundValues << imageID << infos; d->db->execSql(query, boundValues); d->db->recordChangeset(ImageChangeset(imageID, fields)); } void CoreDB::changeImageMetadata(qlonglong imageId, const QVariantList& infos, DatabaseFields::ImageMetadata fields) { if (fields == DatabaseFields::ImageMetadataNone) { return; } QString query(QString::fromUtf8("UPDATE ImageMetadata SET ")); QStringList fieldNames = imageMetadataFieldList(fields); Q_ASSERT(fieldNames.size() == infos.size()); query += fieldNames.join(QString::fromUtf8("=?,")); query += QString::fromUtf8("=? WHERE imageid=?;"); QVariantList boundValues; boundValues << infos << imageId; d->db->execSql(query, boundValues); d->db->recordChangeset(ImageChangeset(imageId, fields)); } void CoreDB::addVideoMetadata(qlonglong imageID, const QVariantList& infos, DatabaseFields::VideoMetadata fields) { if (fields == DatabaseFields::VideoMetadataNone) { return; } QString query(QString::fromUtf8("REPLACE INTO VideoMetadata ( imageid, ")); // need to create this database QStringList fieldNames = videoMetadataFieldList(fields); Q_ASSERT(fieldNames.size() == infos.size()); query += fieldNames.join(QLatin1String(", ")); query += QString::fromUtf8(" ) VALUES ("); addBoundValuePlaceholders(query, infos.size() + 1); query += QString::fromUtf8(");"); QVariantList boundValues; boundValues << imageID << infos; d->db->execSql(query, boundValues); d->db->recordChangeset(ImageChangeset(imageID, fields)); } void CoreDB::changeVideoMetadata(qlonglong imageId, const QVariantList& infos, DatabaseFields::VideoMetadata fields) { if (fields == DatabaseFields::VideoMetadataNone) { return; } QString query(QString::fromUtf8("UPDATE VideoMetadata SET ")); QStringList fieldNames = videoMetadataFieldList(fields); Q_ASSERT(fieldNames.size() == infos.size()); query += fieldNames.join(QString::fromUtf8("=?,")); query += QString::fromUtf8("=? WHERE imageid=?;"); QVariantList boundValues; boundValues << infos << imageId; d->db->execSql(query, boundValues); d->db->recordChangeset(ImageChangeset(imageId, fields)); } void CoreDB::addImagePosition(qlonglong imageID, const QVariantList& infos, DatabaseFields::ImagePositions fields) { if (fields == DatabaseFields::ImagePositionsNone) { return; } QString query(QString::fromUtf8("REPLACE INTO ImagePositions ( imageid, ")); QStringList fieldNames = imagePositionsFieldList(fields); Q_ASSERT(fieldNames.size() == infos.size()); query += fieldNames.join(QLatin1String(", ")); query += QString::fromUtf8(" ) VALUES ("); addBoundValuePlaceholders(query, infos.size() + 1); query += QString::fromUtf8(");"); QVariantList boundValues; boundValues << imageID << infos; d->db->execSql(query, boundValues); d->db->recordChangeset(ImageChangeset(imageID, fields)); } void CoreDB::changeImagePosition(qlonglong imageId, const QVariantList& infos, DatabaseFields::ImagePositions fields) { if (fields == DatabaseFields::ImagePositionsNone) { return; } QString query(QString::fromUtf8("UPDATE ImagePositions SET ")); QStringList fieldNames = imagePositionsFieldList(fields); Q_ASSERT(fieldNames.size() == infos.size()); query += fieldNames.join(QString::fromUtf8("=?,")); query += QString::fromUtf8("=? WHERE imageid=?;"); QVariantList boundValues; boundValues << infos << imageId; d->db->execSql(query, boundValues); d->db->recordChangeset(ImageChangeset(imageId, fields)); } void CoreDB::removeImagePosition(qlonglong imageid) { d->db->execSql(QString(QString::fromUtf8("DELETE FROM ImagePositions WHERE imageid=?;")), imageid); d->db->recordChangeset(ImageChangeset(imageid, DatabaseFields::ImagePositionsAll)); } void CoreDB::removeImagePositionAltitude(qlonglong imageid) { d->db->execSql(QString(QString::fromUtf8("UPDATE ImagePositions SET altitude=NULL WHERE imageid=?;")), imageid); d->db->recordChangeset(ImageChangeset(imageid, DatabaseFields::Altitude)); } QList CoreDB::getImageComments(qlonglong imageID) { QList list; QList values; d->db->execSql(QString::fromUtf8("SELECT id, type, language, author, date, comment " "FROM ImageComments WHERE imageid=?;"), imageID, &values); for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { CommentInfo info; info.imageId = imageID; info.id = (*it).toInt(); ++it; info.type = (DatabaseComment::Type)(*it).toInt(); ++it; info.language = (*it).toString(); ++it; info.author = (*it).toString(); ++it; info.date = (*it).toDateTime(); ++it; info.comment = (*it).toString(); ++it; list << info; } return list; } int CoreDB::setImageComment(qlonglong imageID, const QString& comment, DatabaseComment::Type type, const QString& language, const QString& author, const QDateTime& date) { QVariantList boundValues; boundValues << imageID << (int)type << language << author << date << comment; QVariant id; d->db->execSql(QString::fromUtf8("REPLACE INTO ImageComments " "( imageid, type, language, author, date, comment ) " " VALUES (?,?,?,?,?,?);"), boundValues, 0, &id); d->db->recordChangeset(ImageChangeset(imageID, DatabaseFields::ImageCommentsAll)); return id.toInt(); } void CoreDB::changeImageComment(int commentId, qlonglong imageID, const QVariantList& infos, DatabaseFields::ImageComments fields) { if (fields == DatabaseFields::ImageCommentsNone) { return; } QString query(QString::fromUtf8("UPDATE ImageComments SET ")); QStringList fieldNames = imageCommentsFieldList(fields); Q_ASSERT(fieldNames.size() == infos.size()); query += fieldNames.join(QString::fromUtf8("=?,")); query += QString::fromUtf8("=? WHERE id=?;"); QVariantList boundValues; boundValues << infos << commentId; d->db->execSql(query, boundValues); d->db->recordChangeset(ImageChangeset(imageID, fields)); } void CoreDB::removeImageComment(int commentid, qlonglong imageid) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageComments WHERE id=?;"), commentid); d->db->recordChangeset(ImageChangeset(imageid, DatabaseFields::ImageCommentsAll)); } QString CoreDB::getImageProperty(qlonglong imageID, const QString& property) { QList values; d->db->execSql(QString::fromUtf8("SELECT value FROM ImageProperties " "WHERE imageid=? and property=?;"), imageID, property, &values); if (!values.isEmpty()) { return values.first().toString(); } else { return QString(); } } void CoreDB::setImageProperty(qlonglong imageID, const QString& property, const QString& value) { d->db->execSql(QString::fromUtf8("REPLACE INTO ImageProperties " "(imageid, property, value) " "VALUES(?, ?, ?);"), imageID, property, value); } void CoreDB::removeImageProperty(qlonglong imageID, const QString& property) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageProperties WHERE imageid=? AND property=?;"), imageID, property); } void CoreDB::removeImagePropertyByName(const QString& property) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageProperties WHERE property=?;"), property); } QList CoreDB::getImageCopyright(qlonglong imageID, const QString& property) { QList list; QList values; if (property.isNull()) { d->db->execSql(QString::fromUtf8("SELECT property, value, extraValue FROM ImageCopyright " "WHERE imageid=?;"), imageID, &values); } else { d->db->execSql(QString::fromUtf8("SELECT property, value, extraValue FROM ImageCopyright " "WHERE imageid=? and property=?;"), imageID, property, &values); } for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { CopyrightInfo info; info.id = imageID; info.property = (*it).toString(); ++it; info.value = (*it).toString(); ++it; info.extraValue = (*it).toString(); ++it; list << info; } return list; } void CoreDB::setImageCopyrightProperty(qlonglong imageID, const QString& property, const QString& value, const QString& extraValue, CopyrightPropertyUnique uniqueness) { if (uniqueness == PropertyUnique) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageCopyright " "WHERE imageid=? AND property=?;"), imageID, property); } else if (uniqueness == PropertyExtraValueUnique) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageCopyright " "WHERE imageid=? AND property=? AND extraValue=?;"), imageID, property, extraValue); } d->db->execSql(QString::fromUtf8("REPLACE INTO ImageCopyright " "(imageid, property, value, extraValue) " "VALUES(?, ?, ?, ?);"), imageID, property, value, extraValue); } void CoreDB::removeImageCopyrightProperties(qlonglong imageID, const QString& property, const QString& extraValue, const QString& value) { int removeBy = 0; if (!property.isNull()) { ++removeBy; } if (!extraValue.isNull()) { ++removeBy; } if (!value.isNull()) { ++removeBy; } switch (removeBy) { case 0: d->db->execSql(QString::fromUtf8("DELETE FROM ImageCopyright " "WHERE imageid=?;"), imageID); break; case 1: d->db->execSql(QString::fromUtf8("DELETE FROM ImageCopyright " "WHERE imageid=? AND property=?;"), imageID, property); break; case 2: d->db->execSql(QString::fromUtf8("DELETE FROM ImageCopyright " "WHERE imageid=? AND property=? AND extraValue=?;"), imageID, property, extraValue); break; case 3: d->db->execSql(QString::fromUtf8("DELETE FROM ImageCopyright " "WHERE imageid=? AND property=? AND extraValue=? AND value=?;"), imageID, property, extraValue, value); break; } } QList CoreDB::findByNameAndCreationDate(const QString& fileName, const QDateTime& creationDate) { QList values; d->db->execSql(QString::fromUtf8("SELECT id FROM Images " " LEFT JOIN ImageInformation ON id=imageid " "WHERE name=? AND creationDate=? AND status!=3;"), fileName, creationDate, &values); QList ids; foreach(const QVariant& var, values) { ids << var.toLongLong(); } return ids; } bool CoreDB::hasImageHistory(qlonglong imageId) { QList values; d->db->execSql(QString::fromUtf8("SELECT history FROM ImageHistory WHERE imageid=?;"), imageId, &values); return !values.isEmpty(); } ImageHistoryEntry CoreDB::getImageHistory(qlonglong imageId) { QList values; d->db->execSql(QString::fromUtf8("SELECT uuid, history FROM ImageHistory WHERE imageid=?;"), imageId, &values); ImageHistoryEntry entry; entry.imageId = imageId; if (values.count() != 2) { return entry; } QList::const_iterator it = values.constBegin(); entry.uuid = (*it).toString(); ++it; entry.history = (*it).toString(); ++it; return entry; } QList CoreDB::getItemsForUuid(const QString& uuid) { QList values; d->db->execSql(QString::fromUtf8("SELECT imageid FROM ImageHistory " "INNER JOIN Images ON imageid=id " "WHERE uuid=? AND status!=3;"), uuid, &values); QList imageIds; if (values.isEmpty()) { return imageIds; } for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { imageIds << (*it).toInt(); } return imageIds; } QString CoreDB::getImageUuid(qlonglong imageId) { QList values; d->db->execSql(QString::fromUtf8("SELECT uuid FROM ImageHistory WHERE imageid=?;"), imageId, &values); if (values.isEmpty()) { return QString(); } QString uuid = values.first().toString(); if (uuid.isEmpty()) { return QString(); } return uuid; } void CoreDB::setImageHistory(qlonglong imageId, const QString& history) { d->db->execUpsertDBAction(QLatin1String("changeImageHistory"), imageId, QStringList() << QLatin1String("history"), QVariantList() << history); d->db->recordChangeset(ImageChangeset(imageId, DatabaseFields::ImageHistory)); } void CoreDB::setImageUuid(qlonglong imageId, const QString& uuid) { d->db->execUpsertDBAction(QLatin1String("changeImageHistory"), imageId, QStringList() << QLatin1String("uuid"), QVariantList() << uuid); d->db->recordChangeset(ImageChangeset(imageId, DatabaseFields::ImageUUID)); } void CoreDB::addImageRelation(qlonglong subjectId, qlonglong objectId, DatabaseRelation::Type type) { d->db->execSql(QString::fromUtf8("REPLACE INTO ImageRelations (subject, object, type) VALUES (?, ?, ?);"), subjectId, objectId, type); d->db->recordChangeset(ImageChangeset(QList() << subjectId << objectId, DatabaseFields::ImageRelations)); } void CoreDB::addImageRelations(const QList& subjectIds, const QList& objectIds, DatabaseRelation::Type type) { DbEngineSqlQuery query = d->db->prepareQuery(QString::fromUtf8("REPLACE INTO ImageRelations (subject, object, type) VALUES (?, ?, ?);")); QVariantList subjects, objects, types; for (int i = 0 ; i < subjectIds.size() ; ++i) { subjects << subjectIds.at(i); objects << objectIds.at(i); types << type; } query.addBindValue(subjects); query.addBindValue(objects); query.addBindValue(types); d->db->execBatch(query); d->db->recordChangeset(ImageChangeset(subjectIds + objectIds, DatabaseFields::ImageRelations)); } void CoreDB::addImageRelation(const ImageRelation& relation) { addImageRelation(relation.subjectId, relation.objectId, relation.type); } void CoreDB::removeImageRelation(qlonglong subjectId, qlonglong objectId, DatabaseRelation::Type type) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageRelations WHERE subject=? AND object=? AND type=?;"), subjectId, objectId, type); d->db->recordChangeset(ImageChangeset(QList() << subjectId << objectId, DatabaseFields::ImageRelations)); } void CoreDB::removeImageRelation(const ImageRelation& relation) { removeImageRelation(relation.subjectId, relation.objectId, relation.type); } QList CoreDB::removeAllImageRelationsTo(qlonglong objectId, DatabaseRelation::Type type) { QList affected = getImagesRelatingTo(objectId, type); if (affected.isEmpty()) { return affected; } d->db->execSql(QString::fromUtf8("DELETE FROM ImageRelations WHERE object=? AND type=?;"), objectId, type); d->db->recordChangeset(ImageChangeset(QList() << affected << objectId, DatabaseFields::ImageRelations)); return affected; } QList CoreDB::removeAllImageRelationsFrom(qlonglong subjectId, DatabaseRelation::Type type) { QList affected = getImagesRelatedFrom(subjectId, type); if (affected.isEmpty()) { return affected; } d->db->execSql(QString::fromUtf8("DELETE FROM ImageRelations WHERE subject=? AND type=?;"), subjectId, type); d->db->recordChangeset(ImageChangeset(QList() << affected << subjectId, DatabaseFields::ImageRelations)); return affected; } QList CoreDB::getImagesRelatedFrom(qlonglong subjectId, DatabaseRelation::Type type) { return getRelatedImages(subjectId, true, type, false); } QVector > CoreDB::getImagesRelatedFrom(QList subjectIds, DatabaseRelation::Type type) { return getRelatedImages(subjectIds, true, type, false); } bool CoreDB::hasImagesRelatedFrom(qlonglong subjectId, DatabaseRelation::Type type) { // returns 0 or 1 item in list return !getRelatedImages(subjectId, true, type, true).isEmpty(); } QList CoreDB::getImagesRelatingTo(qlonglong objectId, DatabaseRelation::Type type) { return getRelatedImages(objectId, false, type, false); } QVector > CoreDB::getImagesRelatingTo(QList objectIds, DatabaseRelation::Type type) { return getRelatedImages(objectIds, false, type, false); } bool CoreDB::hasImagesRelatingTo(qlonglong objectId, DatabaseRelation::Type type) { // returns 0 or 1 item in list return !getRelatedImages(objectId, false, type, true).isEmpty(); } QList CoreDB::getRelatedImages(qlonglong id, bool fromOrTo, DatabaseRelation::Type type, bool boolean) { QString sql = d->constructRelatedImagesSQL(fromOrTo, type, boolean); DbEngineSqlQuery query = d->db->prepareQuery(sql); return d->execRelatedImagesQuery(query, id, type); } QVector > CoreDB::getRelatedImages(QList ids, bool fromOrTo, DatabaseRelation::Type type, bool boolean) { if (ids.isEmpty()) { return QVector >(); } QVector > result(ids.size()); QString sql = d->constructRelatedImagesSQL(fromOrTo, type, boolean); DbEngineSqlQuery query = d->db->prepareQuery(sql); for (int i = 0 ; i < ids.size() ; i++) { result[i] = d->execRelatedImagesQuery(query, ids[i], type); } return result; } QList > CoreDB::getRelationCloud(qlonglong imageId, DatabaseRelation::Type type) { QSet todo, done; QSet > pairs; todo << imageId; QString sql = QString::fromUtf8( "SELECT subject, object FROM ImageRelations " "INNER JOIN Images AS SubjectImages ON ImageRelations.subject=SubjectImages.id " "INNER JOIN Images AS ObjectImages ON ImageRelations.object=ObjectImages.id " "WHERE (subject=? OR object=?) %1 AND SubjectImages.status!=3 AND ObjectImages.status!=3;"); if (type == DatabaseRelation::UndefinedType) { sql = sql.arg(QString()); } else { sql = sql.arg(QString::fromUtf8("AND type=?")); } DbEngineSqlQuery query = d->db->prepareQuery(sql); QList values; qlonglong subject, object; while (!todo.isEmpty()) { qlonglong id = *todo.begin(); todo.erase(todo.begin()); done << id; if (type == DatabaseRelation::UndefinedType) { d->db->execSql(query, id, id, &values); } else { d->db->execSql(query, id, id, type, &values); } for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { subject = (*it).toLongLong(); ++it; object = (*it).toLongLong(); ++it; pairs << qMakePair(subject, object); if (!done.contains(subject)) { todo << subject; } if (!done.contains(object)) { todo << object; } } } return pairs.toList(); } QList CoreDB::getOneRelatedImageEach(const QList& ids, DatabaseRelation::Type type) { QString sql = QString::fromUtf8( "SELECT subject, object FROM ImageRelations " "INNER JOIN Images AS SubjectImages ON ImageRelations.subject=SubjectImages.id " "INNER JOIN Images AS ObjectImages ON ImageRelations.object=ObjectImages.id " "WHERE ( (subject=? AND ObjectImages.status!=3) " " OR (object=? AND SubjectImages.status!=3) ) " " %1 LIMIT 1;"); if (type == DatabaseRelation::UndefinedType) { sql = sql.arg(QString()); } else { sql = sql.arg(QString::fromUtf8("AND type=?")); } DbEngineSqlQuery query = d->db->prepareQuery(sql); QSet result; QList values; foreach(const qlonglong& id, ids) { if (type == DatabaseRelation::UndefinedType) { d->db->execSql(query, id, id, &values); } else { d->db->execSql(query, id, id, type, &values); } if (values.size() != 2) { continue; } // one of subject and object is the given id, the other our result if (values.first() != id) { result << values.first().toLongLong(); } else { result << values.last().toLongLong(); } } return result.toList(); } QStringList CoreDB::getItemsURLsWithTag(int tagId) { QList values; d->db->execSql(QString::fromUtf8("SELECT Albums.albumRoot, Albums.relativePath, Images.name FROM Images " "LEFT JOIN ImageTags ON Images.id=ImageTags.imageid " "LEFT JOIN Albums ON Albums.id=Images.album " " WHERE Images.status=1 AND Images.category=1 AND ImageTags.tagid=?;"), tagId, &values); QStringList urls; QString albumRootPath, relativePath, name; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { albumRootPath = CollectionManager::instance()->albumRootPath((*it).toInt()); ++it; relativePath = (*it).toString(); ++it; name = (*it).toString(); ++it; if (relativePath == QLatin1String("/")) { urls << albumRootPath + relativePath + name; } else { urls << albumRootPath + relativePath + QLatin1Char('/') + name; } } return urls; } QStringList CoreDB::getDirtyOrMissingFaceImageUrls() { QList values; d->db->execSql(QString::fromUtf8("SELECT Albums.albumRoot, Albums.relativePath, Images.name FROM Images " "LEFT JOIN ImageScannedMatrix ON Images.id=ImageScannedMatrix.imageid " "LEFT JOIN Albums ON Albums.id=Images.album " " WHERE Images.status=1 AND Images.category=1 AND " " ( ImageScannedMatrix.imageid IS NULL " " OR Images.modificationDate != ImageScannedMatrix.modificationDate " " OR Images.uniqueHash != ImageScannedMatrix.uniqueHash );"), &values); QStringList urls; QString albumRootPath, relativePath, name; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { albumRootPath = CollectionManager::instance()->albumRootPath((*it).toInt()); ++it; relativePath = (*it).toString(); ++it; name = (*it).toString(); ++it; if (relativePath == QLatin1String("/")) { urls << albumRootPath + relativePath + name; } else { urls << albumRootPath + relativePath + QLatin1Char('/') + name; } } return urls; } QList CoreDB::getIdenticalFiles(qlonglong id) { if (!id) { return QList(); } QList values; // retrieve unique hash and file size d->db->execSql(QString::fromUtf8("SELECT uniqueHash, fileSize FROM Images WHERE id=?;"), id, &values); if (values.isEmpty()) { return QList(); } QString uniqueHash = values.at(0).toString(); qlonglong fileSize = values.at(1).toLongLong(); return getIdenticalFiles(uniqueHash, fileSize, id); } QList CoreDB::getIdenticalFiles(const QString& uniqueHash, qlonglong fileSize, qlonglong sourceId) { // enforce validity if (uniqueHash.isEmpty() || fileSize <= 0) { return QList(); } QList values; // find items with same fingerprint d->db->execSql(QString::fromUtf8("SELECT id, album, name, status, category, modificationDate, fileSize FROM Images " " WHERE fileSize=? AND uniqueHash=? AND album IS NOT NULL;"), fileSize, uniqueHash, &values); QList list; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { ItemScanInfo info; info.id = (*it).toLongLong(); ++it; info.albumID = (*it).toInt(); ++it; info.itemName = (*it).toString(); ++it; info.status = (DatabaseItem::Status)(*it).toInt(); ++it; info.category = (DatabaseItem::Category)(*it).toInt(); ++it; info.modificationDate = (*it).toDateTime(); ++it; info.fileSize = (*it).toLongLong(); ++it; // exclude one source id from list if (sourceId == info.id) { continue; } // same for all here, per definition info.uniqueHash = uniqueHash; list << info; } return list; } QStringList CoreDB::imagesFieldList(DatabaseFields::Images fields) { // adds no spaces at beginning or end QStringList list; if (fields & DatabaseFields::Album) { list << QLatin1String("album"); } if (fields & DatabaseFields::Name) { list << QLatin1String("name"); } if (fields & DatabaseFields::Status) { list << QLatin1String("status"); } if (fields & DatabaseFields::Category) { list << QLatin1String("category"); } if (fields & DatabaseFields::ModificationDate) { list << QLatin1String("modificationDate"); } if (fields & DatabaseFields::FileSize) { list << QLatin1String("fileSize"); } if (fields & DatabaseFields::UniqueHash) { list << QLatin1String("uniqueHash"); } if (fields & DatabaseFields::ManualOrder) { list << QLatin1String("manualOrder"); } return list; } QStringList CoreDB::imageInformationFieldList(DatabaseFields::ImageInformation fields) { // adds no spaces at beginning or end QStringList list; if (fields & DatabaseFields::Rating) { list << QLatin1String("rating"); } if (fields & DatabaseFields::CreationDate) { list << QLatin1String("creationDate"); } if (fields & DatabaseFields::DigitizationDate) { list << QLatin1String("digitizationDate"); } if (fields & DatabaseFields::Orientation) { list << QLatin1String("orientation"); } if (fields & DatabaseFields::Width) { list << QLatin1String("width"); } if (fields & DatabaseFields::Height) { list << QLatin1String("height"); } if (fields & DatabaseFields::Format) { list << QLatin1String("format"); } if (fields & DatabaseFields::ColorDepth) { list << QLatin1String("colorDepth"); } if (fields & DatabaseFields::ColorModel) { list << QLatin1String("colorModel"); } return list; } QStringList CoreDB::videoMetadataFieldList(DatabaseFields::VideoMetadata fields) { // adds no spaces at beginning or end QStringList list; if (fields & DatabaseFields::AspectRatio) { list << QLatin1String("aspectRatio"); } if (fields & DatabaseFields::AudioBitRate) { list << QLatin1String("audioBitRate"); } if (fields & DatabaseFields::AudioChannelType) { list << QLatin1String("audioChannelType"); } if (fields & DatabaseFields::AudioCodec) { list << QLatin1String("audioCompressor"); } if (fields & DatabaseFields::Duration) { list << QLatin1String("duration"); } if (fields & DatabaseFields::FrameRate) { list << QLatin1String("frameRate"); } if (fields & DatabaseFields::VideoCodec) { list << QLatin1String("videoCodec"); } return list; } QStringList CoreDB::imageMetadataFieldList(DatabaseFields::ImageMetadata fields) { // adds no spaces at beginning or end QStringList list; if (fields & DatabaseFields::Make) { list << QLatin1String("make"); } if (fields & DatabaseFields::Model) { list << QLatin1String("model"); } if (fields & DatabaseFields::Lens) { list << QLatin1String("lens"); } if (fields & DatabaseFields::Aperture) { list << QLatin1String("aperture"); } if (fields & DatabaseFields::FocalLength) { list << QLatin1String("focalLength"); } if (fields & DatabaseFields::FocalLength35) { list << QLatin1String("focalLength35"); } if (fields & DatabaseFields::ExposureTime) { list << QLatin1String("exposureTime"); } if (fields & DatabaseFields::ExposureProgram) { list << QLatin1String("exposureProgram"); } if (fields & DatabaseFields::ExposureMode) { list << QLatin1String("exposureMode"); } if (fields & DatabaseFields::Sensitivity) { list << QLatin1String("sensitivity"); } if (fields & DatabaseFields::FlashMode) { list << QLatin1String("flash"); } if (fields & DatabaseFields::WhiteBalance) { list << QLatin1String("whiteBalance"); } if (fields & DatabaseFields::WhiteBalanceColorTemperature) { list << QLatin1String("whiteBalanceColorTemperature"); } if (fields & DatabaseFields::MeteringMode) { list << QLatin1String("meteringMode"); } if (fields & DatabaseFields::SubjectDistance) { list << QLatin1String("subjectDistance"); } if (fields & DatabaseFields::SubjectDistanceCategory) { list << QLatin1String("subjectDistanceCategory"); } return list; } QStringList CoreDB::imagePositionsFieldList(DatabaseFields::ImagePositions fields) { // adds no spaces at beginning or end QStringList list; if (fields & DatabaseFields::Latitude) { list << QLatin1String("latitude"); } if (fields & DatabaseFields::LatitudeNumber) { list << QLatin1String("latitudeNumber"); } if (fields & DatabaseFields::Longitude) { list << QLatin1String("longitude"); } if (fields & DatabaseFields::LongitudeNumber) { list << QLatin1String("longitudeNumber"); } if (fields & DatabaseFields::Altitude) { list << QLatin1String("altitude"); } if (fields & DatabaseFields::PositionOrientation) { list << QLatin1String("orientation"); } if (fields & DatabaseFields::PositionTilt) { list << QLatin1String("tilt"); } if (fields & DatabaseFields::PositionRoll) { list << QLatin1String("roll"); } if (fields & DatabaseFields::PositionAccuracy) { list << QLatin1String("accuracy"); } if (fields & DatabaseFields::PositionDescription) { list << QLatin1String("description"); } return list; } QStringList CoreDB::imageCommentsFieldList(DatabaseFields::ImageComments fields) { // adds no spaces at beginning or end QStringList list; if (fields & DatabaseFields::CommentType) { list << QLatin1String("type"); } if (fields & DatabaseFields::CommentLanguage) { list << QLatin1String("language"); } if (fields & DatabaseFields::CommentAuthor) { list << QLatin1String("author"); } if (fields & DatabaseFields::CommentDate) { list << QLatin1String("date"); } if (fields & DatabaseFields::Comment) { list << QLatin1String("comment"); } return list; } void CoreDB::addBoundValuePlaceholders(QString& query, int count) { // adds no spaces at beginning or end QString questionMarks; questionMarks.reserve(count * 2); QString questionMark(QString::fromUtf8("?,")); for (int i = 0 ; i < count ; ++i) { questionMarks += questionMark; } // remove last ',' questionMarks.chop(1); query += questionMarks; } int CoreDB::findInDownloadHistory(const QString& identifier, const QString& name, qlonglong fileSize, const QDateTime& date) { QList values; d->db->execSql(QString::fromUtf8("SELECT id FROM DownloadHistory WHERE " "identifier=? AND filename=? AND filesize=? AND filedate=?;"), identifier, name, fileSize, date, &values); if (values.isEmpty()) { return -1; } return values.first().toInt(); } int CoreDB::addToDownloadHistory(const QString& identifier, const QString& name, qlonglong fileSize, const QDateTime& date) { QVariant id; d->db->execSql(QString::fromUtf8("REPLACE INTO DownloadHistory " "(identifier, filename, filesize, filedate) " "VALUES (?,?,?,?);"), identifier, name, fileSize, date, 0, &id); return id.toInt(); } /* void CoreDB::setItemCaption(qlonglong imageID,const QString& caption) { QList values; d->db->execSql( QString::fromUtf8("UPDATE Images SET caption=? " "WHERE id=?;"), caption, imageID ); CoreDbAccess::attributesWatch() ->sendImageFieldChanged(imageID, DatabaseAttributesWatch::ImageComment); } void CoreDB::setItemCaption(int albumID, const QString& name, const QString& caption) { / * QList values; d->db->execSql( QString::fromUtf8("UPDATE Images SET caption=? " "WHERE dirid=? AND name=?;") .(caption, QString::number(albumID), (name)) ); * / // easier because of attributes watch return setItemCaption(getImageId(albumID, name), caption); } */ void CoreDB::addItemTag(qlonglong imageID, int tagID) { d->db->execSql(QString::fromUtf8("REPLACE INTO ImageTags (imageid, tagid) " "VALUES(?, ?);"), imageID, tagID); d->db->recordChangeset(ImageTagChangeset(imageID, tagID, ImageTagChangeset::Added)); //don't save pick or color tags if (TagsCache::instance()->isInternalTag(tagID)) return; //move current tag to front d->recentlyAssignedTags.removeAll(tagID); d->recentlyAssignedTags.prepend(tagID); if (d->recentlyAssignedTags.size() > 10) { d->recentlyAssignedTags.removeLast(); } } void CoreDB::addItemTag(int albumID, const QString& name, int tagID) { /* d->db->execSql( QString::fromUtf8("REPLACE INTO ImageTags (imageid, tagid) \n " "(SELECT id, ? FROM Images \n " " WHERE dirid=? AND name=?);") .tagID .albumID .(name) ); */ // easier because of attributes watch return addItemTag(getImageId(albumID, name), tagID); } void CoreDB::addTagsToItems(QList imageIDs, QList tagIDs) { if (imageIDs.isEmpty() || tagIDs.isEmpty()) { return; } DbEngineSqlQuery query = d->db->prepareQuery(QString::fromUtf8("REPLACE INTO ImageTags (imageid, tagid) VALUES(?, ?);")); QVariantList images; QVariantList tags; foreach(const qlonglong& imageid, imageIDs) { foreach(int tagid, tagIDs) { images << imageid; tags << tagid; } } query.addBindValue(images); query.addBindValue(tags); d->db->execBatch(query); d->db->recordChangeset(ImageTagChangeset(imageIDs, tagIDs, ImageTagChangeset::Added)); } QList CoreDB::getRecentlyAssignedTags() const { return d->recentlyAssignedTags; } void CoreDB::removeItemTag(qlonglong imageID, int tagID) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageTags " "WHERE imageID=? AND tagid=?;"), imageID, tagID); d->db->recordChangeset(ImageTagChangeset(imageID, tagID, ImageTagChangeset::Removed)); } void CoreDB::removeItemAllTags(qlonglong imageID, const QList& currentTagIds) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageTags " "WHERE imageID=?;"), imageID); d->db->recordChangeset(ImageTagChangeset(imageID, currentTagIds, ImageTagChangeset::RemovedAll)); } void CoreDB::removeTagsFromItems(QList imageIDs, const QList& tagIDs) { if (imageIDs.isEmpty() || tagIDs.isEmpty()) { return; } DbEngineSqlQuery query = d->db->prepareQuery(QString::fromUtf8("DELETE FROM ImageTags WHERE imageID=? AND tagid=?;")); QVariantList images; QVariantList tags; foreach(const qlonglong& imageid, imageIDs) { foreach(int tagid, tagIDs) { images << imageid; tags << tagid; } } query.addBindValue(images); query.addBindValue(tags); d->db->execBatch(query); d->db->recordChangeset(ImageTagChangeset(imageIDs, tagIDs, ImageTagChangeset::Removed)); } QStringList CoreDB::getItemNamesInAlbum(int albumID, bool recursive) { QList values; if (recursive) { int rootId = getAlbumRootId(albumID); QString path = getAlbumRelativePath(albumID); d->db->execSql(QString::fromUtf8("SELECT Images.name FROM Images WHERE Images.album IN " " (SELECT DISTINCT id FROM Albums " " WHERE albumRoot=? AND (relativePath=? OR relativePath LIKE ?));"), rootId, path, path == QString::fromUtf8("/") ? QString::fromUtf8("/%") : QString(path + QLatin1String("/%")), &values); } else { d->db->execSql(QString::fromUtf8("SELECT Images.name " "FROM Images " "WHERE Images.album=?;"), albumID, &values); } QStringList names; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { names << it->toString(); } return names; } qlonglong CoreDB::getItemFromAlbum(int albumID, const QString& fileName) { QList values; d->db->execSql(QString::fromUtf8("SELECT Images.id " "FROM Images " "WHERE Images.album=? AND Images.name=?;"), albumID, fileName, &values); if (values.isEmpty()) { return -1; } else { return values.first().toLongLong(); } } /* QStringList CoreDB::getAllItemURLsWithoutDate() { QList values; d->db->execSql( QString::fromUtf8("SELECT AlbumRoots.absolutePath||Albums.relativePath||'/'||Images.name " "FROM Images " " LEFT JOIN Albums ON Images.album=Albums.id " " LEFT JOIN AlbumRoots ON AlbumRoots.id=Albums.albumRoot " "WHERE (Images.datetime is null or " " Images.datetime == '');"), &values ); QStringList urls; for (QList::iterator it = values.begin(); it != values.end(); ++it) urls << it->toString(); return urls; } */ QList CoreDB::getAllCreationDates() { QList values; d->db->execSql(QString::fromUtf8("SELECT creationDate FROM ImageInformation " " INNER JOIN Images ON Images.id=ImageInformation.imageid " " WHERE Images.status=1;"), &values); QList list; foreach(const QVariant& value, values) { if (!value.isNull()) { list << value.toDateTime(); } } return list; } QMap CoreDB::getAllCreationDatesAndNumberOfImages() { QList values; d->db->execSql(QString::fromUtf8("SELECT creationDate FROM ImageInformation " " INNER JOIN Images ON Images.id=ImageInformation.imageid " " WHERE Images.status=1;"), &values); QMap datesStatMap; foreach(const QVariant& value, values) { if (!value.isNull()) { QDateTime dateTime = value.toDateTime(); if (!dateTime.isValid()) { continue; } QMap::iterator it2 = datesStatMap.find(dateTime); if (it2 == datesStatMap.end()) { datesStatMap.insert(dateTime, 1); } else { it2.value()++; } } } return datesStatMap; } QMap CoreDB::getNumberOfImagesInAlbums() { QList values, allAbumIDs; QMap albumsStatMap; int albumID; // initialize allAbumIDs with all existing albums from db to prevent // wrong album image counters d->db->execSql(QString::fromUtf8("SELECT id from Albums"), &allAbumIDs); for (QList::const_iterator it = allAbumIDs.constBegin() ; it != allAbumIDs.constEnd() ; ++it) { albumID = (*it).toInt(); albumsStatMap.insert(albumID, 0); } d->db->execSql(QString::fromUtf8("SELECT album FROM Images WHERE Images.status=1;"), &values); for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { albumID = (*it).toInt(); ++it; QMap::iterator it2 = albumsStatMap.find(albumID); if (it2 == albumsStatMap.end()) { albumsStatMap.insert(albumID, 1); } else { it2.value()++; } } return albumsStatMap; } QMap CoreDB::getNumberOfImagesInTags() { QList values, allTagIDs; QMap tagsStatMap; int tagID; // initialize allTagIDs with all existing tags from db to prevent // wrong tag counters d->db->execSql(QString::fromUtf8("SELECT id from Tags"), &allTagIDs); for (QList::const_iterator it = allTagIDs.constBegin(); it != allTagIDs.constEnd(); ++it) { tagID = (*it).toInt(); tagsStatMap.insert(tagID, 0); } d->db->execSql(QString::fromUtf8("SELECT tagid FROM ImageTags " " LEFT JOIN Images ON Images.id=ImageTags.imageid " " WHERE Images.status=1;"), &values); for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { tagID = (*it).toInt(); ++it; QMap::iterator it2 = tagsStatMap.find(tagID); if (it2 == tagsStatMap.end()) { tagsStatMap.insert(tagID, 1); } else { it2.value()++; } } return tagsStatMap; } QMap CoreDB::getNumberOfImagesInTagProperties(const QString& property) { QList values; QMap tagsStatMap; int tagID, count; d->db->execSql(QString::fromUtf8("SELECT tagid, COUNT(*) FROM ImageTagProperties " " LEFT JOIN Images ON Images.id=ImageTagProperties.imageid " " WHERE ImageTagProperties.property=? AND Images.status=1 " " GROUP BY tagid;"), property, &values); for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { tagID = (*it).toInt(); ++it; count = (*it).toInt(); ++it; tagsStatMap[tagID] = count; } return tagsStatMap; } int CoreDB::getNumberOfImagesInTagProperties(int tagId, const QString& property) { QList values; d->db->execSql(QString::fromUtf8("SELECT COUNT(*) FROM ImageTagProperties " " LEFT JOIN Images ON Images.id=ImageTagProperties.imageid " " WHERE ImageTagProperties.property=? AND Images.status=1 " " AND ImageTagProperties.tagid=? ;"), property, tagId, &values); return values.first().toInt(); } QList CoreDB::getImagesWithImageTagProperty(int tagId, const QString& property) { QList values; QList imageIds; d->db->execSql(QString::fromUtf8("SELECT DISTINCT Images.id FROM ImageTagProperties " " LEFT JOIN Images ON Images.id=ImageTagProperties.imageid " " WHERE ImageTagProperties.property=? AND Images.status=1 " " AND ImageTagProperties.tagid=? ;"), property, tagId, &values); for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { imageIds.append((*it).toInt()); } return imageIds; } QMap CoreDB::getFormatStatistics() { return getFormatStatistics(DatabaseItem::UndefinedCategory); } QMap CoreDB::getFormatStatistics(DatabaseItem::Category category) { QMap map; QString queryString = QString::fromUtf8( "SELECT COUNT(*), II.format " " FROM ImageInformation AS II " " INNER JOIN Images ON II.imageid=Images.id " " WHERE Images.status=1 "); if (category != DatabaseItem::UndefinedCategory) { queryString.append(QString::fromUtf8("AND Images.category=%1").arg(category)); } queryString.append(QString::fromUtf8(" GROUP BY II.format;")); qCDebug(DIGIKAM_DATABASE_LOG) << queryString; DbEngineSqlQuery query = d->db->prepareQuery(queryString); if (d->db->exec(query)) { while (query.next()) { QString quantity = query.value(0).toString(); QString format = query.value(1).toString(); if (format.isEmpty()) { continue; } map[format] = quantity.isEmpty() ? 0 : quantity.toInt(); } } return map; } QStringList CoreDB::getListFromImageMetadata(DatabaseFields::ImageMetadata field) { QStringList list; QList values; QStringList fieldName = imageMetadataFieldList(field); if (fieldName.count() != 1) { return list; } QString sql = QString::fromUtf8("SELECT DISTINCT %1 FROM ImageMetadata " " INNER JOIN Images ON imageid=Images.id;"); sql = sql.arg(fieldName.first()); d->db->execSql(sql, &values); for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { if (!it->isNull()) { list << it->toString(); } } return list; } /* QList > CoreDB::getItemsAndDate() { QList values; d->db->execSql( QString::fromUtf8("SELECT Images.name, datetime FROM Images;", &values )); QList > data; for ( QList::iterator it = values.begin(); it != values.end(); ) { QPair pair; pair.first = (*it).toString(); ++it; pair.second = QDateTime::fromString( (*it).toString(), Qt::ISODate ); ++it; if (!pair.second.isValid()) continue; data << pair; } return data; } */ /* int CoreDB::getAlbumForPath(const QString& albumRoot, const QString& folder, bool create) { CollectionLocation location = CollectionManager::instance()->locationForAlbumRootPath(albumRoot); if (location.isNull()) return -1; return getAlbumForPath(location.id(), folder, create); } */ int CoreDB::getAlbumForPath(int albumRootId, const QString& folder, bool create) { QList values; d->db->execSql(QString::fromUtf8("SELECT id FROM Albums WHERE albumRoot=? AND relativePath=?;"), albumRootId, folder, &values); int albumID = -1; if (values.isEmpty()) { if (create) { albumID = addAlbum(albumRootId, folder, QString(), QDate::currentDate(), QString()); } } else { albumID = values.first().toInt(); } return albumID; } QList CoreDB::getAlbumAndSubalbumsForPath(int albumRootId, const QString& relativePath) { QList values; d->db->execSql(QString::fromUtf8("SELECT id, relativePath FROM Albums WHERE albumRoot=? AND (relativePath=? OR relativePath LIKE ?);"), albumRootId, relativePath, (relativePath == QLatin1String("/") ? QLatin1String("/%") : QString(relativePath + QLatin1String("/%"))), &values); QList albumIds; int id; QString albumRelativePath; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { id = (*it).toInt(); ++it; QString albumRelativePath = (*it).toString(); ++it; // bug #223050: The LIKE operator is case insensitive if (albumRelativePath.startsWith(relativePath)) { albumIds << id; } } return albumIds; } QList CoreDB::getAlbumsOnAlbumRoot(int albumRootId) { QList values; d->db->execSql(QString::fromUtf8("SELECT id FROM Albums WHERE albumRoot=?;"), albumRootId, &values); QList albumIds; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { albumIds << (*it).toInt(); } return albumIds; } qlonglong CoreDB::addItem(int albumID, const QString& name, DatabaseItem::Status status, DatabaseItem::Category category, const QDateTime& modificationDate, qlonglong fileSize, const QString& uniqueHash) { QVariantList boundValues; boundValues << albumID << name << (int)status << (int)category << modificationDate << fileSize << uniqueHash; QVariant id; d->db->execSql(QString::fromUtf8("REPLACE INTO Images " " ( album, name, status, category, modificationDate, fileSize, uniqueHash ) " " VALUES (?,?,?,?,?,?,?);"), boundValues, 0, &id); if (id.isNull()) { return -1; } d->db->recordChangeset(ImageChangeset(id.toLongLong(), DatabaseFields::ImagesAll)); d->db->recordChangeset(CollectionImageChangeset(id.toLongLong(), albumID, CollectionImageChangeset::Added)); return id.toLongLong(); } void CoreDB::updateItem(qlonglong imageID, DatabaseItem::Category category, const QDateTime& modificationDate, qlonglong fileSize, const QString& uniqueHash) { QVariantList boundValues; boundValues << (int)category << modificationDate << fileSize << uniqueHash << imageID; d->db->execSql(QString::fromUtf8("UPDATE Images SET category=?, modificationDate=?, fileSize=?, uniqueHash=? WHERE id=?;"), boundValues); d->db->recordChangeset(ImageChangeset(imageID, DatabaseFields::Category | DatabaseFields::ModificationDate | DatabaseFields::FileSize | DatabaseFields::UniqueHash)); } void CoreDB::setItemStatus(qlonglong imageID, DatabaseItem::Status status) { QVariantList boundValues; boundValues << (int)status << imageID; d->db->execSql(QString::fromUtf8("UPDATE Images SET status=? WHERE id=?;"), boundValues); d->db->recordChangeset(ImageChangeset(imageID, DatabaseFields::Status)); } void CoreDB::setItemAlbum(qlonglong imageID, qlonglong album) { QVariantList boundValues; boundValues << album << imageID; d->db->execSql(QString::fromUtf8("UPDATE Images SET album=? WHERE id=?;"), boundValues); // record that the image was assigned a new album d->db->recordChangeset(ImageChangeset(imageID, DatabaseFields::Album)); // also record that the collection was changed by adding an image to an album. d->db->recordChangeset(CollectionImageChangeset(imageID, album, CollectionImageChangeset::Added)); } +void CoreDB::renameItem(qlonglong imageID, const QString& newName) +{ + d->db->execSql(QString::fromUtf8("UPDATE Images SET name=? " + "WHERE id=?;"), + newName, imageID); +} + /* QList CoreDB::getTagsFromTagPaths(const QStringList& keywordsList, bool create) { if (keywordsList.isEmpty()) return QList(); QList tagIDs; QStringList keywordsList2Create; // Create a list of the tags currently in database TagInfo::List currentTagsList; QList values; d->db->execSql( "SELECT id, pid, name FROM Tags;", &values ); for (QList::const_iterator it = values.constBegin(); it != values.constEnd();) { TagInfo info; info.id = (*it).toInt(); ++it; info.pid = (*it).toInt(); ++it; info.name = (*it).toString(); ++it; currentTagsList.append(info); } // For every tag in keywordsList, scan taglist to check if tag already exists. for (QStringList::const_iterator kwd = keywordsList.constBegin(); kwd != keywordsList.constEnd(); ++kwd ) { // split full tag "url" into list of single tag names QStringList tagHierarchy = (*kwd).split('/', QString::SkipEmptyParts); if (tagHierarchy.isEmpty()) continue; // last entry in list is the actual tag name bool foundTag = false; QString tagName = tagHierarchy.back(); tagHierarchy.pop_back(); for (TagInfo::List::const_iterator tag = currentTagsList.constBegin(); tag != currentTagsList.constEnd(); ++tag ) { // There might be multiple tags with the same name, but in different // hierarchies. We must check them all until we find the correct hierarchy if ((*tag).name == tagName) { int parentID = (*tag).pid; // Check hierarchy, from bottom to top bool foundParentTag = true; QStringList::iterator parentTagName = tagHierarchy.end(); while (foundParentTag && parentTagName != tagHierarchy.begin()) { --parentTagName; foundParentTag = false; for (TagInfo::List::const_iterator parentTag = currentTagsList.constBegin(); parentTag != currentTagsList.constEnd(); ++parentTag ) { // check if name is the same, and if ID is identical // to the parent ID we got from the child tag if ( (*parentTag).id == parentID && (*parentTag).name == (*parentTagName) ) { parentID = (*parentTag).pid; foundParentTag = true; break; } } // If we traversed the list without a match, // foundParentTag will be false, the while loop breaks. } // If we managed to traverse the full hierarchy, // we have our tag. if (foundParentTag) { // add to result list tagIDs.append((*tag).id); foundTag = true; break; } } } if (!foundTag) keywordsList2Create.append(*kwd); } // If tags do not exist in database, create them. if (create && !keywordsList2Create.isEmpty()) { for (QStringList::const_iterator kwd = keywordsList2Create.constBegin(); kwd != keywordsList2Create.constEnd(); ++kwd ) { // split full tag "url" into list of single tag names QStringList tagHierarchy = (*kwd).split('/', QString::SkipEmptyParts); if (tagHierarchy.isEmpty()) continue; int parentTagID = 0; int tagID = 0; bool parentTagExisted = true; // Traverse hierarchy from top to bottom for (QStringList::const_iterator tagName = tagHierarchy.constBegin(); tagName != tagHierarchy.constEnd(); ++tagName) { tagID = 0; // if the parent tag did not exist, we need not check if the child exists if (parentTagExisted) { for (TagInfo::List::const_iterator tag = currentTagsList.constBegin(); tag != currentTagsList.constEnd(); ++tag ) { // find the tag with tag name according to tagHierarchy, // and parent ID identical to the ID of the tag we found in // the previous run. if ((*tag).name == (*tagName) && (*tag).pid == parentTagID) { tagID = (*tag).id; break; } } } if (tagID != 0) { // tag already found in DB parentTagID = tagID; continue; } // Tag does not yet exist in DB, add it tagID = addTag(parentTagID, (*tagName), QString(), 0); if (tagID == -1) { // Something is wrong in database. Abort. break; } // append to our list of existing tags (for following keywords) TagInfo info; info.id = tagID; info.pid = parentTagID; info.name = (*tagName); currentTagsList.append(info); parentTagID = tagID; parentTagExisted = false; } // add to result list tagIDs.append(tagID); } } return tagIDs; } */ int CoreDB::getItemAlbum(qlonglong imageID) { QList values; d->db->execSql(QString::fromUtf8("SELECT album FROM Images WHERE id=?;"), imageID, &values); if (!values.isEmpty()) { return values.first().toInt(); } else { return 1; } } QString CoreDB::getItemName(qlonglong imageID) { QList values; d->db->execSql(QString::fromUtf8("SELECT name FROM Images WHERE id=?;"), imageID, &values); if (!values.isEmpty()) { return values.first().toString(); } else { return QString(); } } /* bool CoreDB::setItemDate(qlonglong imageID, const QDateTime& datetime) { d->db->execSql ( QString::fromUtf8("UPDATE Images SET datetime=?" "WHERE id=?;"), datetime.toString(Qt::ISODate), imageID ); CoreDbAccess::attributesWatch() ->sendImageFieldChanged(imageID, DatabaseAttributesWatch::ImageDate); return true; } bool CoreDB::setItemDate(int albumID, const QString& name, const QDateTime& datetime) { / * d->db->execSql ( QString::fromUtf8("UPDATE Images SET datetime=?" "WHERE dirid=? AND name=?;") .datetime.toString(Qt::ISODate, QString::number(albumID), (name)) ); return true; * / // easier because of attributes watch return setItemDate(getImageId(albumID, name), datetime); } void CoreDB::setItemRating(qlonglong imageID, int rating) { d->db->execSql ( QString::fromUtf8("REPLACE INTO ImageProperties " "(imageid, property, value) " "VALUES(?, ?, ?);"), imageID, QString("Rating"), rating ); CoreDbAccess::attributesWatch() ->sendImageFieldChanged(imageID, DatabaseAttributesWatch::ImageRating); } int CoreDB::getItemRating(qlonglong imageID) { QList values; d->db->execSql( QString::fromUtf8(SELECT value FROM ImageProperties " "WHERE imageid=? and property=?;"), imageID, QString("Rating"), &values); if (!values.isEmpty()) return values.first().toInt(); else return 0; } */ QStringList CoreDB::getItemURLsInAlbum(int albumID, ItemSortOrder sortOrder) { QList values; int albumRootId = getAlbumRootId(albumID); if (albumRootId == -1) { return QStringList(); } QString albumRootPath = CollectionManager::instance()->albumRootPath(albumRootId); if (albumRootPath.isNull()) { return QStringList(); } QMap bindingMap; bindingMap.insert(QString::fromUtf8(":albumID"), albumID); switch (sortOrder) { case ByItemName: d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("getItemURLsInAlbumByItemName")), bindingMap, &values); break; case ByItemPath: // Don't collate on the path - this is to maintain the same behavior // that happens when sort order is "By Path" d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("getItemURLsInAlbumByItemPath")), bindingMap, &values); break; case ByItemDate: d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("getItemURLsInAlbumByItemDate")), bindingMap, &values); break; case ByItemRating: d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("getItemURLsInAlbumByItemRating")), bindingMap, &values); break; case NoItemSorting: default: d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("getItemURLsInAlbumNoItemSorting")), bindingMap, &values); break; } QStringList urls; QString relativePath, name; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { relativePath = (*it).toString(); ++it; name = (*it).toString(); ++it; if (relativePath == QLatin1String("/")) { urls << albumRootPath + relativePath + name; } else { urls << albumRootPath + relativePath + QLatin1Char('/') + name; } } return urls; } QList CoreDB::getItemIDsInAlbum(int albumID) { QList itemIDs; QList values; d->db->execSql(QString::fromUtf8("SELECT id FROM Images WHERE album=?;"), albumID, &values); for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { itemIDs << (*it).toLongLong(); } return itemIDs; } QMap CoreDB::getItemIDsAndURLsInAlbum(int albumID) { int albumRootId = getAlbumRootId(albumID); if (albumRootId == -1) { return QMap(); } QString albumRootPath = CollectionManager::instance()->albumRootPath(albumRootId); if (albumRootPath.isNull()) { return QMap(); } QMap itemsMap; QList values; d->db->execSql(QString::fromUtf8("SELECT Images.id, Albums.relativePath, Images.name " "FROM Images JOIN Albums ON Albums.id=Images.album " "WHERE Albums.id=?;"), albumID, &values); QString path; qlonglong id; QString relativePath, name; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { id = (*it).toLongLong(); ++it; relativePath = (*it).toString(); ++it; name = (*it).toString(); ++it; if (relativePath == QLatin1String("/")) { path = albumRootPath + relativePath + name; } else { path = albumRootPath + relativePath + QLatin1Char('/') + name; } itemsMap.insert(id, path); }; return itemsMap; } QList CoreDB::getAllItems() { QList values; d->db->execSql(QString::fromUtf8("SELECT id FROM Images;"), &values); QList items; foreach(const QVariant& item, values) { items << item.toLongLong(); } return items; } QList CoreDB::getItemScanInfos(int albumID) { QList values; d->db->execSql(QString::fromUtf8("SELECT id, album, name, status, category, modificationDate, fileSize, uniqueHash " "FROM Images WHERE album=?;"), albumID, &values); QList list; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { ItemScanInfo info; info.id = (*it).toLongLong(); ++it; info.albumID = (*it).toInt(); ++it; info.itemName = (*it).toString(); ++it; info.status = (DatabaseItem::Status)(*it).toInt(); ++it; info.category = (DatabaseItem::Category)(*it).toInt(); ++it; info.modificationDate = (*it).toDateTime(); ++it; info.fileSize = (*it).toLongLong(); ++it; info.uniqueHash = (*it).toString(); ++it; list << info; } return list; } ItemScanInfo CoreDB::getItemScanInfo(qlonglong imageID) { QList values; d->db->execSql(QString::fromUtf8("SELECT id, album, name, status, category, modificationDate, fileSize, uniqueHash " "FROM Images WHERE id=?;"), imageID, &values); ItemScanInfo info; if (!values.isEmpty()) { QList::const_iterator it = values.constBegin(); info.id = (*it).toLongLong(); ++it; info.albumID = (*it).toInt(); ++it; info.itemName = (*it).toString(); ++it; info.status = (DatabaseItem::Status)(*it).toInt(); ++it; info.category = (DatabaseItem::Category)(*it).toInt(); ++it; info.modificationDate = (*it).toDateTime(); ++it; info.fileSize = (*it).toLongLong(); ++it; info.uniqueHash = (*it).toString(); ++it; } return info; } QStringList CoreDB::getItemURLsInTag(int tagID, bool recursive) { QList values; QString imagesIdClause; QMap bindingMap; bindingMap.insert(QString::fromUtf8(":tagID"), tagID); bindingMap.insert(QString::fromUtf8(":tagID2"), tagID); if (recursive) { d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("GetItemURLsInTagRecursive")), bindingMap, &values); } else { d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("GetItemURLsInTag")), bindingMap, &values); } QStringList urls; QString albumRootPath, relativePath, name; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ;) { albumRootPath = CollectionManager::instance()->albumRootPath((*it).toInt()); ++it; relativePath = (*it).toString(); ++it; name = (*it).toString(); ++it; if (relativePath == QLatin1String("/")) { urls << albumRootPath + relativePath + name; } else { urls << albumRootPath + relativePath + QLatin1Char('/') + name; } } return urls; } QList CoreDB::getItemIDsInTag(int tagID, bool recursive) { QList itemIDs; QList values; QMap parameters; parameters.insert(QString::fromUtf8(":tagPID"), tagID); parameters.insert(QString::fromUtf8(":tagID"), tagID); if (recursive) { d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("getItemIDsInTagRecursive")), parameters, &values); } else { d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("getItemIDsInTag")), parameters, &values); } for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { itemIDs << (*it).toLongLong(); } return itemIDs; } /* QString CoreDB::getAlbumPath(int albumID) { QList values; d->db->execSql( QString::fromUtf8("SELECT AlbumRoots.absolutePath||Albums.relativePath " "FROM Albums, AlbumRoots WHERE Albums.id=? AND AlbumRoots.id=Albums.albumRoot; "), albumID, &values); if (!values.isEmpty()) return values.first().toString(); else return QString(); } */ QString CoreDB::getAlbumRelativePath(int albumID) { QList values; d->db->execSql(QString::fromUtf8("SELECT relativePath from Albums WHERE id=?;"), albumID, &values); if (!values.isEmpty()) { return values.first().toString(); } else { return QString(); } } int CoreDB::getAlbumRootId(int albumID) { QList values; d->db->execSql(QString::fromUtf8("SELECT albumRoot FROM Albums WHERE id=?;"), albumID, &values); if (!values.isEmpty()) { return values.first().toInt(); } else { return -1; } } QDate CoreDB::getAlbumLowestDate(int albumID) { QList values; d->db->execSql(QString::fromUtf8("SELECT MIN(creationDate) FROM ImageInformation " " INNER JOIN Images ON Images.id=ImageInformation.imageid " " WHERE Images.album=? GROUP BY Images.album;"), albumID, &values); if (!values.isEmpty()) { return values.first().toDate(); } else { return QDate(); } } QDate CoreDB::getAlbumHighestDate(int albumID) { QList values; d->db->execSql(QString::fromUtf8("SELECT MAX(creationDate) FROM ImageInformation " " INNER JOIN Images ON Images.id=ImageInformation.imageid " " WHERE Images.album=? GROUP BY Images.album;"), albumID , &values); if (!values.isEmpty()) { return values.first().toDate(); } else { return QDate(); } } QDate CoreDB::getAlbumAverageDate(int albumID) { QList values; d->db->execSql(QString::fromUtf8("SELECT creationDate FROM ImageInformation " " INNER JOIN Images ON Images.id=ImageInformation.imageid " " WHERE Images.album=?;"), albumID , &values); QList dates; for (QList::const_iterator it = values.constBegin() ; it != values.constEnd() ; ++it) { QDateTime itemDateTime = (*it).toDateTime(); if (itemDateTime.isValid()) { dates << itemDateTime.date(); } } if (dates.isEmpty()) { return QDate(); } qint64 julianDays = 0; foreach(const QDate& date, dates) { julianDays += date.toJulianDay(); } return QDate::fromJulianDay(julianDays / dates.size()); } void CoreDB::deleteItem(int albumID, const QString& file) { qlonglong imageId = getImageId(albumID, file); if (imageId == -1) { return; } d->db->execSql(QString::fromUtf8("DELETE FROM Images WHERE id=?;"), imageId); d->db->recordChangeset(CollectionImageChangeset(imageId, albumID, CollectionImageChangeset::Deleted)); } void CoreDB::deleteItem(qlonglong imageId) { d->db->execSql(QString::fromUtf8("DELETE FROM Images WHERE id=? AND album IS NULL;"), imageId); } void CoreDB::removeItemsFromAlbum(int albumID, const QList& ids_forInformation) { d->db->execSql(QString::fromUtf8("UPDATE Images SET status=?, album=NULL WHERE album=?;"), (int)DatabaseItem::Trashed, albumID); d->db->recordChangeset(CollectionImageChangeset(ids_forInformation, albumID, CollectionImageChangeset::RemovedAll)); } void CoreDB::removeItems(QList itemIDs, const QList& albumIDs) { DbEngineSqlQuery query = d->db->prepareQuery(QString::fromUtf8("UPDATE Images SET status=?, album=NULL WHERE id=?;")); QVariantList imageIds; QVariantList status; foreach(const qlonglong& id, itemIDs) { status << (int)DatabaseItem::Trashed; imageIds << id; } query.addBindValue(status); query.addBindValue(imageIds); d->db->execBatch(query); d->db->recordChangeset(CollectionImageChangeset(itemIDs, albumIDs, CollectionImageChangeset::Removed)); } void CoreDB::removeItemsPermanently(QList itemIDs, const QList& albumIDs) { DbEngineSqlQuery query = d->db->prepareQuery(QString::fromUtf8("UPDATE Images SET status=?, album=NULL WHERE id=?;")); QVariantList imageIds; QVariantList status; foreach(const qlonglong& id, itemIDs) { status << (int)DatabaseItem::Obsolete; imageIds << id; } query.addBindValue(status); query.addBindValue(imageIds); d->db->execBatch(query); d->db->recordChangeset(CollectionImageChangeset(itemIDs, albumIDs, CollectionImageChangeset::Removed)); } void CoreDB::deleteRemovedItems() { d->db->execSql(QString::fromUtf8("DELETE FROM Images WHERE status=?;"), (int)DatabaseItem::Obsolete); d->db->recordChangeset(CollectionImageChangeset(QList(), QList(), CollectionImageChangeset::RemovedDeleted)); } /* // This method is probably nonsense because a remove image no longer has an associated album void CoreDB::deleteRemovedItems(QList albumIds) { DbEngineSqlQuery query = d->db->prepareQuery( QString::fromUtf8("DELETE FROM Images WHERE status=? AND album=?;") ); QVariantList albumBindIds; QVariantList status; foreach(int albumId, albumIds) { status << (int)DatabaseItem::Removed; albumBindIds << albumId; } query.addBindValue(status); query.addBindValue(albumBindIds); d->db->execBatch(query); d->db->recordChangeset(CollectionImageChangeset(QList(), albumIds, CollectionImageChangeset::RemovedDeleted)); } */ void CoreDB::renameAlbum(int albumID, int newAlbumRoot, const QString& newRelativePath) { int albumRoot = getAlbumRootId(albumID); QString relativePath = getAlbumRelativePath(albumID); if (relativePath == newRelativePath && albumRoot == newAlbumRoot) { return; } // first delete any stale albums left behind at the destination of renaming QMap parameters; parameters.insert(QString::fromUtf8(":albumRoot"), newAlbumRoot); parameters.insert(QString::fromUtf8(":relativePath"), newRelativePath); if (BdEngineBackend::NoErrors != d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("deleteAlbumRootPath")), parameters)) { return; } // now update the album d->db->execSql(QString::fromUtf8("UPDATE Albums SET albumRoot=?, relativePath=? WHERE id=? AND albumRoot=?;"), newAlbumRoot, newRelativePath, albumID, albumRoot); d->db->recordChangeset(AlbumChangeset(albumID, AlbumChangeset::Renamed)); /* if (renameSubalbums) { // now find the list of all subalbums which need to be updated QList values; d->db->execSql( QString::fromUtf8("SELECT id, relativePath FROM Albums WHERE albumRoot=? AND relativePath LIKE ?;"), albumRoot, oldUrl + "/%", &values ); // and update their url QString newChildURL; for (QList::iterator it = values.begin(); it != values.end(); ) { int childAlbumId = (*it).toInt(); ++it; newChildURL = (*it).toString(); ++it; newChildURL.replace(oldUrl, newRelativePath); d->db->execSql(QString::fromUtf8("UPDATE Albums SET albumRoot=?, relativePath=? WHERE albumRoot=? AND relativePath=?"), newAlbumRoot, newChildURL, albumRoot, (*it) ); d->db->recordChangeset(AlbumChangeset(childAlbumId, AlbumChangeset::Renamed)); } } */ } void CoreDB::setTagName(int tagID, const QString& name) { d->db->execSql(QString::fromUtf8("UPDATE Tags SET name=? WHERE id=?;"), name, tagID); d->db->recordChangeset(TagChangeset(tagID, TagChangeset::Renamed)); } void CoreDB::moveItem(int srcAlbumID, const QString& srcName, int dstAlbumID, const QString& dstName) { // find id of src image qlonglong imageId = getImageId(srcAlbumID, srcName); if (imageId == -1) { return; } // first delete any stale database entries (for destination) if any deleteItem(dstAlbumID, dstName); d->db->execSql(QString::fromUtf8("UPDATE Images SET album=?, name=? " "WHERE id=?;"), dstAlbumID, dstName, imageId); d->db->recordChangeset(CollectionImageChangeset(imageId, srcAlbumID, CollectionImageChangeset::Moved)); d->db->recordChangeset(CollectionImageChangeset(imageId, srcAlbumID, CollectionImageChangeset::Removed)); d->db->recordChangeset(CollectionImageChangeset(imageId, dstAlbumID, CollectionImageChangeset::Added)); } int CoreDB::copyItem(int srcAlbumID, const QString& srcName, int dstAlbumID, const QString& dstName) { // find id of src image qlonglong srcId = getImageId(srcAlbumID, srcName); if (srcId == -1 || dstAlbumID == -1 || dstName.isEmpty()) { return -1; } // check for src == dest if (srcAlbumID == dstAlbumID && srcName == dstName) { return srcId; } // first delete any stale database entries if any deleteItem(dstAlbumID, dstName); // copy entry in Images table QVariant id; d->db->execSql(QString::fromUtf8("INSERT INTO Images " " ( album, name, status, category, modificationDate, fileSize, uniqueHash ) " " SELECT ?, ?, status, category, modificationDate, fileSize, uniqueHash " " FROM Images WHERE id=?;"), dstAlbumID, dstName, srcId, 0, &id); if (id.isNull()) { return -1; } d->db->recordChangeset(ImageChangeset(id.toLongLong(), DatabaseFields::ImagesAll)); d->db->recordChangeset(CollectionImageChangeset(id.toLongLong(), srcAlbumID, CollectionImageChangeset::Copied)); d->db->recordChangeset(CollectionImageChangeset(id.toLongLong(), dstAlbumID, CollectionImageChangeset::Added)); // copy all other tables copyImageAttributes(srcId, id.toLongLong()); return id.toLongLong(); } void CoreDB::copyImageAttributes(qlonglong srcId, qlonglong dstId) { // Go through all image-specific tables and copy the entries DatabaseFields::Set fields; d->db->execSql(QString::fromUtf8("REPLACE INTO ImageInformation " " (imageid, rating, creationDate, digitizationDate, orientation, " " width, height, format, colorDepth, colorModel) " "SELECT ?, rating, creationDate, digitizationDate, orientation, " " width, height, format, colorDepth, colorModel " "FROM ImageInformation WHERE imageid=?;"), dstId, srcId); fields |= DatabaseFields::ImageInformationAll; d->db->execSql(QString::fromUtf8("REPLACE INTO ImageMetadata " " (imageid, make, model, lens, aperture, focalLength, focalLength35, " " exposureTime, exposureProgram, exposureMode, sensitivity, flash, whiteBalance, " " whiteBalanceColorTemperature, meteringMode, subjectDistance, subjectDistanceCategory) " "SELECT ?, make, model, lens, aperture, focalLength, focalLength35, " " exposureTime, exposureProgram, exposureMode, sensitivity, flash, whiteBalance, " " whiteBalanceColorTemperature, meteringMode, subjectDistance, subjectDistanceCategory " "FROM ImageMetadata WHERE imageid=?;"), dstId, srcId); fields |= DatabaseFields::ImageMetadataAll; d->db->execSql(QString::fromUtf8("REPLACE INTO VideoMetadata " " (imageid, aspectRatio, audioBitRate, audioChannelType, audioCompressor, duration, frameRate, " " videoCodec) " "SELECT ?, aspectRatio, audioBitRate, audioChannelType, audioCompressor, duration, frameRate, " " videoCodec " "FROM VideoMetadata WHERE imageid=?;"), dstId, srcId); fields |= DatabaseFields::VideoMetadataAll; d->db->execSql(QString::fromUtf8("REPLACE INTO ImagePositions " " (imageid, latitude, latitudeNumber, longitude, longitudeNumber, " " altitude, orientation, tilt, roll, accuracy, description) " "SELECT ?, latitude, latitudeNumber, longitude, longitudeNumber, " " altitude, orientation, tilt, roll, accuracy, description " "FROM ImagePositions WHERE imageid=?;"), dstId, srcId); fields |= DatabaseFields::ImagePositionsAll; d->db->execSql(QString::fromUtf8("REPLACE INTO ImageComments " " (imageid, type, language, author, date, comment) " "SELECT ?, type, language, author, date, comment " "FROM ImageComments WHERE imageid=?;"), dstId, srcId); fields |= DatabaseFields::ImageCommentsAll; d->db->execSql(QString::fromUtf8("REPLACE INTO ImageCopyright " " (imageid, property, value, extraValue) " "SELECT ?, property, value, extraValue " "FROM ImageCopyright WHERE imageid=?;"), dstId, srcId); d->db->execSql(QString::fromUtf8("REPLACE INTO ImageHistory " " (imageid, uuid, history) " "SELECT ?, uuid, history " "FROM ImageHistory WHERE imageid=?;"), dstId, srcId); fields |= DatabaseFields::ImageHistoryInfoAll; d->db->execSql(QString::fromUtf8("REPLACE INTO ImageRelations " " (subject, object, type) " "SELECT ?, object, type " "FROM ImageRelations WHERE subject=?;"), dstId, srcId); d->db->execSql(QString::fromUtf8("REPLACE INTO ImageRelations " " (subject, object, type) " "SELECT subject, ?, type " "FROM ImageRelations WHERE object=?;"), dstId, srcId); fields |= DatabaseFields::ImageRelations; d->db->recordChangeset(ImageChangeset(dstId, fields)); copyImageTags(srcId, dstId); copyImageProperties(srcId, dstId); } void CoreDB::copyImageProperties(qlonglong srcId, qlonglong dstId) { d->db->execSql(QString::fromUtf8("REPLACE INTO ImageProperties " " (imageid, property, value) " "SELECT ?, property, value " "FROM ImageProperties WHERE imageid=?;"), dstId, srcId); } void CoreDB::copyImageTags(qlonglong srcId, qlonglong dstId) { d->db->execSql(QString::fromUtf8("REPLACE INTO ImageTags " " (imageid, tagid) " "SELECT ?, tagid " "FROM ImageTags WHERE imageid=?;"), dstId, srcId); d->db->execSql(QString::fromUtf8("REPLACE INTO ImageTagProperties " " (imageid, tagid, property, value) " "SELECT ?, tagid, property, value " "FROM ImageTagProperties WHERE imageid=?;"), dstId, srcId); // leave empty tag list for now d->db->recordChangeset(ImageTagChangeset(dstId, QList(), ImageTagChangeset::Added)); d->db->recordChangeset(ImageTagChangeset(dstId, QList(), ImageTagChangeset::PropertiesChanged)); } bool CoreDB::copyAlbumProperties(int srcAlbumID, int dstAlbumID) { if (srcAlbumID == dstAlbumID) { return true; } QList values; d->db->execSql(QString::fromUtf8("SELECT date, caption, collection, icon " "FROM Albums WHERE id=?;"), srcAlbumID, &values); if (values.isEmpty()) { qCWarning(DIGIKAM_DATABASE_LOG) << " src album ID " << srcAlbumID << " does not exist"; return false; } QList boundValues; boundValues << values.at(0) << values.at(1) << values.at(2) << values.at(3); boundValues << dstAlbumID; d->db->execSql(QString::fromUtf8("UPDATE Albums SET date=?, caption=?, " "collection=?, icon=? WHERE id=?;"), boundValues); return true; } QList CoreDB::getImageIdsFromArea(qreal lat1, qreal lat2, qreal lng1, qreal lng2, int /*sortMode*/, const QString& /*sortBy*/) { QList values; QList boundValues; boundValues << lat1 << lat2 << lng1 << lng2; //CoreDbAccess access; d->db->execSql(QString::fromUtf8("Select ImageInformation.imageid, ImageInformation.rating, ImagePositions.latitudeNumber, ImagePositions.longitudeNumber" " FROM ImageInformation INNER JOIN ImagePositions" " ON ImageInformation.imageid = ImagePositions.imageid" " WHERE (ImagePositions.latitudeNumber>? AND ImagePositions.latitudeNumber? AND ImagePositions.longitudeNumberdb->execSql(QString::fromUtf8("DELETE FROM ImageInformation WHERE imageid=?;"), imageID); fields |= DatabaseFields::ImageInformationAll; d->db->execSql(QString::fromUtf8("DELETE FROM ImagePositions WHERE imageid=?;"), imageID); fields |= DatabaseFields::ImagePositionsAll; d->db->execSql(QString::fromUtf8("DELETE FROM ImageCopyright WHERE imageid=?;"), imageID); d->db->execSql(QString::fromUtf8("DELETE FROM ImageComments WHERE imageid=?;"), imageID); fields |= DatabaseFields::ImageCommentsAll; d->db->execSql(QString::fromUtf8("DELETE FROM ImageMetadata WHERE imageid=?;"), imageID); fields |= DatabaseFields::ImageMetadataAll; d->db->execSql(QString::fromUtf8("DELETE FROM VideoMetadata WHERE imageid=?;"), imageID); fields |= DatabaseFields::VideoMetadataAll; d->db->execSql(QString::fromUtf8("DELETE FROM ImageHistory WHERE imageid=?;"), imageID); fields |= DatabaseFields::ImageHistoryInfoAll; d->db->recordChangeset(ImageChangeset(imageID, fields)); QList tagIds = getItemTagIDs(imageID); if (!tagIds.isEmpty()) { d->db->execSql(QString::fromUtf8("DELETE FROM ImageTags WHERE imageid=?;"), imageID); d->db->recordChangeset(ImageTagChangeset(imageID, tagIds, ImageTagChangeset::RemovedAll)); } QList properties = getImageTagProperties(imageID); if (!properties.isEmpty()) { QList tagIds; foreach(const ImageTagProperty& property, properties) { tagIds << property.imageId; } d->db->execSql(QString::fromUtf8("DELETE FROM ImageTagProperties WHERE imageid=?;"), imageID); d->db->recordChangeset(ImageTagChangeset(imageID, tagIds, ImageTagChangeset::PropertiesChanged)); } } bool CoreDB::integrityCheck() { QList values; d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("checkCoreDbIntegrity")), &values); switch (d->db->databaseType()) { case BdEngineBackend::DbType::SQLite: // For SQLite the integrity check returns a single row with one string column "ok" on success and multiple rows on error. return values.size() == 1 && values.first().toString().toLower().compare(QLatin1String("ok")) == 0; case BdEngineBackend::DbType::MySQL: // For MySQL, for every checked table, the table name, operation (check), message type (status) and the message text (ok on success) // are returned. So we check if there are four elements and if yes, whether the fourth element is "ok". //qCDebug(DIGIKAM_DATABASE_LOG) << "MySQL check returned " << values.size() << " rows"; if ((values.size() % 4) != 0) { return false; } for (QList::iterator it = values.begin() ; it != values.end() ;) { QString tableName = (*it).toString(); ++it; QString operation = (*it).toString(); ++it; QString messageType = (*it).toString(); ++it; QString messageText = (*it).toString(); ++it; if (messageText.toLower().compare(QLatin1String("ok")) != 0) { qCDebug(DIGIKAM_DATABASE_LOG) << "Failed integrity check for table " << tableName << ". Reason:" << messageText; return false; } else { //qCDebug(DIGIKAM_DATABASE_LOG) << "Passed integrity check for table " << tableName; } } // No error conditions. Db passed the integrity check. return true; default: return false; } } void CoreDB::vacuum() { d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("vacuumCoreDB"))); } void CoreDB::readSettings() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(d->configGroupName); d->recentlyAssignedTags = group.readEntry(d->configRecentlyUsedTags, QList()); } void CoreDB::writeSettings() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(d->configGroupName); group.writeEntry(d->configRecentlyUsedTags, d->recentlyAssignedTags); } } // namespace Digikam diff --git a/core/libs/database/coredb/coredb.h b/core/libs/database/coredb/coredb.h index 04e1e33681..30d36c807e 100644 --- a/core/libs/database/coredb/coredb.h +++ b/core/libs/database/coredb/coredb.h @@ -1,1384 +1,1390 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2004-06-18 * Description : Core database interface. * * Copyright (C) 2004-2005 by Renchi Raju * Copyright (C) 2006-2018 by Gilles Caulier * Copyright (C) 2006-2012 by Marcel Wiesweg * Copyright (C) 2012 by Andi Clemens * * 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, 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. * * ============================================================ */ #ifndef DIGIKAM_CORE_DB_H #define DIGIKAM_CORE_DB_H // Qt includes #include #include #include #include #include #include #include // Local includes #include "coredbalbuminfo.h" #include "coredbfields.h" #include "coredbaccess.h" #include "coredbconstants.h" #include "digikam_export.h" #include "dbenginesqlquery.h" #include "album.h" namespace Digikam { class CoreDbBackend; class DIGIKAM_DATABASE_EXPORT CoreDB { public: /** * This adds a keyword-value combination to the database Settings table * if the keyword already exists, the value will be replaced with the new * value. * @param keyword The keyword * @param value The value */ void setSetting(const QString& keyword, const QString& value); /** * This function returns the value which is stored in the database * (table Settings). * @param keyword The keyword for which the value has to be returned. * @return The values which belongs to the keyword, or a null string if * no value is set. */ QString getSetting(const QString& keyword); /** * Get the settings for the file name filters of this database. * Returns a list with lowercase suffixes only, no wildcards added ("png", not "*.png") * Returned is a joint result of main and user settings. * If you are not interested in a specific value, pass 0. */ void getFilterSettings(QStringList* imageFilter, QStringList* videoFilter, QStringList* audioFilter); /** * Returns the user-configurable filter settings. * If you are not interested in a specific value, pass 0. */ void getUserFilterSettings(QString* imageFilterString, QString* videoFilterString, QString* audioFilterString); /** * Sets the main filter settings of the database. Should only be called at schema update. */ void setFilterSettings(const QStringList& imageFilter, const QStringList& videoFilter, const QStringList& audioFilter); /** * Sets the user-configurable filter settings. The lists shall be as specified for getFilterSettings. * They may include entries starting with "-", which indicates that this format shall be removed from * the list, if it is included in the main settings list. */ void setUserFilterSettings(const QStringList& imageFilter, const QStringList& videoFilter, const QStringList& audioFilter); void setIgnoreDirectoryFilterSettings(const QStringList& ignoreDirectoryFilter); void setUserIgnoreDirectoryFilterSettings(const QStringList& ignoreDirectoryFilters); void getUserIgnoreDirectoryFilterSettings(QString* ignoreDirectoryFilterString); void getIgnoreDirectoryFilterSettings(QStringList* ignoreDirectoryFilter); /** * Returns a UUID for the database file. * This UUID is kept stable over schema updates. */ QUuid databaseUuid(); /** * Returns the version used for the unique hash in this database. * The value is cached. */ int getUniqueHashVersion(); void setUniqueHashVersion(int version); bool isUniqueHashV2(); // ----------- AlbumRoot operations ----------- /** * Returns all albums and their attributes in the database * @return a list of albums and their attributes */ QList getAlbumRoots(); /** * Add a new album to the database with the given attributes * @param type The type of the album root * @param specificPath The path specific to volume * @param label An (optional) user-visible label * @returns the album root id of the newly created root */ int addAlbumRoot(AlbumRoot::Type type, const QString& identifier, const QString& specificPath, const QString& label); /** * Deletes an album root from the database. * @param rootId the id of the album root */ void deleteAlbumRoot(int rootId); /** * Migrates a given album root to a new disk location. This only changes the values * in the AlbumRoots table. It is expected that this merely reflects underlying partition changes, * still pointing to the same data. */ void migrateAlbumRoot(int rootId, const QString& identifier); /** * Changes the label of the specified album root * @param rootId the id of the album root * @param newLabel new label for the album root */ void setAlbumRootLabel(int rootId, const QString& newLabel); /** * Sets the type of the specified album root to a new value. */ void changeAlbumRootType(int rootId, AlbumRoot::Type newType); // ----------- Album Listing operations ----------- /** * Returns all albums and their attributes in the database * @return a list of albums and their attributes */ AlbumInfo::List scanAlbums(); /** * Returns all tags and their attributes in the database * @return a list of tags and their attributes */ TagInfo::List scanTags(); TagInfo getTagInfo(int tagId); /** * Returns all searches from the database * @return a list of searches from the database */ SearchInfo::List scanSearches(); /** * Returns all albums in the database with their albumRoot and ID, * ordered by id. */ QList getAlbumShortInfos(); /** * Returns all tags in the database with their parent id and name, * ordered by id. */ QList getTagShortInfos(); // ----------- Operations on PAlbums ----------- /** * Add a new album to the database with the given attributes * @param albumRootId id of the album root of the new album * @param relativePath url of the album * @param caption the album caption * @param date the date for the album * @param collection the album collection * @return the id of the album added or -1 if it failed */ int addAlbum(int albumRootId, const QString& relativePath, const QString& caption, const QDate& date, const QString& collection); /* int addAlbum(const QString& albumRoot, const QString& relativePath, const QString& caption, const QDate& date, const QString& collection); */ /** * Find out the album for a given folder. * @param albumRootId id of the album root of the album * @param relativePath The relative path for which you want the albumID relative to the album root * @param create If true, an album is newly created if it does not yet exist. * If false, -1 is returned if no album exists. * @return The albumID for that folder, or -1 if it does not exist and create is false. */ int getAlbumForPath(int albumRootId, const QString& relativePath, bool create = true); //int getAlbumForPath(const QString& albumRoot, const QString& relativePath, bool create = true); /** * Find out the album ids for a given relative path, including the subalbums. * @param albumRootId id of the album root of the album * @param relativePath The path for which you want the albumIDs relative to the album root * @return a list of album ids. The list is empty if no albums are found. */ QList getAlbumAndSubalbumsForPath(int albumRootId, const QString& relativePath); /* * * List the urls of all subalbums of the album specified by albumRoot and path. * @param onlyDirectSubalbums if this is true, only first-level subalbums are returned, * if false, all levels of children are returned (include subalbums of subalbums). */ //QStringList getSubalbumsForPath(const QString& albumRoot, const QString& path, bool onlyDirectSubalbums = true); /** * Find out all album ids of a given album root * @return a list of album ids. */ QList getAlbumsOnAlbumRoot(int albumRootId); /** * Deletes an album from the database. This will not delete the * subalbums of the album. * @param albumID the id of the album */ void deleteAlbum(int albumID); /** Makes the album a stale entry by setting the albumRoot to 0. * Emits the same changeset as deleteAlbum() */ void makeStaleAlbum(int albumID); /** * Deletes albums from the database that were previously removed * with makeStaleAlbum() */ void deleteStaleAlbums(); /** * Copy the properties of the given srcAlbum to the dstAlbum. * Both albums must exist. * @return true if the operations succeeds */ bool copyAlbumProperties(int srcAlbumID, int dstAlbumID); /** * Give an existing album a new relativePath and a newAlbumRootId */ void renameAlbum(int albumID, int newAlbumRootId, const QString& newRelativePath); /** * Set a caption for the album. * @param albumID the id of the album * @param caption the new caption for the album */ void setAlbumCaption(int albumID, const QString& caption); /** * Set a category for the album. * @param albumID the id of the album * @param category the new category for the album */ void setAlbumCategory(int albumID, const QString& category); /** * Set a date for the album. * @param albumID the id of the album * @param date the date for the album */ void setAlbumDate(int albumID, const QDate& date); /** * Set the icon for the album. * @param albumID the id of the album * @param iconID the id of the icon file */ void setAlbumIcon(int albumID, qlonglong iconID); /** * Given an albumid, this returns the album root id for that album * @param albumID the id of the albumdb * @return the id of the album root of this album */ int getAlbumRootId(int albumID); /** * Given an albumid, this returns the path for that album * @param albumID the id of the album * @return the url of the album */ //QString getAlbumPath(int albumID); /** * Given an albumid, this returns the relative path for that album * (the path below the album root, starting with a slash) * @param albumID the id of the album * @return the url of the album */ QString getAlbumRelativePath(int albumID); /** * Returns the lowest/oldest date of all images for that album. * @param albumID the id of the album to calculate * @return the date. */ QDate getAlbumLowestDate(int albumID); /** * Returns the highest/newest date of all images for that album. * @param albumID the id of the album to calculate * @return the date. */ QDate getAlbumHighestDate(int albumID); /** * Returns the average date of all images for that album. * @param albumID the id of the album to calculate * @return the date. */ QDate getAlbumAverageDate(int albumID); /** * Returns a QMap of album id -> count of items * in the album */ QMap getNumberOfImagesInAlbums(); // ----------- Operations on TAlbums ----------- /** * Adds a new tag to the database with given name, icon and parent id. * @param parentTagID the id of the tag which will become the new tags parent * @param name the name of the tag * @param iconKDE the name of the icon file (this is filename which kde * iconloader can load up) * @param iconID the id of the icon file * Note: if the iconKDE parameter is empty, then the iconID parameter is used * @return the id of the tag added or -1 if it failed */ int addTag(int parentTagID, const QString& name, const QString& iconKDE, qlonglong iconID); /** * Get a list of recently assigned tags (only last 6 tags are listed) * @return the list of recently assigned tags */ //TODO move to other place (AlbumManager) QList getRecentlyAssignedTags() const; /** * Deletes a tag from the database. This will not delete the * subtags of the tag. * @param tagID the id of the tag */ void deleteTag(int tagID); /** * Set a new name for the tag. * @param tagID the id of the tag * @param name the new name for the tag */ void setTagName(int tagID, const QString& name); /** * Set the icon for the tag. * @param tagID the id of the tag * @param iconKDE the filename for the kde icon file * @param iconID the id of the icon file * Note: Only one of the iconKDE or iconID parameters is used. * if the iconKDE parameter is empty, then the iconID parameter is used */ void setTagIcon(int tagID, const QString& iconKDE, qlonglong iconID); /** * Set the parent tagid for the tag. This is equivalent to reparenting * the tag * @param tagID the id of the tag * @param newParentTagID the new parentid for the tag */ void setTagParentID(int tagID, int newParentTagID); /** * Returns the list of all tag properties (ordered by tag id, then property). */ QList getTagProperties(); /** * Returns the list of tag properties of the given tag. */ QList getTagProperties(int tagID); /** * Returns the list of tag properties with the given attribute. */ QList getTagProperties(const QString& property); /** * Adds a tag property. Note that this never replaces existing entries. * It is also all right to add multiple entries for a tag with the same property. * To replace an existing entry, remove the entry before. */ void addTagProperty(int tagId, const QString& property, const QString& value); void addTagProperty(const TagProperty& property); /** * Removes properties for the given tag. If the value is given, removes only * the entries with the given property/value pair. If only property is given, * removes all properties with the given name. If property is null, * removes all properties for the given tag. */ void removeTagProperties(int tagId, const QString& property = QString(), const QString& value = QString()); /** * Returns a list of tag ids with the specified property. * FIXME: Not tested, might not work at all. */ QList getTagsWithProperty(const QString& property); // ----------- Operations on SAlbums ----------- /** * Add a new search to the database with the given attributes * @param type search type * @param name name of the search * @param query search query to use * @return the id of the album added or -1 if it failed */ int addSearch(DatabaseSearch::Type type, const QString& name, const QString& query); /** * Updates Search with new attributes * @param searchID the id of the search * @param type type of the search * @param query database query of the search */ void updateSearch(int searchID, DatabaseSearch::Type type, const QString& name, const QString& query); /** * Delete a search from the database. * @param searchID the id of the search */ void deleteSearch(int searchID); /** * Delete all search with the given type */ void deleteSearches(DatabaseSearch::Type type); /** * Get information about the specified search */ SearchInfo getSearchInfo(int searchId); /** * Get the query for the search specified by its id */ QString getSearchQuery(int searchId); // ----------- Adding and deleting Items ----------- /** * Put a new item in the database or replace an existing one. * @return the id of item added or -1 if it fails */ qlonglong addItem(int albumID, const QString& name, DatabaseItem::Status status, DatabaseItem::Category category, const QDateTime& modificationDate, qlonglong fileSize, const QString& uniqueHash); /** * Deletes an item from the database. * @param albumID The id of the album. * @param file The filename of the file to delete. */ void deleteItem(int albumID, const QString& file); /** * Deletes an item from the database if it does not belong to an album. * This method can only be used if the album of the image is NULL! * @param imageId The id of the image. */ void deleteItem(qlonglong imageId); /** * Marks all items in the specified album as removed, * resets their dirids. * The album can be deleted afterwards without removing * the entries for the items, which * can later be removed by deleteRemovedItems(). * @param albumID The id of the album * @param ids Fully optional: The image ids in the album, if you know them anyway. * This parameter is only used for distributing the change notification. */ void removeItemsFromAlbum(int albumID, const QList& ids_forInformation = QList()); /** * Marks all items in the list as removed, * resets their dirids. * The items can later be removed by deleteRemovedItems(). * @param itemIDs a list of item IDs to be marked * @param albumIDs this parameter is purely informational. * it shall contain the albums that the items are removed from. */ void removeItems(QList itemIDs, const QList& albumIDs = QList()); /** * Marks all items in the list as obsolete, * resets their dirids. * The items can later be removed by deleteRemovedItems(). * @param itemIDs a list of item IDs to be marked * @param albumIDs this parameter is purely informational. * it shall contain the albums that the items are removed from. */ void removeItemsPermanently(QList itemIDs, const QList& albumIDs = QList()); /** * Delete all items from the database that are marked as removed. * Use with care! */ void deleteRemovedItems(); // ----------- Finding items ----------- /** * Get the imageId of the item * @param albumID the albumID of the item * @param name the name of the item * @return the ImageId for the item, or -1 if it does not exist */ qlonglong getImageId(int albumID, const QString& name); /** * Get the imageId fitting to the information given for the item * @param albumID the albumID of the item (-1 means NULL) * @param name the name of the item * @param status the status of the item * @return the ImageIds for the item, or an empty list if there are no matching entries. */ QList getImageIds(int albumID, const QString& name, DatabaseItem::Status status); /** * Returns all image ids with the given status. * @param status The status. * @return The ids of the images that have the given status. */ QList getImageIds(DatabaseItem::Status status); /** * Returns all image ids with the given status and category. * @param status The status. * @param category The category. * @return The ids of the images that have the given status. */ QList getImageIds(DatabaseItem::Status status, DatabaseItem::Category category); /** * Get the imageId fitting to the information given for the item * @param albumID the albumID of the item (-1 means NULL) * @param name the name of the item * @param status the status of the item * @param category the category of the item * @param modificationDate the modification date * @param fileSize the file size * @param uniqueHash the unique hash * @return the ImageId for the item, or -1 if no matching or more than one infos were found. */ qlonglong getImageId(int albumID, const QString& name, DatabaseItem::Status status, DatabaseItem::Category category, const QDateTime& modificationDate, qlonglong fileSize, const QString& uniqueHash); enum ItemSortOrder { NoItemSorting, ByItemName, ByItemPath, ByItemDate, ByItemRating }; /** * Returns all items for a given albumid. This is used to * verify if all items on disk are consistent with the database * in the CollectionScanner class. * @param albumID The albumID for which you want all items. * @param recursive perform a recursive folder hierarchy parsing * @return It returns a QStringList with the filenames. */ QStringList getItemNamesInAlbum(int albumID, bool recursive=false); /** * Returns all ids of items in images table. */ QList getAllItems(); /** * Returns the id of the item with the given filename in * the album with the given id. * @param albumId The albumId in which we search the item. * @param fileName The name of the item file. * @return The item id or -1 if not existent. */ qlonglong getItemFromAlbum(int albumID, const QString& fileName); /** * Returns an ItemScanInfo object for each item in the album * with the specified album id. */ QList getItemScanInfos(int albumID); /** * Given a albumID, get a list of the url of all items in the album * NOTE: Uses the CollectionManager * @param albumID the id of the album * @param order order for the returned items to use * @return a list of urls for the items in the album. The urls are the * absolute path of the items */ QStringList getItemURLsInAlbum(int albumID, ItemSortOrder order = NoItemSorting); /** * Given a albumID, get a list of Ids of all items in the album * @param albumID the id of the album * @return a list of Ids for the items in the album. */ QList getItemIDsInAlbum(int albumID); /** * Given a albumID, get a map of Ids and urls of all items in the album * NOTE: Uses the CollectionManager * @param albumID the id of the album * @return a map of Ids and urls for the items in the album. The urls are the * absolute path of the items */ QMap getItemIDsAndURLsInAlbum(int albumID); /** * Given a tagid, get a list of the url of all items in the tag * NOTE: Uses the CollectionManager * @param tagID the id of the tag * @param recursive perform a recursive folder hierarchy parsing * @return a list of urls for the items in the tag. The urls are the * absolute path of the items */ QStringList getItemURLsInTag(int tagID, bool recursive = false); /** * Given a tagID, get a list of Ids of all items in the tag * @param tagID the id of the tag * @param recursive perform a recursive folder hierarchy parsing * @return a list of Ids for the items in the tag. */ QList getItemIDsInTag(int tagID, bool recursive = false); /** * Returns all creation dates found in the image metadata table */ QList getAllCreationDates(); /** * Returns a QMap of creationDate -> count of items * with the tag */ QMap getAllCreationDatesAndNumberOfImages(); // ----------- Item properties ----------- /** * Find the album of an item * @param imageID The ID of the item * @return The ID of the PAlbum of the item, or -1 if not found */ int getItemAlbum(qlonglong imageID); /** * Retrieve the name of the item * @param imageID The ID of the item * @return The name of the item, or a null string if not found */ QString getItemName(qlonglong imageID); /** * Get item and album info from the image ID */ ItemShortInfo getItemShortInfo(qlonglong imageID); /** * Get item and album if from albumRootId, album path and file name. */ ItemShortInfo getItemShortInfo(int albumRootId, const QString& relativePath, const QString& name); /** * Get scan info from the image ID */ ItemScanInfo getItemScanInfo(qlonglong imageID); /** * Update the fields of the Images table that have changed when * the file has been modified on disk. * @param imageID the image that has been modified */ void updateItem(qlonglong imageID, DatabaseItem::Category category, const QDateTime& modificationDate, qlonglong fileSize, const QString& uniqueHash); /** * Updates the status field for the item. * Note: Do not use this to set to the Removed status, see removeItems(). */ void setItemStatus(qlonglong imageID, DatabaseItem::Status status); /** * Updates the album field for the item. * Note: Do not use this to move the item. This function only has the purpose to * reuse image infos for restored images from trash. */ void setItemAlbum(qlonglong imageID, qlonglong albumId); + /** + * Rename the item. + * Note: we not use here ImageChangeset. + */ + void renameItem(qlonglong imageID, const QString& newName); + /** * Returns the requested fields from the Images table. * Choose the fields with the mask. * The fields will be returned in the following order and type: * 0) Int Album * 1) String Name * 2) Int Status * 3) Int Category * 4) DateTime ModificationDate * 5) int FileSize * 6) String uniqueHash */ QVariantList getImagesFields(qlonglong imageID, DatabaseFields::Images imagesFields); /** * Add (or replace) the ImageInformation of the specified item. * If there is already an entry, it will be discarded. * The QVariantList shall have 9 entries, of types in this order: * 0) Int rating * 1) DateTime* creationDate * 2) DateTime* digitizationDate * 3) Int orientation * 4) Int width * 5) Int height * 6) String format * 7) Int colorDepth * 8) Int colorModel * ( (*) You can provide the date also as a string in the format Qt::IsoDate) * You can leave out entries from this list, which will then be filled with null values. * Indicate the values that you have passed in the ImageInformation flag in the third parameters. */ void addImageInformation(qlonglong imageID, const QVariantList& infos, DatabaseFields::ImageInformation fields = DatabaseFields::ImageInformationAll); /** * Change the indicated fields of the image information for the specified item. * Fields not indicated by the fields parameter will not be touched. * This method does nothing if the item does not yet have an entry in the ImageInformation table. * The parameters are as for the method above. */ void changeImageInformation(qlonglong imageID, const QVariantList& infos, DatabaseFields::ImageInformation fields = DatabaseFields::ImageInformationAll); /** * Change the indicated fields of the images for the specified item. * Fields not indicated by the fields parameter will not be touched. * This method does nothing if the item does not yet have an entry in the Images table. * The parameters are as for the method above. */ void changeImages(qlonglong imageID, const QVariantList& infos, DatabaseFields::Images fields = DatabaseFields::ImagesAll); /** * Read image information. Parameters as above. */ QVariantList getImageInformation(qlonglong imageID, DatabaseFields::ImageInformation infoFields = DatabaseFields::ImageInformationAll); /** * Add (or replace) the ImageMetadata of the specified item. * If there is already an entry, it will be discarded. * The QVariantList shall have at most 16 entries, of types as defined * in the DBSCHEMA and in metadatainfo.h, in this order: * 0) String make * 1) String model * 2) String lens * 3) Double aperture * 4) Double focalLength * 5) Double focalLength35 * 6) Double exposureTime * 7) Int exposureProgram * 8) Int exposureMode * 9) Int sensitivity * 10) Int flash * 11) Int WhiteBalance * 12) Int WhiteBalanceColorTemperature * 13) Int meteringMode * 14) Double subjectDistance * 15) Double subjectDistanceCategory * You can leave out entries from this list. Indicate the values that you have * passed in the ImageMetadata flag in the third parameters. */ void addImageMetadata(qlonglong imageID, const QVariantList& infos, DatabaseFields::ImageMetadata fields = DatabaseFields::ImageMetadataAll); /** * Change the indicated fields of the image information for the specified item. * This method does nothing if the item does not yet have an entry in the ImageInformation table. * The parameters are as for the method above. */ void changeImageMetadata(qlonglong imageID, const QVariantList& infos, DatabaseFields::ImageMetadata fields = DatabaseFields::ImageMetadataAll); /** * Read image metadata. Parameters as above. */ QVariantList getImageMetadata(qlonglong imageID, DatabaseFields::ImageMetadata metadataFields = DatabaseFields::ImageMetadataAll); /** * Add (or replace) the VideoMetadata of the specified item. * If there is already an entry, it will be discarded. * The QVariantList shall have 8 entries, of types in this order: * 0) String AspectRatio * 1) String AudioBitRate * 2) String AudioChannelType * 3) String AudioCodec * 4) String Duration * 5) String FrameRate * 6) String VideoCodec * You can leave out entries from this list, which will then be filled with null values. * Indicate the values that you have passed in the VideoMetadata flag in the third parameters. */ void addVideoMetadata(qlonglong imageID, const QVariantList& infos, DatabaseFields::VideoMetadata fields = DatabaseFields::VideoMetadataAll); /** * Change the indicated fields of the video information for the specified item. * This method does nothing if the item does not yet have an entry in the ImageInformation table. * The parameters are as for the method above. */ void changeVideoMetadata(qlonglong imageID, const QVariantList& infos, DatabaseFields::VideoMetadata fields = DatabaseFields::VideoMetadataAll); /** * Read video metadata. Parameters as above. */ QVariantList getVideoMetadata(qlonglong imageID, DatabaseFields::VideoMetadata metadataFields = DatabaseFields::VideoMetadataAll); /** * Add (or replace) the ImagePosition of the specified item. * If there is already an entry, it will be discarded. * The QVariantList shall have at most 10 entries, of types in this order: * 0) String Latitude * 1) Double LatitudeNumber * 2) String Longitude * 3) Double LongitudeNumber * 4) Double Altitude * 5) Double Orientation * 6) Double Tilt * 7) Double Roll * 8) Double Accuracy * 9) String Description * You can leave out entries from this list. Indicate the values that you have * passed in the ImageInfo flag in the third parameters. */ void addImagePosition(qlonglong imageID, const QVariantList& infos, DatabaseFields::ImagePositions fields = DatabaseFields::ImagePositionsAll); /** * Change the indicated fields of the image information for the specified item. * This method does nothing if the item does not yet have an entry in the ImageInformation table. * The parameters are as for the method above. */ void changeImagePosition(qlonglong imageID, const QVariantList& infos, DatabaseFields::ImagePositions fields = DatabaseFields::ImagePositionsAll); /** * Read image metadata. Parameters as above. */ QVariantList getImagePosition(qlonglong imageID, DatabaseFields::ImagePositions positionFields = DatabaseFields::ImagePositionsAll); QVariantList getImagePositions(QList imageIDs, DatabaseFields::ImagePositions fields); /** * Remove the entry in ImagePositions for the given image */ void removeImagePosition(qlonglong imageid); /** * Remove the altitude in ImagePositions for the given image */ void removeImagePositionAltitude(qlonglong imageid); /** * Retrieves all available comments for the specified item. */ QList getImageComments(qlonglong imageID); /** * Sets the comments for the image. A comment for the image with the same * source, language and author will be overwritten. * @param imageID The imageID of the image * @param comment The comment string * @param type The type of the comment * @param language Information about the language of the comment. A null string shall be used * if language information is not available from the source, or if * the comment is in the default language. * @param author Optional information about the author who wrote the comment. * If not supported by the source, pass a null string. * @param date Optional information about the date when the comment was written * If not supported by the source, pass a null string. * @returns the comment ID of the comment */ int setImageComment(qlonglong imageID, const QString& comment, DatabaseComment::Type type, const QString& language = QString(), const QString& author = QString(), const QDateTime& date = QDateTime()); /** * Changes the properties of a comment. * The QVariantList shall have at most 5 entries, of types in this order: * 0) Int Type * 1) String Language * 2) String Author * 3) DateTime Date * 4) String Comment */ void changeImageComment(int commentId, qlonglong imageID, const QVariantList& infos, DatabaseFields::ImageComments fields = DatabaseFields::ImageCommentsAll); /** * Remove the specified entry in ImageComments */ void removeImageComment(int commentId, qlonglong imageid); /** * Returns the property with the specified name for the specified image */ QString getImageProperty(qlonglong imageID, const QString& property); /** * Sets the property with the given name for the given image to the specified value */ void setImageProperty(qlonglong imageID, const QString& property, const QString& value); void removeImageProperty(qlonglong imageID, const QString& property); void removeImagePropertyByName(const QString& property); /** * Returns the copyright properties of the specified image. * If property is not null, only the given property is returned. */ QList getImageCopyright(qlonglong imageID, const QString& property = QString()); enum CopyrightPropertyUnique { PropertyUnique, PropertyExtraValueUnique, PropertyNoConstraint }; /** * Sets the property with the given name for the given image to the specified value and extraValue */ void setImageCopyrightProperty(qlonglong imageID, const QString& property, const QString& value, const QString& extraValue = QString(), CopyrightPropertyUnique uniqueness = PropertyUnique); /** Removes copyright properties for the given image id. All values after the first null value, * in order of parameters, are treated as wild cards (you can give value as wildcard; value and * extraValue; or property, extraValue and value). * Note that extraValue is ordered before value in this method! */ void removeImageCopyrightProperties(qlonglong imageID, const QString& property = QString(), const QString& extraValue = QString(), const QString& value = QString() /* NOTE parameter order */); /** * Returns all items with the given file name and creation date. */ QList findByNameAndCreationDate(const QString& fileName, const QDateTime& creationDate); /** * Retrieves the history entry for the given image. */ ImageHistoryEntry getImageHistory(qlonglong imageId); /** * Retrieves the image UUID */ QString getImageUuid(qlonglong imageId); /** * Retrieves the images with the given UUID */ QList getItemsForUuid(const QString& uuid); /** * Changes (adds or updates) the image history */ void setImageHistory(qlonglong imageId, const QString& history); void setImageUuid(qlonglong imageId, const QString& uuid); /** * Returns true if the image has a history stored in DB * If not, it returns false */ bool hasImageHistory(qlonglong imageId); /** * Adds an image relation entry. */ void addImageRelation(qlonglong subjectId, qlonglong objectId, DatabaseRelation::Type type); void addImageRelation(const ImageRelation& relation); /// This method requires two lists of same size and will add list1[0]->list2[0],...,list1[n]->list2[n] void addImageRelations(const QList& subjectIds, const QList& objectIds, DatabaseRelation::Type type); /** * Removes image relations. * The batch methods return all removed partners. */ void removeImageRelation(qlonglong subjectId, qlonglong objectId, DatabaseRelation::Type type); void removeImageRelation(const ImageRelation& relation); QList removeAllImageRelationsTo(qlonglong objectId, DatabaseRelation::Type type); QList removeAllImageRelationsFrom(qlonglong subjectId, DatabaseRelation::Type type); /** * Retrieves all images that the given image is related to (retrieves objects, given image is subject) * If type is given, filters by type, otherwise returns all types. * "Get images related to from this" */ QList getImagesRelatedFrom(qlonglong subjectId, DatabaseRelation::Type type = DatabaseRelation::UndefinedType); bool hasImagesRelatedFrom(qlonglong subjectId, DatabaseRelation::Type type = DatabaseRelation::UndefinedType); QVector > getImagesRelatedFrom(QList subjectIds, DatabaseRelation::Type type = DatabaseRelation::UndefinedType); /** * Retrieves all images that relate to the given image (retrieves subject, given image is object) * If type is given, filters by type, otherwise returns all types. * "Get images this image is relating to" */ QList getImagesRelatingTo(qlonglong objectId, DatabaseRelation::Type type = DatabaseRelation::UndefinedType); bool hasImagesRelatingTo(qlonglong objectId, DatabaseRelation::Type type = DatabaseRelation::UndefinedType); QVector > getImagesRelatingTo(QList objectIds, DatabaseRelation::Type type = DatabaseRelation::UndefinedType); /** * For the given image id, retrieves all relations of all related images: * Each pair (a,b) means "a is related to b". * Each a and b in the list will have a direct or indirect relation to the initial imageId. * If type is given, filters by type, otherwise returns all types. */ QList > getRelationCloud(qlonglong imageId, DatabaseRelation::Type type = DatabaseRelation::UndefinedType); /** * For each of the given ids, find one single related image (direction does not matter). * Ids are unique in the returned list, and do not correspond by index to the given list. */ QList getOneRelatedImageEach(const QList& ids, DatabaseRelation::Type type = DatabaseRelation::UndefinedType); /** * Returns a list of all images where the Faces have either not been detected * yet, or is outdated because the file is identified as changed since * the generation of the fingerprint. * Return image ids or item URLs. */ QStringList getDirtyOrMissingFaceImageUrls(); /** * Find items that are, with reasonable certainty, identical * to the file pointed to by id. * Criteria: Unique Hash, file size and album non-null. * The first variant will not return an ItemScanInfo for id. * The second allows to pass one id as source id for exclusion from the list. * If this is -1, no id is excluded. */ QList getIdenticalFiles(qlonglong id); QList getIdenticalFiles(const QString& uniqueHash, qlonglong fileSize, qlonglong sourceId = -1); /** * Returns a list of all images where tagId is assigned * Return item URLs. */ QStringList getItemsURLsWithTag(int tagId); // ----------- Items and their tags ----------- /** * Add a tag for the item * @param imageID the ID of the item * @param tagID the tagID for the tag */ void addItemTag(qlonglong imageID, int tagID); /** * Add a tag for the item * @param albumID the albumID of the item * @param name the name of the item * @param tagID the tagID for the tag */ void addItemTag(int albumID, const QString& name, int tagID); /** * Add each tag of a list of tags * to each member of a list of items. */ void addTagsToItems(QList imageIDs, QList tagIDs); /** * Remove a specific tag for the item * @param imageID the ID of the item * @param tagID the tagID for the tag */ void removeItemTag(qlonglong imageID, int tagID); /** * Remove all tags for the item * @param imageID the ID of the item * @param currentTagIds the current tags ids assigned to the item */ void removeItemAllTags(qlonglong imageID, const QList& currentTagIds); /** * Remove each tag from a list of tags * from a each member of a list of items. */ void removeTagsFromItems(QList imageIDs, const QList& tagIDs); /** * Get a list of names of all the tags for the item * @param imageID the ID of the item * @return the list of names of all tags for the item */ QStringList getItemTagNames(qlonglong imageID); /** * Get a list of IDs of all the tags for the item * @param imageID the ID of the item * @return the list of IDs of all tags for the item */ QList getItemTagIDs(qlonglong imageID); /** * For a list of items, return the tag ids associated with the item. * Amounts to calling getItemTagIDs for each id in imageIds, but is optimized. */ QVector > getItemsTagIDs(const QList imageIds); /** * Get the properties for the given image/tag pair. * If the tagID is -1, returns the ImageTagProperties for all tagIds of the given image. */ QList getImageTagProperties(qlonglong imageId, int tagId = -1); /** * Get all tagIds for which ImageTagProperties exist for the given image. */ QList getTagIdsWithProperties(qlonglong imageId); /** * Adds a tag property. Note that this never replaces existing entries. * It is also all right to add multiple entries for a tag with the same property. * To replace an existing entry, remove the entry before. */ void addImageTagProperty(qlonglong imageId, int tagId, const QString& property, const QString& value); void addImageTagProperty(const ImageTagProperty& property); /** * Removes properties for the given tag. If the value is given, removes only * the entries with the given property/value pair. If only property is given, * removes all properties with the given name. If property is null, * removes all properties for the given tag. * If tagId is -1, removes all image tag properties for the given image. * Note: After the first parameter you give as a wildcard, the following will be ignored and taken as wildcard as well. */ void removeImageTagProperties(qlonglong imageId, int tagId = -1, const QString& property = QString(), const QString& value = QString()); /** * Given a set of items (identified by their IDs), * this will see if any of the items has a tag. * @param imageIDList a list of IDs of the items * @return true if at least one of the items has a tag */ bool hasTags(const QList& imageIDList); /** * Given a set of items (identified by their IDs), * get a list of ID of all common tags * @param imageIDList a list of IDs of the items * @return the list of common IDs of the given items */ QList getItemCommonTagIDs(const QList& imageIDList); /** * Returns a QMap of tag id -> count of items * with the tag */ QMap getNumberOfImagesInTags(); /** * Returns a QMap of tag id -> count of items * with the given tag property */ QMap getNumberOfImagesInTagProperties(const QString& property); /** * Returns the count of images that have a tag property for the given tag. */ int getNumberOfImagesInTagProperties(int tagId, const QString& property); /** * Returns all image ids that are associated to the tag with the given property. */ QList getImagesWithImageTagProperty(int tagId, const QString& property); /** * Returns a QMap of ImageInformation.format * -> count of items with that format. */ QMap getFormatStatistics(); QMap getFormatStatistics(DatabaseItem::Category category); /** * Return a list from a field from imageMetadata */ QStringList getListFromImageMetadata(DatabaseFields::ImageMetadata field); // ----------- Moving and Copying Items ----------- /** * Move the attributes of an item to a different item. Useful when * say a file is renamed * @param srcAlbumID the id of the source album * @param dstAlbumID the id of the destination album * @param srcName the name of the source file * @param dstName the name of the destination file */ void moveItem(int srcAlbumID, const QString& srcName, int dstAlbumID, const QString& dstName); /** * Copy the attributes of an item to a different item. Useful when * say a file is copied. * The operation fails (returns -1) of src and dest are identical. * @param srcAlbumID the id of the source album * @param dstAlbumID the id of the destination album * @param srcName the name of the source file * @param dstName the name of the destination file * @return the id of item added or -1 if it fails */ int copyItem(int srcAlbumID, const QString& srcName, int dstAlbumID, const QString& dstName); /** * Copies all image-specific information, in all tables, from image srcId to destId. */ void copyImageAttributes(qlonglong srcId, qlonglong destId); // Copies all entries in the ImageProperties table void copyImageProperties(qlonglong srcId, qlonglong dstId); // Copies all entries in the ImageTags table void copyImageTags(qlonglong srcId, qlonglong dstId); // ------------ Clear all Item Metadata ----------- /** * Clear all metadata of an item * @param imageID the ID of the item */ void clearMetadataFromImage(qlonglong imageID); // ----------- Download history methods ----------- /** * Search for the specified fingerprint in the download history table. * Returns the id of the entry, or -1 if not found. */ int findInDownloadHistory(const QString& identifier, const QString& name, qlonglong fileSize, const QDateTime& date); /** * Add the specified fingerprint to the download history table. * Returns the id of the entry. */ int addToDownloadHistory(const QString& identifier, const QString& name, qlonglong fileSize, const QDateTime& date); QList getImageIdsFromArea(qreal lat1, qreal lat2, qreal lng1, qreal lng2, int sortMode, const QString& sortBy); // ----------- Database shrinking methods ---------- /** * Returns true if the integrity of the database is preserved. */ bool integrityCheck(); /** * Shrinks the database. */ void vacuum(); // ----------- Static helper methods for constructing SQL queries ----------- static QStringList imagesFieldList(DatabaseFields::Images fields); static QStringList imageInformationFieldList(DatabaseFields::ImageInformation fields); static QStringList videoMetadataFieldList(DatabaseFields::VideoMetadata fields); static QStringList imageMetadataFieldList(DatabaseFields::ImageMetadata fields); static QStringList imagePositionsFieldList(DatabaseFields::ImagePositions fields); static QStringList imageCommentsFieldList(DatabaseFields::ImageComments fields); static void addBoundValuePlaceholders(QString& query, int count); public: friend class Digikam::CoreDbAccess; /** * Constructor */ explicit CoreDB(CoreDbBackend* const backend); /** * Destructor */ ~CoreDB(); protected: QList getRelatedImages(qlonglong id, bool fromOrTo, DatabaseRelation::Type type, bool boolean); QVector > getRelatedImages(QList ids, bool fromOrTo, DatabaseRelation::Type type, bool boolean); private: void readSettings(); void writeSettings(); private: class Private; Private* const d; }; } // namespace Digikam #endif // DIGIKAM_CORE_DB_H diff --git a/core/libs/database/dbjobs/dbjob.h b/core/libs/database/dbjobs/dbjob.h index 0a961259e2..c0b94f9ae0 100644 --- a/core/libs/database/dbjobs/dbjob.h +++ b/core/libs/database/dbjobs/dbjob.h @@ -1,182 +1,182 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2015-06-01 * Description : DB Jobs for listing and scanning * * Copyright (C) 2015 by Mohamed_Anwer * * 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, 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. * * ============================================================ */ #ifndef DIGIKAM_DB_JOB_H #define DIGIKAM_DB_JOB_H // Local includes #include "dbjobinfo.h" #include "dbjobsthread.h" #include "imagelisterrecord.h" #include "duplicatesprogressobserver.h" #include "actionthreadbase.h" #include "digikam_export.h" namespace Digikam { class DuplicatesProgressObserver; class DIGIKAM_DATABASE_EXPORT DBJob : public ActionJob { Q_OBJECT protected: explicit DBJob(); ~DBJob(); Q_SIGNALS: void data(const QList& records); void error(const QString& err); }; // ---------------------------------------------- class DIGIKAM_DATABASE_EXPORT AlbumsJob : public DBJob { Q_OBJECT public: explicit AlbumsJob(const AlbumsDBJobInfo& jobInfo); ~AlbumsJob(); protected: void run(); Q_SIGNALS: void foldersData(const QMap&); private: AlbumsDBJobInfo m_jobInfo; }; // ---------------------------------------------- class DIGIKAM_DATABASE_EXPORT DatesJob : public DBJob { Q_OBJECT public: explicit DatesJob(const DatesDBJobInfo& jobInfo); ~DatesJob(); protected: void run(); Q_SIGNALS: void foldersData(const QMap& datesStatMap); private: DatesDBJobInfo m_jobInfo; }; // ---------------------------------------------- class DIGIKAM_DATABASE_EXPORT GPSJob : public DBJob { Q_OBJECT public: explicit GPSJob(const GPSDBJobInfo& jobInfo); ~GPSJob(); protected: void run(); Q_SIGNALS: void directQueryData(const QList& data); private: GPSDBJobInfo m_jobInfo; }; // ---------------------------------------------- class DIGIKAM_DATABASE_EXPORT TagsJob : public DBJob { Q_OBJECT public: explicit TagsJob(const TagsDBJobInfo& jobInfo); ~TagsJob(); protected: void run(); Q_SIGNALS: - void foldersData(const QMap & data); + void foldersData(const QMap& data); void faceFoldersData(const QMap >& data); private: TagsDBJobInfo m_jobInfo; }; // ---------------------------------------------- class DIGIKAM_DATABASE_EXPORT SearchesJob : public DBJob { Q_OBJECT public: explicit SearchesJob(const SearchesDBJobInfo& jobInfo); ~SearchesJob(); bool isCanceled(); Q_SIGNALS: void processedSize(int); void totalSize(int); protected: void run(); private: SearchesDBJobInfo m_jobInfo; }; } // namespace Digikam #endif // DIGIKAM_DB_JOB_H diff --git a/core/libs/database/item/imageinfo.cpp b/core/libs/database/item/imageinfo.cpp index cd658e7e94..35f421f94f 100644 --- a/core/libs/database/item/imageinfo.cpp +++ b/core/libs/database/item/imageinfo.cpp @@ -1,1978 +1,1992 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2005-04-21 * Description : Handling accesss to one image and associated data * * Copyright (C) 2005 by Renchi Raju * Copyright (C) 2007-2013 by Marcel Wiesweg * Copyright (C) 2009-2018 by Gilles Caulier * Copyright (C) 2013 by Michael G. Hansen * * 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, 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. * * ============================================================ */ #include "imageinfo.h" // Qt includes #include #include #include // Local includes #include "digikam_debug.h" #include "digikam_globals.h" #include "coredb.h" #include "coredbaccess.h" #include "coredbinfocontainers.h" #include "coredboperationgroup.h" #include "dimagehistory.h" #include "collectionmanager.h" #include "collectionlocation.h" #include "imageinfodata.h" #include "imageinfocache.h" #include "imagelister.h" #include "imagelisterrecord.h" #include "imageinfolist.h" #include "imagecomments.h" #include "imagecopyright.h" #include "imageextendedproperties.h" #include "imageposition.h" #include "imagescanner.h" #include "imagetagpair.h" #include "tagscache.h" #include "template.h" #include "thumbnailinfo.h" #include "infocontainer.h" namespace Digikam { namespace { MetadataInfo::Field DatabaseVideoMetadataFieldsToMetadataInfoField(const DatabaseFields::VideoMetadata videoMetadataField) { switch (videoMetadataField) { case DatabaseFields::AspectRatio: return MetadataInfo::AspectRatio; case DatabaseFields::AudioBitRate: return MetadataInfo::AudioBitRate; case DatabaseFields::AudioChannelType: return MetadataInfo::AudioChannelType; case DatabaseFields::AudioCodec: return MetadataInfo::AudioCodec; case DatabaseFields::Duration: return MetadataInfo::Duration; case DatabaseFields::FrameRate: return MetadataInfo::FrameRate; case DatabaseFields::VideoCodec: return MetadataInfo::VideoCodec; default: break; } /// @todo Invalid request... return MetadataInfo::Field(); } MetadataInfo::Field DatabaseImageMetadataFieldsToMetadataInfoField(const DatabaseFields::ImageMetadata imageMetadataField) { switch (imageMetadataField) { case DatabaseFields::Make: return MetadataInfo::Make; case DatabaseFields::Model: return MetadataInfo::Model; case DatabaseFields::Lens: return MetadataInfo::Lens; case DatabaseFields::Aperture: return MetadataInfo::Aperture; case DatabaseFields::FocalLength: return MetadataInfo::FocalLength; case DatabaseFields::FocalLength35: return MetadataInfo::FocalLengthIn35mm; case DatabaseFields::ExposureTime: return MetadataInfo::ExposureTime; case DatabaseFields::ExposureProgram: return MetadataInfo::ExposureProgram; case DatabaseFields::ExposureMode: return MetadataInfo::ExposureMode; case DatabaseFields::Sensitivity: return MetadataInfo::Sensitivity; case DatabaseFields::FlashMode: return MetadataInfo::FlashMode; case DatabaseFields::WhiteBalance: return MetadataInfo::WhiteBalance; case DatabaseFields::WhiteBalanceColorTemperature: return MetadataInfo::WhiteBalanceColorTemperature; case DatabaseFields::MeteringMode: return MetadataInfo::MeteringMode; case DatabaseFields::SubjectDistance: return MetadataInfo::SubjectDistance; case DatabaseFields::SubjectDistanceCategory: return MetadataInfo::SubjectDistanceCategory; default: break; } /// @todo Invalid request... return MetadataInfo::Field(); } } ImageInfoStatic* ImageInfoStatic::m_instance = 0; void ImageInfoStatic::create() { if (!m_instance) { m_instance = new ImageInfoStatic; } } void ImageInfoStatic::destroy() { delete m_instance; m_instance = 0; } ImageInfoCache* ImageInfoStatic::cache() { return &m_instance->m_cache; } // --------------------------------------------------------------- ImageInfoData::ImageInfoData() { id = -1; currentReferenceImage = -1; albumId = -1; albumRootId = -1; pickLabel = NoPickLabel; colorLabel = NoColorLabel; rating = -1; category = DatabaseItem::UndefinedCategory; fileSize = 0; longitude = 0; latitude = 0; altitude = 0; currentSimilarity = 0.0; hasCoordinates = false; hasAltitude = false; groupedImages = 0; groupImage = -1; defaultTitleCached = false; defaultCommentCached = false; pickLabelCached = false; colorLabelCached = false; ratingCached = false; categoryCached = false; formatCached = false; creationDateCached = false; modificationDateCached = false; fileSizeCached = false; imageSizeCached = false; tagIdsCached = false; positionsCached = false; groupedImagesCached = false; groupImageCached = false; uniqueHashCached = false; invalid = false; videoMetadataCached = DatabaseFields::VideoMetadataNone; imageMetadataCached = DatabaseFields::ImageMetadataNone; hasVideoMetadata = true; hasImageMetadata = true; } ImageInfoData::~ImageInfoData() { } // --------------------------------------------------------------- ImageInfo::ImageInfo() : m_data(0) { } ImageInfo::ImageInfo(const ImageListerRecord& record) { m_data = ImageInfoStatic::cache()->infoForId(record.imageID); ImageInfoWriteLocker lock; bool newlyCreated = m_data->albumId == -1; m_data->albumId = record.albumID; m_data->albumRootId = record.albumRootID; m_data->name = record.name; m_data->rating = record.rating; m_data->category = record.category; m_data->format = record.format; m_data->creationDate = record.creationDate; m_data->modificationDate = record.modificationDate; m_data->fileSize = record.fileSize; m_data->imageSize = record.imageSize; m_data->currentSimilarity = record.currentSimilarity; m_data->currentReferenceImage = record.currentFuzzySearchReferenceImage; m_data->ratingCached = true; m_data->categoryCached = true; m_data->formatCached = true; m_data->creationDateCached = true; m_data->modificationDateCached = true; // field is only signed 32 bit in the protocol. -1 indicates value is larger, reread m_data->fileSizeCached = m_data->fileSize != -1; m_data->imageSizeCached = true; m_data->videoMetadataCached = DatabaseFields::VideoMetadataNone; m_data->imageMetadataCached = DatabaseFields::ImageMetadataNone; m_data->hasVideoMetadata = true; m_data->hasImageMetadata = true; m_data->databaseFieldsHashRaw.clear(); if (newlyCreated) { ImageInfoStatic::cache()->cacheByName(m_data); } } ImageInfo::ImageInfo(qlonglong ID) { m_data = ImageInfoStatic::cache()->infoForId(ID); // is this a newly created structure, need to populate? if (m_data->albumId == -1) { // retrieve immutable values now, the rest on demand ItemShortInfo info = CoreDbAccess().db()->getItemShortInfo(ID); if (info.id) { ImageInfoWriteLocker lock; m_data->albumId = info.albumID; m_data->albumRootId = info.albumRootID; m_data->name = info.itemName; ImageInfoStatic::cache()->cacheByName(m_data); } else { // invalid image id ImageInfoData* const olddata = m_data.unassign(); if (olddata) { ImageInfoStatic::cache()->dropInfo(olddata); } m_data = 0; } } } ImageInfo ImageInfo::fromUrl(const QUrl& url) { return fromLocalFile(url.toLocalFile()); } ImageInfo ImageInfo::fromLocalFile(const QString& path) { CollectionLocation location = CollectionManager::instance()->locationForPath(path); if (location.isNull()) { qCWarning(DIGIKAM_DATABASE_LOG) << "No location could be retrieved for" << path; return ImageInfo(); } QUrl url = QUrl::fromLocalFile(path); QString album = CollectionManager::instance()->album(url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).toLocalFile()); QString name = url.fileName(); return fromLocationAlbumAndName(location.id(), album, name); } ImageInfo ImageInfo::fromLocationAlbumAndName(int locationId, const QString& album, const QString& name) { if (!locationId || album.isEmpty() || name.isEmpty()) { return ImageInfo(); } ImageInfo info; // Cached ? info.m_data = ImageInfoStatic::cache()->infoForPath(locationId, album, name); if (!info.m_data) { ItemShortInfo shortInfo = CoreDbAccess().db()->getItemShortInfo(locationId, album, name); if (!shortInfo.id) { qCWarning(DIGIKAM_DATABASE_LOG) << "No itemShortInfo could be retrieved from the database for image" << name; info.m_data = 0; return info; } info.m_data = ImageInfoStatic::cache()->infoForId(shortInfo.id); ImageInfoWriteLocker lock; info.m_data->albumId = shortInfo.albumID; info.m_data->albumRootId = shortInfo.albumRootID; info.m_data->name = shortInfo.itemName; ImageInfoStatic::cache()->cacheByName(info.m_data); } return info; } ImageInfo::~ImageInfo() { ImageInfoData* const olddata = m_data.unassign(); if (olddata) { ImageInfoStatic::cache()->dropInfo(olddata); } } ImageInfo::ImageInfo(const ImageInfo& info) { m_data = info.m_data; } ImageInfo& ImageInfo::operator=(const ImageInfo& info) { if (m_data == info.m_data) { return *this; } ImageInfoData* const olddata = m_data.assign(info.m_data); if (olddata) { ImageInfoStatic::cache()->dropInfo(olddata); } return *this; } bool ImageInfo::isNull() const { return !m_data; } bool ImageInfo::operator==(const ImageInfo& info) const { if (m_data && info.m_data) { // not null, compare id return m_data->id == info.m_data->id; } else { // both null? return m_data == info.m_data; } } bool ImageInfo::operator<(const ImageInfo& info) const { if (m_data) { if (info.m_data) // both not null, sort by id { return m_data->id < info.m_data->id; } else // only other is null, this is greater than { return false; } } else { // this is less than if the other is not null return info.m_data; } } uint ImageInfo::hash() const { if (m_data) { return ::qHash(m_data->id); } else { return ::qHash((int)0); } } /** * Access rules for all methods in this class: * ImageInfoData members shall be accessed only under CoreDbAccess lock. * The id and albumId are the exception to this rule, as they are * primitive and will never change during the lifetime of an object. */ qlonglong ImageInfo::id() const { return m_data ? m_data->id : -1; } int ImageInfo::albumId() const { return m_data ? m_data->albumId : -1; } int ImageInfo::albumRootId() const { return m_data ? m_data->albumRootId : -1; } QString ImageInfo::name() const { if (!m_data) { return QString(); } ImageInfoReadLocker lock; return m_data->name; } #define RETURN_IF_CACHED(x) \ if (m_data->x##Cached) \ { \ ImageInfoReadLocker lock; \ if (m_data->x##Cached) \ { \ return m_data->x; \ } \ } #define RETURN_ASPECTRATIO_IF_IMAGESIZE_CACHED() \ if (m_data->imageSizeCached) \ { \ ImageInfoReadLocker lock; \ if (m_data->imageSizeCached) \ { \ return (double)m_data->imageSize.width()/m_data->imageSize.height(); \ } \ } #define STORE_IN_CACHE_AND_RETURN(x, retrieveMethod) \ ImageInfoWriteLocker lock; \ m_data.constCastData()->x##Cached = true; \ if (!values.isEmpty()) \ { \ m_data.constCastData()->x = retrieveMethod; \ } \ return m_data->x; qlonglong ImageInfo::fileSize() const { if (!m_data) { return 0; } RETURN_IF_CACHED(fileSize) QVariantList values = CoreDbAccess().db()->getImagesFields(m_data->id, DatabaseFields::FileSize); STORE_IN_CACHE_AND_RETURN(fileSize, values.first().toLongLong()) } QString ImageInfo::uniqueHash() const { if (!m_data) { return QString(); } RETURN_IF_CACHED(uniqueHash) QVariantList values = CoreDbAccess().db()->getImagesFields(m_data->id, DatabaseFields::UniqueHash); STORE_IN_CACHE_AND_RETURN(uniqueHash, values.first().toString()) } QString ImageInfo::title() const { if (!m_data) { return QString(); } RETURN_IF_CACHED(defaultTitle) QString title; { CoreDbAccess access; ImageComments comments(access, m_data->id); title = comments.defaultComment(DatabaseComment::Title); } ImageInfoWriteLocker lock; m_data.constCastData()->defaultTitle = title; m_data.constCastData()->defaultTitleCached = true; return m_data->defaultTitle; } QString ImageInfo::comment() const { if (!m_data) { return QString(); } RETURN_IF_CACHED(defaultComment) QString comment; { CoreDbAccess access; ImageComments comments(access, m_data->id); comment = comments.defaultComment(); } ImageInfoWriteLocker lock; m_data.constCastData()->defaultComment = comment; m_data.constCastData()->defaultCommentCached = true; return m_data->defaultComment; } double ImageInfo::aspectRatio() const { if (!m_data) { return 0; } RETURN_ASPECTRATIO_IF_IMAGESIZE_CACHED() return (double)m_data->imageSize.width() / m_data->imageSize.height(); } int ImageInfo::pickLabel() const { if (!m_data) { return NoPickLabel; } RETURN_IF_CACHED(pickLabel) int pickLabel = TagsCache::instance()->pickLabelFromTags(tagIds()); ImageInfoWriteLocker lock; m_data.constCastData()->pickLabel = (pickLabel == -1) ? NoPickLabel : pickLabel; m_data.constCastData()->pickLabelCached = true; return m_data->pickLabel; } int ImageInfo::colorLabel() const { if (!m_data) { return NoColorLabel; } RETURN_IF_CACHED(colorLabel) int colorLabel = TagsCache::instance()->colorLabelFromTags(tagIds()); ImageInfoWriteLocker lock; m_data.constCastData()->colorLabel = (colorLabel == -1) ? NoColorLabel : colorLabel; m_data.constCastData()->colorLabelCached = true; return m_data->colorLabel; } int ImageInfo::rating() const { if (!m_data) { return 0; } RETURN_IF_CACHED(rating) QVariantList values = CoreDbAccess().db()->getImageInformation(m_data->id, DatabaseFields::Rating); STORE_IN_CACHE_AND_RETURN(rating, values.first().toLongLong()) } QString ImageInfo::format() const { if (!m_data) { return QString(); } RETURN_IF_CACHED(format) QVariantList values = CoreDbAccess().db()->getImageInformation(m_data->id, DatabaseFields::Format); STORE_IN_CACHE_AND_RETURN(format, values.first().toString()) } DatabaseItem::Category ImageInfo::category() const { if (!m_data) { return DatabaseItem::UndefinedCategory; } RETURN_IF_CACHED(category) QVariantList values = CoreDbAccess().db()->getImagesFields(m_data->id, DatabaseFields::Category); STORE_IN_CACHE_AND_RETURN(category, (DatabaseItem::Category)values.first().toInt()) } QDateTime ImageInfo::dateTime() const { if (!m_data) { return QDateTime(); } RETURN_IF_CACHED(creationDate) QVariantList values = CoreDbAccess().db()->getImageInformation(m_data->id, DatabaseFields::CreationDate); STORE_IN_CACHE_AND_RETURN(creationDate, values.first().toDateTime()) } QDateTime ImageInfo::modDateTime() const { if (!m_data) { return QDateTime(); } RETURN_IF_CACHED(modificationDate) QVariantList values = CoreDbAccess().db()->getImagesFields(m_data->id, DatabaseFields::ModificationDate); STORE_IN_CACHE_AND_RETURN(modificationDate, values.first().toDateTime()) } QSize ImageInfo::dimensions() const { if (!m_data) { return QSize(); } RETURN_IF_CACHED(imageSize) QVariantList values = CoreDbAccess().db()->getImageInformation(m_data->id, DatabaseFields::Width | DatabaseFields::Height); ImageInfoWriteLocker lock; m_data.constCastData()->imageSizeCached = true; if (values.size() == 2) { m_data.constCastData()->imageSize = QSize(values.at(0).toInt(), values.at(1).toInt()); } return m_data->imageSize; } QList ImageInfo::tagIds() const { if (!m_data) { return QList(); } RETURN_IF_CACHED(tagIds) QList ids = CoreDbAccess().db()->getItemTagIDs(m_data->id); ImageInfoWriteLocker lock; m_data.constCastData()->tagIds = ids; m_data.constCastData()->tagIdsCached = true; return ids; } void ImageInfoList::loadTagIds() const { QVector > allTagIds = CoreDbAccess().db()->getItemsTagIDs(toImageIdList()); ImageInfoWriteLocker lock; for (int i = 0 ; i < size() ; i++) { const ImageInfo& info = at(i); const QList& ids = allTagIds.at(i); if (!info.m_data) { continue; } info.m_data.constCastData()->tagIds = ids; info.m_data.constCastData()->tagIdsCached = true; } } int ImageInfo::orientation() const { if (!m_data) { return 0; // ORIENTATION_UNSPECIFIED } QVariantList values = CoreDbAccess().db()->getImageInformation(m_data->id, DatabaseFields::Orientation); if (values.isEmpty()) { return 0; } return values.first().toInt(); } QUrl ImageInfo::fileUrl() const { return QUrl::fromLocalFile(filePath()); } QString ImageInfo::filePath() const { if (!m_data) { return QString(); } QString albumRoot = CollectionManager::instance()->albumRootPath(m_data->albumRootId); if (albumRoot.isNull()) { return QString(); } QString album = ImageInfoStatic::cache()->albumRelativePath(m_data->albumId); ImageInfoReadLocker lock; if (album == QLatin1String("/")) { return albumRoot + album + m_data->name; } else { return albumRoot + album + QLatin1Char('/') + m_data->name; } } bool ImageInfo::isVisible() const { if (!m_data) { return false; } QVariantList value = CoreDbAccess().db()->getImagesFields(m_data->id, DatabaseFields::Status); if (!value.isEmpty()) { return value.first().toInt() == DatabaseItem::Visible; } return false; } bool ImageInfo::isRemoved() const { if (!m_data) { return true; } QVariantList value = CoreDbAccess().db()->getImagesFields(m_data->id, DatabaseFields::Status); if (!value.isEmpty()) { return (value.first().toInt() == DatabaseItem::Trashed) || (value.first().toInt() == DatabaseItem::Obsolete); } return false; } void ImageInfo::setVisible(bool isVisible) { if (!m_data) { return; } if (m_data->albumId == 0) { qCWarning(DIGIKAM_DATABASE_LOG) << "Attempt to make a Removed item visible with ImageInfo::setVisible"; return; } CoreDbAccess().db()->setItemStatus(m_data->id, isVisible ? DatabaseItem::Visible : DatabaseItem::Hidden); } bool ImageInfo::hasDerivedImages() const { if (!m_data) { return false; } return CoreDbAccess().db()->hasImagesRelatingTo(m_data->id, DatabaseRelation::DerivedFrom); } bool ImageInfo::hasAncestorImages() const { if (!m_data) { return false; } return CoreDbAccess().db()->hasImagesRelatedFrom(m_data->id, DatabaseRelation::DerivedFrom); } QList ImageInfo::derivedImages() const { if (!m_data) { return QList(); } return ImageInfoList(CoreDbAccess().db()->getImagesRelatingTo(m_data->id, DatabaseRelation::DerivedFrom)); } QList ImageInfo::ancestorImages() const { if (!m_data) { return QList(); } return ImageInfoList(CoreDbAccess().db()->getImagesRelatedFrom(m_data->id, DatabaseRelation::DerivedFrom)); } QList > ImageInfo::relationCloud() const { if (!m_data) { return QList >(); } return CoreDbAccess().db()->getRelationCloud(m_data->id, DatabaseRelation::DerivedFrom); } void ImageInfo::markDerivedFrom(const ImageInfo& ancestor) { if (!m_data || ancestor.isNull()) { return; } CoreDbAccess().db()->addImageRelation(m_data->id, ancestor.id(), DatabaseRelation::DerivedFrom); } bool ImageInfo::hasGroupedImages() const { return numberOfGroupedImages(); } int ImageInfo::numberOfGroupedImages() const { if (!m_data) { return false; } RETURN_IF_CACHED(groupedImages) int groupedImages = CoreDbAccess().db()->getImagesRelatingTo(m_data->id, DatabaseRelation::Grouped).size(); ImageInfoWriteLocker lock; m_data.constCastData()->groupedImages = groupedImages; m_data.constCastData()->groupedImagesCached = true; return m_data->groupedImages; } qlonglong ImageInfo::groupImageId() const { if (!m_data) { return -1; } RETURN_IF_CACHED(groupImage) QList ids = CoreDbAccess().db()->getImagesRelatedFrom(m_data->id, DatabaseRelation::Grouped); // list size should be 0 or 1 int groupImage = ids.isEmpty() ? -1 : ids.first(); ImageInfoWriteLocker lock; m_data.constCastData()->groupImage = groupImage; m_data.constCastData()->groupImageCached = true; return m_data->groupImage; } void ImageInfoList::loadGroupImageIds() const { QVector > allGroupIds = CoreDbAccess().db()->getImagesRelatedFrom(toImageIdList(), DatabaseRelation::Grouped); ImageInfoWriteLocker lock; for (int i = 0 ; i < size() ; i++) { const ImageInfo& info = at(i); const QList& groupIds = allGroupIds.at(i); if (!info.m_data) { continue; } info.m_data.constCastData()->groupImage = groupIds.isEmpty() ? -1 : groupIds.first(); info.m_data.constCastData()->groupImageCached = true; } } bool ImageInfo::isGrouped() const { return groupImageId() != -1; } ImageInfo ImageInfo::groupImage() const { qlonglong id = groupImageId(); if (id == -1) { return ImageInfo(); } return ImageInfo(id); } QList ImageInfo::groupedImages() const { if (!m_data || !hasGroupedImages()) { return QList(); } return ImageInfoList(CoreDbAccess().db()->getImagesRelatingTo(m_data->id, DatabaseRelation::Grouped)); } void ImageInfo::addToGroup(const ImageInfo& givenLeader) { if (!m_data || givenLeader.isNull() || givenLeader.id() == m_data->id) { return; } // Take care: Once we start this, we cannot rely on change notifications and cache invalidation! CoreDbOperationGroup group; // Handle grouping on an already grouped image, and prevent circular grouping ImageInfo leader; QList alreadySeen; alreadySeen << m_data->id; for (leader = givenLeader ; leader.isGrouped() ;) { ImageInfo nextLeader = leader.groupImage(); // is the new leader currently grouped on this image, or do we have a circular grouping? if (alreadySeen.contains(nextLeader.id())) { // break loop (special case: remove b->a where we want to add a->b) leader.removeFromGroup(); break; } else { alreadySeen << leader.id(); leader = nextLeader; } } // Already grouped correctly? if (groupImageId() == leader.id()) { return; } // All images grouped on this image need a new group leader QList idsToBeGrouped = CoreDbAccess().db()->getImagesRelatingTo(m_data->id, DatabaseRelation::Grouped); // and finally, this image needs to be grouped idsToBeGrouped << m_data->id; foreach(const qlonglong& ids, idsToBeGrouped) { // remove current grouping CoreDbAccess().db()->removeAllImageRelationsFrom(ids, DatabaseRelation::Grouped); // add the new grouping CoreDbAccess().db()->addImageRelation(ids, leader.id(), DatabaseRelation::Grouped); } } void ImageInfo::removeFromGroup() { if (!m_data) { return; } if (!isGrouped()) { return; } CoreDbAccess().db()->removeAllImageRelationsFrom(m_data->id, DatabaseRelation::Grouped); } void ImageInfo::clearGroup() { if (!m_data) { return; } if (!hasGroupedImages()) { return; } CoreDbAccess().db()->removeAllImageRelationsTo(m_data->id, DatabaseRelation::Grouped); } ImageComments ImageInfo::imageComments(CoreDbAccess& access) const { if (!m_data) { return ImageComments(); } return ImageComments(access, m_data->id); } ImageCopyright ImageInfo::imageCopyright() const { if (!m_data) { return ImageCopyright(); } return ImageCopyright(m_data->id); } ImageExtendedProperties ImageInfo::imageExtendedProperties() const { if (!m_data) { return ImageExtendedProperties(); } return ImageExtendedProperties(m_data->id); } ImagePosition ImageInfo::imagePosition() const { if (!m_data) { return ImagePosition(); } ImagePosition pos(m_data->id); if (!m_data->positionsCached) { ImageInfoWriteLocker lock; m_data.constCastData()->longitude = pos.longitudeNumber(); m_data.constCastData()->latitude = pos.latitudeNumber(); m_data.constCastData()->altitude = pos.altitude(); m_data.constCastData()->hasCoordinates = pos.hasCoordinates(); m_data.constCastData()->hasAltitude = pos.hasAltitude(); m_data.constCastData()->positionsCached = true; } return pos; } double ImageInfo::longitudeNumber() const { if (!m_data) { return 0; } if (!m_data->positionsCached) { imagePosition(); } return m_data->longitude; } double ImageInfo::latitudeNumber() const { if (!m_data) { return 0; } if (!m_data->positionsCached) { imagePosition(); } return m_data->latitude; } double ImageInfo::altitudeNumber() const { if (!m_data) { return 0; } if (!m_data->positionsCached) { imagePosition(); } return m_data->altitude; } bool ImageInfo::hasCoordinates() const { if (!m_data) { return 0; } if (!m_data->positionsCached) { imagePosition(); } return m_data->hasCoordinates; } bool ImageInfo::hasAltitude() const { if (!m_data) { return 0; } if (!m_data->positionsCached) { imagePosition(); } return m_data->hasAltitude; } ImageTagPair ImageInfo::imageTagPair(int tagId) const { if (!m_data) { return ImageTagPair(); } return ImageTagPair(*this, tagId); } QList ImageInfo::availableImageTagPairs() const { if (!m_data) { return QList(); } return ImageTagPair::availablePairs(*this); } DImageHistory ImageInfo::imageHistory() const { if (!m_data) { return DImageHistory(); } ImageHistoryEntry entry = CoreDbAccess().db()->getImageHistory(m_data->id); return DImageHistory::fromXml(entry.history); } void ImageInfo::setImageHistory(const DImageHistory& history) { if (!m_data) { return; } CoreDbAccess().db()->setImageHistory(m_data->id, history.toXml()); } bool ImageInfo::hasImageHistory() const { if (!m_data) { return false; } return CoreDbAccess().db()->hasImageHistory(m_data->id); } QString ImageInfo::uuid() const { if (!m_data) { return QString(); } return CoreDbAccess().db()->getImageUuid(m_data->id); } void ImageInfo::setUuid(const QString& uuid) { if (!m_data) { return; } CoreDbAccess().db()->setImageUuid(m_data->id, uuid); } HistoryImageId ImageInfo::historyImageId() const { if (!m_data) { return HistoryImageId(); } HistoryImageId id(uuid()); id.setCreationDate(dateTime()); id.setFileName(name()); id.setPathOnDisk(filePath()); if (CoreDbAccess().db()->isUniqueHashV2()) { ItemScanInfo info = CoreDbAccess().db()->getItemScanInfo(m_data->id); id.setUniqueHash(info.uniqueHash, info.fileSize); } return id; } ImageCommonContainer ImageInfo::imageCommonContainer() const { if (!m_data) { return ImageCommonContainer(); } ImageCommonContainer container; ImageScanner::fillCommonContainer(m_data->id, &container); return container; } ImageMetadataContainer ImageInfo::imageMetadataContainer() const { if (!m_data) { return ImageMetadataContainer(); } ImageMetadataContainer container; const DatabaseFieldsHashRaw rawVideoMetadata = getDatabaseFieldsRaw(DatabaseFields::ImageMetadataAll); bool allFieldsNull = true; for (DatabaseFields::ImageMetadataIterator it ; !it.atEnd() ; ++it) { const QVariant fieldValue = rawVideoMetadata.value(*it); allFieldsNull &= fieldValue.isNull(); if (!fieldValue.isNull()) { const MetadataInfo::Field mdField = DatabaseImageMetadataFieldsToMetadataInfoField(*it); const QString fieldString = DMetadata::valueToString(fieldValue, mdField); switch (*it) { case DatabaseFields::Make: container.make = fieldString; break; case DatabaseFields::Model: container.model = fieldString; break; case DatabaseFields::Lens: container.lens = fieldString; break; case DatabaseFields::Aperture: container.aperture = fieldString; break; case DatabaseFields::FocalLength: container.focalLength = fieldString; break; case DatabaseFields::FocalLength35: container.focalLength35 = fieldString; break; case DatabaseFields::ExposureTime: container.exposureTime = fieldString; break; case DatabaseFields::ExposureProgram: container.exposureProgram = fieldString; break; case DatabaseFields::ExposureMode: container.exposureMode = fieldString; break; case DatabaseFields::Sensitivity: container.sensitivity = fieldString; break; case DatabaseFields::FlashMode: container.flashMode = fieldString; break; case DatabaseFields::WhiteBalance: container.whiteBalance = fieldString; break; case DatabaseFields::WhiteBalanceColorTemperature: container.whiteBalanceColorTemperature = fieldString; break; case DatabaseFields::SubjectDistance: container.subjectDistance = fieldString; break; case DatabaseFields::SubjectDistanceCategory: container.subjectDistanceCategory = fieldString; break; default: break; } } } // store whether we have at least one valid field container.allFieldsNull = allFieldsNull; return container; } VideoMetadataContainer ImageInfo::videoMetadataContainer() const { if (!m_data) { return VideoMetadataContainer(); } VideoMetadataContainer container; const DatabaseFieldsHashRaw rawVideoMetadata = getDatabaseFieldsRaw(DatabaseFields::VideoMetadataAll); bool allFieldsNull = true; for (DatabaseFields::VideoMetadataIterator it ; !it.atEnd() ; ++it) { const QVariant fieldValue = rawVideoMetadata.value(*it); allFieldsNull &= fieldValue.isNull(); if (!fieldValue.isNull()) { const MetadataInfo::Field mdField = DatabaseVideoMetadataFieldsToMetadataInfoField(*it); const QString fieldString = DMetadata::valueToString(fieldValue, mdField); switch (*it) { case DatabaseFields::AspectRatio: container.aspectRatio = fieldString; break; case DatabaseFields::AudioBitRate: container.audioBitRate = fieldString; break; case DatabaseFields::AudioChannelType: container.audioChannelType = fieldString; break; case DatabaseFields::AudioCodec: container.audioCodec = fieldString; break; case DatabaseFields::Duration: container.duration = fieldString; break; case DatabaseFields::FrameRate: container.frameRate = fieldString; break; case DatabaseFields::VideoCodec: container.videoCodec = fieldString; break; default: break; } } } // store whether we have at least one valid field container.allFieldsNull = allFieldsNull; return container; } PhotoInfoContainer ImageInfo::photoInfoContainer() const { if (!m_data) { return PhotoInfoContainer(); } ImageMetadataContainer meta = imageMetadataContainer(); PhotoInfoContainer photoInfo; photoInfo.make = meta.make; photoInfo.model = meta.model; photoInfo.lens = meta.lens; photoInfo.exposureTime = meta.exposureTime; photoInfo.exposureMode = meta.exposureMode; photoInfo.exposureProgram = meta.exposureProgram; photoInfo.aperture = meta.aperture; photoInfo.focalLength = meta.focalLength; photoInfo.focalLength35mm = meta.focalLength35; photoInfo.sensitivity = meta.sensitivity; photoInfo.flash = meta.flashMode; photoInfo.whiteBalance = meta.whiteBalance; photoInfo.dateTime = dateTime(); return photoInfo; } VideoInfoContainer ImageInfo::videoInfoContainer() const { if (!m_data) { return VideoInfoContainer(); } VideoMetadataContainer meta = videoMetadataContainer(); VideoInfoContainer videoInfo; videoInfo.aspectRatio = meta.aspectRatio; videoInfo.audioBitRate = meta.audioBitRate; videoInfo.audioChannelType = meta.audioChannelType; videoInfo.audioCodec = meta.audioCodec; videoInfo.duration = meta.duration; videoInfo.frameRate = meta.frameRate; videoInfo.videoCodec = meta.videoCodec; return videoInfo; } Template ImageInfo::metadataTemplate() const { if (!m_data) { return Template(); } Template t; imageCopyright().fillTemplate(t); ImageExtendedProperties ep = imageExtendedProperties(); t.setLocationInfo(ep.location()); t.setIptcSubjects(ep.subjectCode()); return t; } void ImageInfo::setMetadataTemplate(const Template& t) { if (!m_data) { return; } removeMetadataTemplate(); imageCopyright().setFromTemplate(t); ImageExtendedProperties ep = imageExtendedProperties(); ep.setLocation(t.locationInfo()); ep.setSubjectCode(t.IptcSubjects()); } void ImageInfo::removeMetadataTemplate() { if (!m_data) { return; } imageCopyright().removeAll(); ImageExtendedProperties ep = imageExtendedProperties(); ep.removeLocation(); ep.removeSubjectCode(); } void ImageInfo::setPickLabel(int pickId) { if (!m_data || pickId < FirstPickLabel || pickId > LastPickLabel) { return; } QList currentTagIds = tagIds(); QVector pickLabelTags = TagsCache::instance()->pickLabelTags(); // Pick Label is an exclusive tag. // Perform "switch" operation atomic { CoreDbAccess access; foreach(int tagId, currentTagIds) { if (pickLabelTags.contains(tagId)) { removeTag(tagId); } } setTag(pickLabelTags[pickId]); } ImageInfoWriteLocker lock; m_data->pickLabel = pickId; m_data->pickLabelCached = true; } void ImageInfo::setColorLabel(int colorId) { if (!m_data || colorId < FirstColorLabel || colorId > LastColorLabel) { return; } QList currentTagIds = tagIds(); QVector colorLabelTags = TagsCache::instance()->colorLabelTags(); // Color Label is an exclusive tag. // Perform "switch" operation atomic { CoreDbAccess access; foreach(int tagId, currentTagIds) { if (colorLabelTags.contains(tagId)) { removeTag(tagId); } } setTag(colorLabelTags[colorId]); } ImageInfoWriteLocker lock; m_data->colorLabel = colorId; m_data->colorLabelCached = true; } void ImageInfo::setRating(int value) { if (!m_data) { return; } CoreDbAccess().db()->changeImageInformation(m_data->id, QVariantList() << value, DatabaseFields::Rating); ImageInfoWriteLocker lock; m_data->rating = value; m_data->ratingCached = true; } void ImageInfo::setManualOrder(int value) { // return ; if (!m_data) { return; } CoreDbAccess().db()->changeImages(m_data->id, QVariantList() << value, DatabaseFields::ManualOrder); // ImageInfoWriteLocker lock; // m_data->manualOrder = value; // m_data->manualOrderCached = true; } void ImageInfo::setOrientation(int value) { if (!m_data) { return; } CoreDbAccess().db()->changeImageInformation(m_data->id, QVariantList() << value, DatabaseFields::Orientation); } +void ImageInfo::setName(const QString& newName) +{ + if (!m_data || newName.isEmpty()) + { + return; + } + + CoreDbAccess().db()->renameItem(m_data->id, newName); + + ImageInfoWriteLocker lock; + m_data->name = newName; + ImageInfoStatic::cache()->cacheByName(m_data); +} + void ImageInfo::setDateTime(const QDateTime& dateTime) { if (!m_data || !dateTime.isValid()) { return; } CoreDbAccess().db()->changeImageInformation(m_data->id, QVariantList() << dateTime, DatabaseFields::CreationDate); ImageInfoWriteLocker lock; m_data->creationDate = dateTime; m_data->creationDateCached = true; } void ImageInfo::setTag(int tagID) { if (!m_data || tagID <= 0) { return; } CoreDbAccess().db()->addItemTag(m_data->id, tagID); } void ImageInfo::removeTag(int tagID) { if (!m_data) { return; } CoreDbAccess access; access.db()->removeItemTag(m_data->id, tagID); access.db()->removeImageTagProperties(m_data->id, tagID); } void ImageInfo::removeAllTags() { if (!m_data) { return; } CoreDbAccess().db()->removeItemAllTags(m_data->id, tagIds()); } void ImageInfo::addTagPaths(const QStringList& tagPaths) { if (!m_data) { return; } QList tagIds = TagsCache::instance()->tagsForPaths(tagPaths); CoreDbAccess().db()->addTagsToItems(QList() << m_data->id, tagIds); } ImageInfo ImageInfo::copyItem(int dstAlbumID, const QString& dstFileName) { if (!m_data) { return ImageInfo(); } { ImageInfoReadLocker lock; if (dstAlbumID == m_data->albumId && dstFileName == m_data->name) { return (*this); } } int id = CoreDbAccess().db()->copyItem(m_data->albumId, m_data->name, dstAlbumID, dstFileName); if (id == -1) { return ImageInfo(); } return ImageInfo(id); } bool ImageInfo::isLocationAvailable() const { if (!m_data) { return false; } return CollectionManager::instance()->locationForAlbumRootId(m_data->albumRootId).isAvailable(); } double ImageInfo::similarityTo(const qlonglong imageId) const { return imageExtendedProperties().similarityTo(imageId); } double ImageInfo::currentSimilarity() const { if (!m_data) { return 0.0; } return m_data->currentSimilarity; } qlonglong ImageInfo::currentReferenceImage() const { if (!m_data) { return -1; } return m_data->currentReferenceImage; } QList ImageInfo::fromUniqueHash(const QString& uniqueHash, qlonglong fileSize) { QList scanInfos = CoreDbAccess().db()->getIdenticalFiles(uniqueHash, fileSize); QList infos; foreach (const ItemScanInfo& scanInfo, scanInfos) { infos << ImageInfo(scanInfo.id); } return infos; } ThumbnailIdentifier ImageInfo::thumbnailIdentifier() const { if (!m_data) { return ThumbnailIdentifier(); } ThumbnailIdentifier id; id.id = m_data->id; id.filePath = filePath(); return id; } ThumbnailInfo ImageInfo::thumbnailInfo() const { if (!m_data) { return ThumbnailInfo(); } ThumbnailInfo thumbinfo; QVariantList values; thumbinfo.id = m_data->id; thumbinfo.filePath = filePath(); thumbinfo.fileName = name(); thumbinfo.isAccessible = CollectionManager::instance()->locationForAlbumRootId(m_data->albumRootId).isAvailable(); CoreDbAccess access; values = access.db()->getImagesFields(m_data->id, DatabaseFields::ModificationDate | DatabaseFields::FileSize | DatabaseFields::UniqueHash); if (!values.isEmpty()) { thumbinfo.modificationDate = values.at(0).toDateTime(); thumbinfo.fileSize = values.at(1).toLongLong(); thumbinfo.uniqueHash = values.at(2).toString(); } values = access.db()->getImageInformation(m_data->id, DatabaseFields::Orientation); if (!values.isEmpty()) { thumbinfo.orientationHint = values.first().toInt(); } return thumbinfo; } ThumbnailIdentifier ImageInfo::thumbnailIdentifier(qlonglong id) { ImageInfo info(id); return info.thumbnailIdentifier(); } QDebug operator<<(QDebug stream, const ImageInfo& info) { return stream << "ImageInfo [id = " << info.id() << ", path = " << info.filePath() << "]"; } ImageInfo::DatabaseFieldsHashRaw ImageInfo::getDatabaseFieldsRaw(const DatabaseFields::Set& requestedSet) const { if (!m_data || (!m_data->hasVideoMetadata && !m_data->hasImageMetadata)) { return DatabaseFieldsHashRaw(); } DatabaseFields::VideoMetadataMinSizeType cachedVideoMetadata; DatabaseFields::ImageMetadataMinSizeType cachedImageMetadata; ImageInfo::DatabaseFieldsHashRaw cachedHash; // consolidate to one ReadLocker. In particular, the shallow copy of the QHash must be done under protection { ImageInfoReadLocker lock; cachedVideoMetadata = m_data->videoMetadataCached; cachedImageMetadata = m_data->imageMetadataCached; cachedHash = m_data->databaseFieldsHashRaw; } if (requestedSet.hasFieldsFromVideoMetadata() && m_data->hasVideoMetadata) { const DatabaseFields::VideoMetadata requestedVideoMetadata = requestedSet.getVideoMetadata(); const DatabaseFields::VideoMetadata missingVideoMetadata = requestedVideoMetadata & ~cachedVideoMetadata; if (missingVideoMetadata) { const QVariantList fieldValues = CoreDbAccess().db()->getVideoMetadata(m_data->id, missingVideoMetadata); ImageInfoWriteLocker lock; if (fieldValues.isEmpty()) { m_data.constCastData()->hasVideoMetadata = false; m_data.constCastData()->databaseFieldsHashRaw.removeAllFields(DatabaseFields::VideoMetadataAll); m_data.constCastData()->videoMetadataCached = DatabaseFields::VideoMetadataNone; } else { int fieldsIndex = 0; for (DatabaseFields::VideoMetadataIteratorSetOnly it(missingVideoMetadata) ; !it.atEnd() ; ++it) { const QVariant fieldValue = fieldValues.at(fieldsIndex); ++fieldsIndex; m_data.constCastData()->databaseFieldsHashRaw.insertField(*it, fieldValue); } m_data.constCastData()->videoMetadataCached |= missingVideoMetadata; } // update for return value cachedHash = m_data->databaseFieldsHashRaw; } } if (requestedSet.hasFieldsFromImageMetadata() && m_data->hasImageMetadata) { const DatabaseFields::ImageMetadata requestedImageMetadata = requestedSet.getImageMetadata(); const DatabaseFields::ImageMetadata missingImageMetadata = requestedImageMetadata & ~cachedImageMetadata; if (missingImageMetadata) { const QVariantList fieldValues = CoreDbAccess().db()->getImageMetadata(m_data->id, missingImageMetadata); ImageInfoWriteLocker lock; if (fieldValues.isEmpty()) { m_data.constCastData()->hasImageMetadata = false; m_data.constCastData()->databaseFieldsHashRaw.removeAllFields(DatabaseFields::ImageMetadataAll); m_data.constCastData()->imageMetadataCached = DatabaseFields::ImageMetadataNone; } else { int fieldsIndex = 0; for (DatabaseFields::ImageMetadataIteratorSetOnly it(missingImageMetadata) ; !it.atEnd() ; ++it) { const QVariant fieldValue = fieldValues.at(fieldsIndex); ++fieldsIndex; m_data.constCastData()->databaseFieldsHashRaw.insertField(*it, fieldValue); } m_data.constCastData()->imageMetadataCached |= missingImageMetadata; } cachedHash = m_data->databaseFieldsHashRaw; } } // We always return all fields, the caller can just retrieve the ones he needs. return cachedHash; } QVariant ImageInfo::getDatabaseFieldRaw(const DatabaseFields::Set& requestedField) const { DatabaseFieldsHashRaw rawHash = getDatabaseFieldsRaw(requestedField); if (requestedField.hasFieldsFromImageMetadata()) { const DatabaseFields::ImageMetadata requestedFieldFlag = requestedField; const QVariant value = rawHash.value(requestedFieldFlag); return value; } if (requestedField.hasFieldsFromVideoMetadata()) { const DatabaseFields::VideoMetadata requestedFieldFlag = requestedField; const QVariant value = rawHash.value(requestedFieldFlag); return value; } return QVariant(); } } // namespace Digikam diff --git a/core/libs/database/item/imageinfo.h b/core/libs/database/item/imageinfo.h index 84a2e03947..c3fb1cc829 100644 --- a/core/libs/database/item/imageinfo.h +++ b/core/libs/database/item/imageinfo.h @@ -1,530 +1,536 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2005-04-21 * Description : Handling accesses to one image and associated data * * Copyright (C) 2005 by Renchi Raju * Copyright (C) 2007-2013 by Marcel Wiesweg * Copyright (C) 2009-2018 by Gilles Caulier * Copyright (C) 2013 by Michael G. Hansen * * 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, 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. * * ============================================================ */ #ifndef DIGIKAM_IMAGE_INFO_H #define DIGIKAM_IMAGE_INFO_H // Qt includes #include #include #include #include #include // Local includes #include "coredbalbuminfo.h" #include "digikam_export.h" #include "dshareddata.h" #include "coredburl.h" #include "imageinfolist.h" #include "coredbfields.h" namespace Digikam { class DImageHistory; class HistoryImageId; class ImageComments; class ImageCommonContainer; class ImageCopyright; class ImageExtendedProperties; class ImageInfoData; class ImageListerRecord; class ImageMetadataContainer; class VideoMetadataContainer; class ImagePosition; class ImageTagPair; class PhotoInfoContainer; class VideoInfoContainer; class Template; class ThumbnailIdentifier; class ThumbnailInfo; /** * The ImageInfo class contains provides access to the database for a single image. * The properties can be read and written. Information will be cached. */ class DIGIKAM_DATABASE_EXPORT ImageInfo { public: /** * Constructor * Creates a null image info */ ImageInfo(); /** * Constructor. Creates an ImageInfo object without any cached data initially. * @param ID unique ID for this image */ explicit ImageInfo(qlonglong ID); /** * Constructor. Creates an ImageInfo object where the provided information * will initially be available cached, without database access. */ explicit ImageInfo(const ImageListerRecord& record); ImageInfo(const ImageInfo& info); /** * Creates an ImageInfo object from a file url. */ static ImageInfo fromLocalFile(const QString& path); static ImageInfo fromUrl(const QUrl& url); /** * Create an ImageInfo object from the given combination, which * must be cleaned and corresponding to the values in the database */ static ImageInfo fromLocationAlbumAndName(int locationId, const QString& album, const QString& name); /** * Destructor */ ~ImageInfo(); ImageInfo& operator=(const ImageInfo& info); bool operator==(const ImageInfo& info) const; bool operator!=(const ImageInfo& info) const { return !operator==(info); } bool operator<(const ImageInfo& info) const; uint hash() const; /** * Returns if this objects contains valid data */ bool isNull() const; /** * @return the name of the image */ QString name() const; /** * @return the datetime of the image */ QDateTime dateTime() const; /** * @return the modification datetime of the image */ QDateTime modDateTime() const; /** * @return the filesize of the image */ qlonglong fileSize() const; /** * @return the unique hash of the image. */ QString uniqueHash() const; /** * @return the dimensions of the image (valid only if dimensions * have been requested) */ QSize dimensions() const; /** * Returns the file:// url. * This is equivalent to QUrl::fromLocalFile(filePath()) */ QUrl fileUrl() const; /** * Returns the file path to the image */ QString filePath() const; /** * @return the unique image id for this item */ qlonglong id() const; /** * @return the id of the PAlbum to which this item belongs */ int albumId() const; /** * The album root id */ int albumRootId() const; /** * @return the default title for this item */ QString title() const; /** * @return the default comment for this item */ QString comment() const; /** * @return the id of the Aspect Ratio for this item */ double aspectRatio() const; /** * Returns the Pick Label Id (see PickLabel values in globals.h) */ int pickLabel() const; /** * Returns the Color Label Id (see ColorLabel values in globals.h) */ int colorLabel() const; /** * Returns the rating */ int rating() const; /** * Returns the category of the item: Image, Audio, Video */ DatabaseItem::Category category() const; /** Returns the image format / mimetype as a standardized * string (see DBSCHEMA.ODS). */ QString format() const; /** * @return a list of IDs of tags assigned to this item * @see tagNames * @see tagPaths * @see Album::id() */ QList tagIds() const; /** * Returns true if the image is marked as visible in the database. */ bool isVisible() const; /** * Returns true if the corresponding file was not deleted. */ bool isRemoved() const; /** * Returns the orientation of the image, * (MetaEngine::ImageOrientation, EXIF standard) */ int orientation() const; /** * Retrieve the ImageComments object for this item. * This object allows full read and write access to all comments * and their properties. * You need to hold CoreDbAccess to ensure the validity. * For simple, cached read access see comment(). */ ImageComments imageComments(CoreDbAccess& access) const; /** * Retrieve the ImageCopyright object for this item. * This object allows full read and write access to all copyright * values. */ ImageCopyright imageCopyright() const; /** * Retrieve the ImageExtendedProperties object for this item. * This object allows full read and write access to all extended properties * values. */ ImageExtendedProperties imageExtendedProperties() const; /** * Retrieve the ImagePosition object for this item. */ ImagePosition imagePosition() const; /** * Retrieves the coordinates and the altitude. * Returns 0 if hasCoordinates(), or hasAltitude resp, is false. */ double longitudeNumber() const; double latitudeNumber() const; double altitudeNumber() const; bool hasCoordinates() const; bool hasAltitude() const; /** * Retrieve an ImageTagPair object for a single tag, or for all * image/tag pairs for which properties are available * (not necessarily the assigned tags) */ ImageTagPair imageTagPair(int tagId) const; QList availableImageTagPairs() const; /** * Retrieves and sets the image history from the database. * Note: The image history retrieved here does typically include all * steps from the original to this image, but does not reference this image * itself. * */ DImageHistory imageHistory() const; void setImageHistory(const DImageHistory& history); bool hasImageHistory() const; /** * Retrieves and sets this' images UUID */ QString uuid() const; void setUuid(const QString& uuid); /** * Constructs a HistoryImageId with all available information for this image. */ HistoryImageId historyImageId() const; /** * Retrieve information about images from which this image * is derived (ancestorImages) and images that have been derived * from this images (derivedImages). */ bool hasDerivedImages() const; bool hasAncestorImages() const; QList derivedImages() const; QList ancestorImages() const; /** * Returns the cloud of all directly or indirectly related images, * derived images or ancestors, in from of "a derived from b" pairs. */ QList > relationCloud() const; /** * Add a relation to the database: * This image is derived from the ancestorImage. */ void markDerivedFrom(const ImageInfo& ancestorImage); /** * The image is grouped in the group of another (leading) image. */ bool isGrouped() const; /** * The image is the leading image of a group, * there are other images grouped behind this one. */ bool hasGroupedImages() const; int numberOfGroupedImages() const; /** * Returns the leading image of the group. * Returns a null image if this image is not grouped (isGrouped()) */ ImageInfo groupImage() const; qlonglong groupImageId() const; /** * Returns the list of images grouped behind this image (not including this * image itself) and an empty list if there is none. */ QList groupedImages() const; /** * Group this image behind the given image */ void addToGroup(const ImageInfo& info); /** * This image is grouped behind another image: * Remove this image from its group */ void removeFromGroup(); /** * This image hasGroupedImages(): Split up the group, * remove all groupedImages() from this image's group. */ void clearGroup(); /** * Retrieve information about the image, * in form of numbers and user presentable strings, * for certain defined fields of information (see databaseinfocontainers.h) */ ImageCommonContainer imageCommonContainer() const; ImageMetadataContainer imageMetadataContainer() const; VideoMetadataContainer videoMetadataContainer() const; PhotoInfoContainer photoInfoContainer() const; VideoInfoContainer videoInfoContainer() const; typedef DatabaseFields::Hash DatabaseFieldsHashRaw; /** * @todo Supports only VideoMetadataField and ImageMetadataField values for now. */ DatabaseFieldsHashRaw getDatabaseFieldsRaw(const DatabaseFields::Set& requestedSet) const; QVariant getDatabaseFieldRaw(const DatabaseFields::Set& requestedField) const; /** * Retrieve metadata template information about the image. */ Template metadataTemplate() const; /** * Set metadata template information (write it to database) * @param t the new template data. */ void setMetadataTemplate(const Template& t); /** * Remove all template info about the image from database. */ void removeMetadataTemplate(); + /** + * Set the name (write it to database) + * @param newName the new name. + */ + void setName(const QString& newName); + /** * Set the date and time (write it to database) * @param dateTime the new date and time. */ void setDateTime(const QDateTime& dateTime); /** * Adds a tag to the item (writes it to database) * @param tagID the ID of the tag to add */ void setTag(int tagID); /** * Adds tags in the list to the item. * Tags are created if they do not yet exist */ void addTagPaths(const QStringList& tagPaths); /** * Remove a tag from the item (removes it from database) * @param tagID the ID of the tag to remove */ void removeTag(int tagID); /** * Remove all tags from the item (removes it from database) */ void removeAllTags(); /** Set the pick Label Id for the item (see PickLabel values from globals.h) */ void setPickLabel(int value); /** * Set the color Label Id for the item (see ColorLabel values from globals.h) */ void setColorLabel(int value); /** * Set the rating for the item */ void setRating(int value); /** * Set the manul sorting order for the item */ void setManualOrder(int value); /** * Set the orientation for the item */ void setOrientation(int value); /** * Set the visibility flag - triggers between Visible and Hidden */ void setVisible(bool isVisible); /** * Copy database information of this item to a newly created item * @param dstAlbumID destination album id * @param dstFileName new filename * @return an ImageInfo object of the new item */ //TODO: Move to album? ImageInfo copyItem(int dstAlbumID, const QString& dstFileName); /** * Returns true if this is a valid ImageInfo, * and the location of the image is currently available * (information freshly obtained from CollectionManager) */ bool isLocationAvailable() const; /** * Scans the database for items with the given signature. */ QList fromUniqueHash(const QString& uniqueHash, qlonglong fileSize); /** * Fills a ThumbnailIdentifier / ThumbnailInfo from this ImageInfo */ ThumbnailIdentifier thumbnailIdentifier() const; ThumbnailInfo thumbnailInfo() const; static ThumbnailIdentifier thumbnailIdentifier(qlonglong id); double similarityTo(const qlonglong imageId) const; double currentSimilarity() const; /** * Returns the id of the current fuzzy search reference image. */ qlonglong currentReferenceImage() const; private: friend class ImageInfoCache; friend class ImageInfoList; DSharedDataPointer m_data; }; inline uint qHash(const ImageInfo& info) { return info.hash(); } //! qDebug() stream operator. Writes property @a info to the debug output in a nicely formatted way. DIGIKAM_DATABASE_EXPORT QDebug operator<<(QDebug stream, const ImageInfo& info); } // namespace Digikam Q_DECLARE_TYPEINFO(Digikam::ImageInfo, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(Digikam::ImageInfo) #endif // DIGIKAM_IMAGE_INFO_H diff --git a/core/libs/database/item/imageinfocache.cpp b/core/libs/database/item/imageinfocache.cpp index 06beb5c8fb..0aebba6eeb 100644 --- a/core/libs/database/item/imageinfocache.cpp +++ b/core/libs/database/item/imageinfocache.cpp @@ -1,375 +1,375 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2007-05-01 * Description : ImageInfo common data * * Copyright (C) 2007-2013 by Marcel Wiesweg * Copyright (C) 2014-2018 by Gilles Caulier * Copyright (C) 2013 by Michael G. Hansen * * 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, 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. * * ============================================================ */ #include "imageinfocache.h" // Local includes #include "coredb.h" #include "coredbalbuminfo.h" #include "imageinfo.h" #include "imageinfolist.h" #include "imageinfodata.h" #include "digikam_debug.h" namespace Digikam { ImageInfoCache::ImageInfoCache() : m_needUpdateAlbums(true) { qRegisterMetaType("ImageInfo"); qRegisterMetaType("ImageInfoList"); qRegisterMetaType >("QList"); CoreDbWatch* const dbwatch = CoreDbAccess::databaseWatch(); connect(dbwatch, SIGNAL(imageChange(ImageChangeset)), this, SLOT(slotImageChanged(ImageChangeset)), Qt::DirectConnection); connect(dbwatch, SIGNAL(imageTagChange(ImageTagChangeset)), this, SLOT(slotImageTagChanged(ImageTagChangeset)), Qt::DirectConnection); connect(dbwatch, SIGNAL(albumChange(AlbumChangeset)), this, SLOT(slotAlbumChange(AlbumChangeset)), Qt::DirectConnection); } ImageInfoCache::~ImageInfoCache() { } template DSharedDataPointer toStrongRef(T* weakRef) { // Called under read lock if (!weakRef) { return DSharedDataPointer(); } // The weak ref is a data object which is not deleted // (because deletion is done under mutex protection) // but may have a ref count of 0. // If the ref count is 0 and we gave the object away to another // thread, it might get deleted by this thread before the mutex // is acquired in the first thread which initially dropped the ref // count to 0 and also intends to delete it, then operating // on deleted data and crashing. // That means if the weakRef had a ref count of 0 before we incremented, // we need to drop it. int previousRef = weakRef->ref.fetchAndAddOrdered(1); if (previousRef == 0) { // drop weakRef weakRef->ref.deref(); return DSharedDataPointer(); } // Convert to a strong reference. Will ref() the weakRef once again DSharedDataPointer ptr(weakRef); // decrease counter, which we incremented twice now weakRef->ref.deref(); return ptr; } static bool lessThanForAlbumShortInfo(const AlbumShortInfo& first, const AlbumShortInfo& second) { return first.id < second.id; } void ImageInfoCache::checkAlbums() { if (m_needUpdateAlbums) { // list comes sorted from db QList infos = CoreDbAccess().db()->getAlbumShortInfos(); ImageInfoWriteLocker lock; m_albums = infos; m_needUpdateAlbums = false; } } DSharedDataPointer ImageInfoCache::infoForId(qlonglong id) { { ImageInfoReadLocker lock; DSharedDataPointer ptr = toStrongRef(m_infos.value(id)); if (ptr) { return ptr; } } ImageInfoWriteLocker lock; ImageInfoData* const data = new ImageInfoData(); data->id = id; m_infos[id] = data; return DSharedDataPointer(data); } -void ImageInfoCache::cacheByName(ImageInfoData* data) +void ImageInfoCache::cacheByName(ImageInfoData* const data) { // Called with Write lock if (!data || data->id == -1 || data->name.isEmpty()) { return; } // Called in a context where we can assume that the entry is not yet cached by name (newly created data) m_nameHash.remove(m_dataHash.value(data), data); m_nameHash.insert(data->name, data); m_dataHash.insert(data, data->name); } DSharedDataPointer ImageInfoCache::infoForPath(int albumRootId, const QString& relativePath, const QString& name) { ImageInfoReadLocker lock; // We check all entries in the multi hash with matching file name QMultiHash::const_iterator it; - for (it = m_nameHash.constFind(name); it != m_nameHash.constEnd() && it.key() == name; ++it) + for (it = m_nameHash.constFind(name) ; it != m_nameHash.constEnd() && it.key() == name ; ++it) { // first check that album root matches if (it.value()->albumRootId != albumRootId) { continue; } // check that relativePath matches. We get relativePath from entry's id and compare to given name. QList::const_iterator albumIt = findAlbum(it.value()->albumId); if (albumIt == m_albums.constEnd() || albumIt->relativePath != relativePath) { continue; } // we have now a match by name, albumRootId and relativePath return toStrongRef(it.value()); } return DSharedDataPointer(); } -void ImageInfoCache::dropInfo(ImageInfoData* infodata) +void ImageInfoCache::dropInfo(ImageInfoData* const infodata) { if (!infodata) { return; } ImageInfoWriteLocker lock; m_infos.remove(infodata->id); m_nameHash.remove(m_dataHash.value(infodata), infodata); m_nameHash.remove(infodata->name, infodata); m_dataHash.remove(infodata); delete infodata; } QList::const_iterator ImageInfoCache::findAlbum(int id) { // Called with read lock AlbumShortInfo info; info.id = id; // we use the fact that d->infos is sorted by id return qBinaryFind(m_albums.constBegin(), m_albums.constEnd(), info, lessThanForAlbumShortInfo); } QString ImageInfoCache::albumRelativePath(int albumId) { checkAlbums(); ImageInfoReadLocker lock; QList::const_iterator it = findAlbum(albumId); if (it != m_albums.constEnd()) { return it->relativePath; } return QString(); } void ImageInfoCache::invalidate() { ImageInfoWriteLocker lock; QHash::iterator it; - for (it = m_infos.begin(); it != m_infos.end(); ++it) + for (it = m_infos.begin() ; it != m_infos.end() ; ++it) { if ((*it)->isReferenced()) { (*it)->invalid = true; - (*it)->id = -1; + (*it)->id = -1; } else { delete *it; } } m_infos.clear(); m_albums.clear(); } void ImageInfoCache::slotImageChanged(const ImageChangeset& changeset) { ImageInfoWriteLocker lock; foreach(const qlonglong& imageId, changeset.ids()) { QHash::iterator it = m_infos.find(imageId); if (it != m_infos.end()) { // invalidate the relevant field. It will be lazy-loaded at first access. DatabaseFields::Set changes = changeset.changes(); if (changes & DatabaseFields::ImageCommentsAll) { (*it)->defaultCommentCached = false; (*it)->defaultTitleCached = false; } if (changes & DatabaseFields::Category) { (*it)->categoryCached = false; } if (changes & DatabaseFields::Format) { (*it)->formatCached = false; } if (changes & DatabaseFields::PickLabel) { (*it)->pickLabelCached = false; } if (changes & DatabaseFields::ColorLabel) { (*it)->colorLabelCached = false; } if (changes & DatabaseFields::Rating) { (*it)->ratingCached = false; } if (changes & DatabaseFields::CreationDate) { (*it)->creationDateCached = false; } if (changes & DatabaseFields::ModificationDate) { (*it)->modificationDateCached = false; } if (changes & DatabaseFields::FileSize) { (*it)->fileSizeCached = false; } if ((changes & DatabaseFields::Width) || (changes & DatabaseFields::Height)) { (*it)->imageSizeCached = false; } if (changes & DatabaseFields::LatitudeNumber || changes & DatabaseFields::LongitudeNumber || changes & DatabaseFields::Altitude) { (*it)->positionsCached = false; } if (changes & DatabaseFields::ImageRelations) { (*it)->groupedImagesCached = false; (*it)->groupImageCached = false; } if (changes.hasFieldsFromVideoMetadata()) { const DatabaseFields::VideoMetadata changedVideoMetadata = changes.getVideoMetadata(); (*it)->videoMetadataCached&=~changedVideoMetadata; (*it)->databaseFieldsHashRaw.removeAllFields(changedVideoMetadata); } if (changes.hasFieldsFromImageMetadata()) { const DatabaseFields::ImageMetadata changedImageMetadata = changes.getImageMetadata(); (*it)->imageMetadataCached&=~changedImageMetadata; (*it)->databaseFieldsHashRaw.removeAllFields(changedImageMetadata); } } } } void ImageInfoCache::slotImageTagChanged(const ImageTagChangeset& changeset) { if (changeset.propertiesWereChanged()) { return; } ImageInfoWriteLocker lock; foreach(const qlonglong& imageId, changeset.ids()) { QHash::iterator it = m_infos.find(imageId); if (it != m_infos.end()) { (*it)->tagIdsCached = false; (*it)->colorLabelCached = false; (*it)->pickLabelCached = false; } } } void ImageInfoCache::slotAlbumChange(const AlbumChangeset& changeset) { switch (changeset.operation()) { case AlbumChangeset::Added: case AlbumChangeset::Deleted: case AlbumChangeset::Renamed: case AlbumChangeset::PropertiesChanged: m_needUpdateAlbums = true; break; case AlbumChangeset::Unknown: break; } } } // namespace Digikam diff --git a/core/libs/database/item/imageinfocache.h b/core/libs/database/item/imageinfocache.h index d2b5cda5a0..4495052667 100644 --- a/core/libs/database/item/imageinfocache.h +++ b/core/libs/database/item/imageinfocache.h @@ -1,114 +1,114 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2007-05-01 * Description : ImageInfo common data * * Copyright (C) 2007-2013 by Marcel Wiesweg * Copyright (C) 2013-2018 by Gilles Caulier * * 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, 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. * * ============================================================ */ #ifndef DIGIKAM_IMAGE_INFO_CACHE_H #define DIGIKAM_IMAGE_INFO_CACHE_H // Qt includes #include #include #include // Local includes #include "coredbwatch.h" #include "dshareddata.h" namespace Digikam { class AlbumShortInfo; class ImageInfoData; // No EXPORT class class ImageInfoCache : public QObject { Q_OBJECT public: explicit ImageInfoCache(); ~ImageInfoCache(); /** * Return an ImageInfoData object for the given image id. * A new object is created, or an existing object is returned. * If a new object is created, the id field will be initialized. */ DSharedDataPointer infoForId(qlonglong id); /** * Call this when the data has been dereferenced, * before deletion. */ - void dropInfo(ImageInfoData* infodata); + void dropInfo(ImageInfoData* const infodata); /** * Call this to put data in the hash by file name if you have newly created data * and the name is filled. * Call under write lock. */ - void cacheByName(ImageInfoData* data); + void cacheByName(ImageInfoData* const data); /** * Return an ImageInfoData object for the given album root, relativePath and file name triple. * Works if previously cached with cacheByName. * Returns 0 if not found. */ DSharedDataPointer infoForPath(int albumRootId, const QString& relativePath, const QString& name); /** * Returns the cached relativePath for the given album id. */ QString albumRelativePath(int albumId); /** * Invalidate the cache and all its cached data */ void invalidate(); private Q_SLOTS: void slotImageChanged(const ImageChangeset& changeset); void slotImageTagChanged(const ImageTagChangeset& changeset); void slotAlbumChange(const AlbumChangeset&); private: QList::const_iterator findAlbum(int id); void checkAlbums(); private: QHash m_infos; QHash m_dataHash; QMultiHash m_nameHash; volatile bool m_needUpdateAlbums; QList m_albums; }; } // namespace Digikam #endif // DIGIKAM_IMAGE_INFO_CACHE_H diff --git a/core/libs/database/models/imagelistmodel.cpp b/core/libs/database/models/imagelistmodel.cpp index fafce34747..06a72772a9 100644 --- a/core/libs/database/models/imagelistmodel.cpp +++ b/core/libs/database/models/imagelistmodel.cpp @@ -1,70 +1,71 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2010-12-06 * Description : An image model based on a static list * * Copyright (C) 2010-2011 by Marcel Wiesweg * * 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, 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. * * ============================================================ */ #include "imagelistmodel.h" // Local includes #include "digikam_debug.h" #include "coredbaccess.h" #include "coredbchangesets.h" #include "coredbwatch.h" #include "imageinfo.h" #include "imageinfolist.h" namespace Digikam { ImageListModel::ImageListModel(QObject* parent) : ImageThumbnailModel(parent) { connect(CoreDbAccess::databaseWatch(), SIGNAL(collectionImageChange(CollectionImageChangeset)), this, SLOT(slotCollectionImageChange(CollectionImageChangeset))); } ImageListModel::~ImageListModel() { } void ImageListModel::slotCollectionImageChange(const CollectionImageChangeset& changeset) { if (isEmpty()) { return; } switch (changeset.operation()) { case CollectionImageChangeset::Added: break; + case CollectionImageChangeset::Deleted: case CollectionImageChangeset::Removed: case CollectionImageChangeset::RemovedAll: removeImageInfos(ImageInfoList(changeset.ids())); break; default: break; } } } // namespace Digikam diff --git a/core/libs/database/models/imagemodel.cpp b/core/libs/database/models/imagemodel.cpp index 4a10d1fc02..4420b57d1c 100644 --- a/core/libs/database/models/imagemodel.cpp +++ b/core/libs/database/models/imagemodel.cpp @@ -1,1368 +1,1368 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-03-05 * Description : Qt item model for database entries * * Copyright (C) 2009-2011 by Marcel Wiesweg * * 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, 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. * * ============================================================ */ #include "imagemodel.h" // Qt includes #include #include // Local includes #include "digikam_debug.h" #include "coredbchangesets.h" #include "coredbfields.h" #include "coredbwatch.h" #include "imageinfo.h" #include "imageinfolist.h" #include "abstractitemdragdrophandler.h" namespace Digikam { class ImageModel::Private { public: explicit Private() { preprocessor = 0; keepFilePathCache = false; sendRemovalSignals = false; incrementalUpdater = 0; refreshing = false; reAdding = false; incrementalRefreshRequested = false; } ImageInfoList infos; QList extraValues; QHash idHash; bool keepFilePathCache; QHash filePathHash; bool sendRemovalSignals; QObject* preprocessor; bool refreshing; bool reAdding; bool incrementalRefreshRequested; DatabaseFields::Set watchFlags; class ImageModelIncrementalUpdater* incrementalUpdater; ImageInfoList pendingInfos; QList pendingExtraValues; inline bool isValid(const QModelIndex& index) { if (!index.isValid()) { return false; } if (index.row() < 0 || index.row() >= infos.size()) { qCDebug(DIGIKAM_GENERAL_LOG) << "Invalid index" << index; return false; } return true; } inline bool extraValueValid(const QModelIndex& index) { // we assume isValid() being called before, no duplicate checks if (index.row() >= extraValues.size()) { qCDebug(DIGIKAM_GENERAL_LOG) << "Invalid index for extraData" << index; return false; } return true; } }; typedef QPair IntPair; // to make foreach macro happy typedef QList IntPairList; class ImageModelIncrementalUpdater { public: explicit ImageModelIncrementalUpdater(ImageModel::Private* d); void appendInfos(const QList& infos, const QList& extraValues); void aboutToBeRemovedInModel(const IntPairList& aboutToBeRemoved); QList oldIndexes(); static QList toContiguousPairs(const QList& ids); public: QHash oldIds; QList oldExtraValues; QList newInfos; QList newExtraValues; QList modelRemovals; }; ImageModel::ImageModel(QObject* parent) : QAbstractListModel(parent), d(new Private) { connect(CoreDbAccess::databaseWatch(), SIGNAL(imageChange(ImageChangeset)), this, SLOT(slotImageChange(ImageChangeset))); connect(CoreDbAccess::databaseWatch(), SIGNAL(imageTagChange(ImageTagChangeset)), this, SLOT(slotImageTagChange(ImageTagChangeset))); } ImageModel::~ImageModel() { delete d->incrementalUpdater; delete d; } // ------------ Access methods ------------- void ImageModel::setKeepsFilePathCache(bool keepCache) { d->keepFilePathCache = keepCache; } bool ImageModel::keepsFilePathCache() const { return d->keepFilePathCache; } bool ImageModel::isEmpty() const { return d->infos.isEmpty(); } void ImageModel::setWatchFlags(const DatabaseFields::Set& set) { d->watchFlags = set; } ImageInfo ImageModel::imageInfo(const QModelIndex& index) const { if (!d->isValid(index)) { return ImageInfo(); } return d->infos.at(index.row()); } ImageInfo& ImageModel::imageInfoRef(const QModelIndex& index) const { return d->infos[index.row()]; } qlonglong ImageModel::imageId(const QModelIndex& index) const { if (!d->isValid(index)) { return 0; } return d->infos.at(index.row()).id(); } QList ImageModel::imageInfos(const QList& indexes) const { QList infos; foreach(const QModelIndex& index, indexes) { infos << imageInfo(index); } return infos; } QList ImageModel::imageIds(const QList& indexes) const { QList ids; foreach(const QModelIndex& index, indexes) { ids << imageId(index); } return ids; } ImageInfo ImageModel::imageInfo(int row) const { if (row >= d->infos.size()) { return ImageInfo(); } return d->infos.at(row); } ImageInfo& ImageModel::imageInfoRef(int row) const { return d->infos[row]; } qlonglong ImageModel::imageId(int row) const { if (row < 0 || row >= d->infos.size()) { return -1; } return d->infos.at(row).id(); } QModelIndex ImageModel::indexForImageInfo(const ImageInfo& info) const { return indexForImageId(info.id()); } QModelIndex ImageModel::indexForImageInfo(const ImageInfo& info, const QVariant& extraValue) const { return indexForImageId(info.id(), extraValue); } QList ImageModel::indexesForImageInfo(const ImageInfo& info) const { return indexesForImageId(info.id()); } QModelIndex ImageModel::indexForImageId(qlonglong id) const { int index = d->idHash.value(id, -1); if (index != -1) { return createIndex(index, 0); } return QModelIndex(); } QModelIndex ImageModel::indexForImageId(qlonglong id, const QVariant& extraValue) const { if (d->extraValues.isEmpty()) return indexForImageId(id); QHash::const_iterator it; for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it) { if (d->extraValues.at(it.value()) == extraValue) return createIndex(it.value(), 0); } return QModelIndex(); } QList ImageModel::indexesForImageId(qlonglong id) const { QList indexes; QHash::const_iterator it; for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it) { indexes << createIndex(it.value(), 0); } return indexes; } int ImageModel::numberOfIndexesForImageInfo(const ImageInfo& info) const { return numberOfIndexesForImageId(info.id()); } int ImageModel::numberOfIndexesForImageId(qlonglong id) const { if (d->extraValues.isEmpty()) { return 0; } int count = 0; - QHash::const_iterator it; + QHash::const_iterator it; for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it) { ++count; } return count; } // static method ImageInfo ImageModel::retrieveImageInfo(const QModelIndex& index) { if (!index.isValid()) { return ImageInfo(); } ImageModel* const model = index.data(ImageModelPointerRole).value(); int row = index.data(ImageModelInternalId).toInt(); if (!model) { return ImageInfo(); } return model->imageInfo(row); } // static method qlonglong ImageModel::retrieveImageId(const QModelIndex& index) { if (!index.isValid()) { return 0; } ImageModel* const model = index.data(ImageModelPointerRole).value(); int row = index.data(ImageModelInternalId).toInt(); if (!model) { return 0; } return model->imageId(row); } QModelIndex ImageModel::indexForPath(const QString& filePath) const { if (d->keepFilePathCache) { return indexForImageId(d->filePathHash.value(filePath)); } else { const int size = d->infos.size(); for (int i = 0 ; i < size ; ++i) { if (d->infos.at(i).filePath() == filePath) { return createIndex(i, 0); } } } return QModelIndex(); } QList ImageModel::indexesForPath(const QString& filePath) const { if (d->keepFilePathCache) { return indexesForImageId(d->filePathHash.value(filePath)); } else { QList indexes; const int size = d->infos.size(); for (int i = 0 ; i < size ; ++i) { if (d->infos.at(i).filePath() == filePath) { indexes << createIndex(i, 0); } } return indexes; } } ImageInfo ImageModel::imageInfo(const QString& filePath) const { if (d->keepFilePathCache) { qlonglong id = d->filePathHash.value(filePath); if (id) { int index = d->idHash.value(id, -1); if (index != -1) { return d->infos.at(index); } } } else { foreach(const ImageInfo& info, d->infos) { if (info.filePath() == filePath) { return info; } } } return ImageInfo(); } QList ImageModel::imageInfos(const QString& filePath) const { QList infos; if (d->keepFilePathCache) { qlonglong id = d->filePathHash.value(filePath); if (id) { foreach(int index, d->idHash.values(id)) { infos << d->infos.at(index); } } } else { foreach(const ImageInfo& info, d->infos) { if (info.filePath() == filePath) { infos << info; } } } return infos; } void ImageModel::addImageInfo(const ImageInfo& info) { addImageInfos(QList() << info, QList()); } void ImageModel::addImageInfos(const QList& infos) { addImageInfos(infos, QList()); } void ImageModel::addImageInfos(const QList& infos, const QList& extraValues) { if (infos.isEmpty()) { return; } if (d->incrementalUpdater) { d->incrementalUpdater->appendInfos(infos, extraValues); } else { appendInfos(infos, extraValues); } } void ImageModel::addImageInfoSynchronously(const ImageInfo& info) { addImageInfosSynchronously(QList() << info, QList()); } void ImageModel::addImageInfosSynchronously(const QList& infos) { addImageInfos(infos, QList()); } void ImageModel::addImageInfosSynchronously(const QList& infos, const QList& extraValues) { if (infos.isEmpty()) { return; } publiciseInfos(infos, extraValues); emit processAdded(infos, extraValues); } void ImageModel::ensureHasImageInfo(const ImageInfo& info) { ensureHasImageInfos(QList() << info, QList()); } void ImageModel::ensureHasImageInfos(const QList& infos) { ensureHasImageInfos(infos, QList()); } void ImageModel::ensureHasImageInfos(const QList& infos, const QList& extraValues) { if (extraValues.isEmpty()) { if (!d->pendingExtraValues.isEmpty()) { qCDebug(DIGIKAM_GENERAL_LOG) << "ExtraValue / No Extra Value mismatch. Ignoring added infos."; return; } } else { if (d->pendingInfos.size() != d->pendingExtraValues.size()) { qCDebug(DIGIKAM_GENERAL_LOG) << "ExtraValue / No Extra Value mismatch. Ignoring added infos."; return; } } d->pendingInfos << infos; d->pendingExtraValues << extraValues; cleanSituationChecks(); } void ImageModel::clearImageInfos() { d->infos.clear(); d->extraValues.clear(); d->idHash.clear(); d->filePathHash.clear(); delete d->incrementalUpdater; d->incrementalUpdater = 0; d->pendingInfos.clear(); d->pendingExtraValues.clear(); d->refreshing = false; d->reAdding = false; d->incrementalRefreshRequested = false; beginResetModel(); endResetModel(); imageInfosCleared(); } void ImageModel::setImageInfos(const QList& infos) { clearImageInfos(); addImageInfos(infos); } QList ImageModel::imageInfos() const { return d->infos; } QList ImageModel::imageIds() const { return d->idHash.keys(); } bool ImageModel::hasImage(qlonglong id) const { return d->idHash.contains(id); } bool ImageModel::hasImage(const ImageInfo& info) const { return d->idHash.contains(info.id()); } bool ImageModel::hasImage(const ImageInfo& info, const QVariant& extraValue) const { return hasImage(info.id(), extraValue); } bool ImageModel::hasImage(qlonglong id, const QVariant& extraValue) const { if (d->extraValues.isEmpty()) return hasImage(id); QHash::const_iterator it; for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it) { if (d->extraValues.at(it.value()) == extraValue) return true; } return false;; } QList ImageModel::uniqueImageInfos() const { if (d->extraValues.isEmpty()) { return d->infos; } QList uniqueInfos; const int size = d->infos.size(); for (int i = 0 ; i < size ; ++i) { const ImageInfo& info = d->infos.at(i); if (d->idHash.value(info.id()) == i) { uniqueInfos << info; } } return uniqueInfos; } void ImageModel::emitDataChangedForAll() { if (d->infos.isEmpty()) { return; } QModelIndex first = createIndex(0, 0); QModelIndex last = createIndex(d->infos.size() - 1, 0); emit dataChanged(first, last); } void ImageModel::emitDataChangedForSelection(const QItemSelection& selection) { if (!selection.isEmpty()) { foreach(const QItemSelectionRange& range, selection) { emit dataChanged(range.topLeft(), range.bottomRight()); } } } void ImageModel::ensureHasGroupedImages(const ImageInfo& groupLeader) { ensureHasImageInfos(groupLeader.groupedImages()); } // ------------ Preprocessing ------------- void ImageModel::setPreprocessor(QObject* preprocessor) { unsetPreprocessor(d->preprocessor); d->preprocessor = preprocessor; } void ImageModel::unsetPreprocessor(QObject* preprocessor) { if (preprocessor && d->preprocessor == preprocessor) { disconnect(this, SIGNAL(preprocess(QList,QList)), 0, 0); disconnect(d->preprocessor, 0, this, SLOT(reAddImageInfos(QList,QList))); disconnect(d->preprocessor, 0, this, SLOT(reAddingFinished())); } } void ImageModel::appendInfos(const QList& infos, const QList& extraValues) { if (infos.isEmpty()) { return; } if (d->preprocessor) { d->reAdding = true; emit preprocess(infos, extraValues); } else { publiciseInfos(infos, extraValues); } } void ImageModel::appendInfosChecked(const QList& infos, const QList& extraValues) { // This method does deduplication. It is private because in context of readding or refreshing it is of no use. if (extraValues.isEmpty()) { QList checkedInfos; foreach(const ImageInfo& info, infos) { if (!hasImage(info)) { checkedInfos << info; } } appendInfos(checkedInfos, QList()); } else { QList checkedInfos; QList checkedExtraValues; const int size = infos.size(); for (int i = 0 ; i < size ; i++) { if (!hasImage(infos[i], extraValues[i])) { checkedInfos << infos[i]; checkedExtraValues << extraValues[i]; } } appendInfos(checkedInfos, checkedExtraValues); } } void ImageModel::reAddImageInfos(const QList& infos, const QList& extraValues) { // addImageInfos -> appendInfos -> preprocessor -> reAddImageInfos publiciseInfos(infos, extraValues); } void ImageModel::reAddingFinished() { d->reAdding = false; cleanSituationChecks(); } void ImageModel::startRefresh() { d->refreshing = true; } void ImageModel::finishRefresh() { d->refreshing = false; cleanSituationChecks(); } bool ImageModel::isRefreshing() const { return d->refreshing; } void ImageModel::cleanSituationChecks() { // For starting an incremental refresh we want a clear situation: // Any remaining batches from non-incremental refreshing subclasses have been received in appendInfos(), // any batches sent to preprocessor for re-adding have been re-added. if (d->refreshing || d->reAdding) { return; } if (!d->pendingInfos.isEmpty()) { appendInfosChecked(d->pendingInfos, d->pendingExtraValues); d->pendingInfos.clear(); d->pendingExtraValues.clear(); cleanSituationChecks(); return; } if (d->incrementalRefreshRequested) { d->incrementalRefreshRequested = false; emit readyForIncrementalRefresh(); } else { emit allRefreshingFinished(); } } void ImageModel::publiciseInfos(const QList& infos, const QList& extraValues) { if (infos.isEmpty()) { return; } Q_ASSERT(infos.size() == extraValues.size() || (extraValues.isEmpty() && d->extraValues.isEmpty())); emit imageInfosAboutToBeAdded(infos); const int firstNewIndex = d->infos.size(); const int lastNewIndex = d->infos.size() + infos.size() - 1; beginInsertRows(QModelIndex(), firstNewIndex, lastNewIndex); d->infos << infos; d->extraValues << extraValues; for (int i = firstNewIndex ; i <= lastNewIndex ; ++i) { const ImageInfo& info = d->infos.at(i); qlonglong id = info.id(); d->idHash.insertMulti(id, i); if (d->keepFilePathCache) { d->filePathHash[info.filePath()] = id; } } endInsertRows(); emit imageInfosAdded(infos); } void ImageModel::requestIncrementalRefresh() { if (d->reAdding) { d->incrementalRefreshRequested = true; } else { emit readyForIncrementalRefresh(); } } bool ImageModel::hasIncrementalRefreshPending() const { return d->incrementalRefreshRequested; } void ImageModel::startIncrementalRefresh() { delete d->incrementalUpdater; d->incrementalUpdater = new ImageModelIncrementalUpdater(d); } void ImageModel::finishIncrementalRefresh() { if (!d->incrementalUpdater) { return; } // remove old entries QList > pairs = d->incrementalUpdater->oldIndexes(); removeRowPairs(pairs); // add new indexes appendInfos(d->incrementalUpdater->newInfos, d->incrementalUpdater->newExtraValues); delete d->incrementalUpdater; d->incrementalUpdater = 0; } void ImageModel::removeIndex(const QModelIndex& index) { removeIndexes(QList() << index); } void ImageModel::removeIndexes(const QList& indexes) { QList listIndexes; foreach(const QModelIndex& index, indexes) { if (d->isValid(index)) { listIndexes << index.row(); } } if (listIndexes.isEmpty()) { return; } removeRowPairsWithCheck(ImageModelIncrementalUpdater::toContiguousPairs(listIndexes)); } void ImageModel::removeImageInfo(const ImageInfo& info) { removeImageInfos(QList() << info); } void ImageModel::removeImageInfos(const QList& infos) { QList listIndexes; foreach(const ImageInfo& info, infos) { QModelIndex index = indexForImageId(info.id()); if (index.isValid()) { listIndexes << index.row(); } } removeRowPairsWithCheck(ImageModelIncrementalUpdater::toContiguousPairs(listIndexes)); } void ImageModel::removeImageInfos(const QList& infos, const QList& extraValues) { if (extraValues.isEmpty()) { removeImageInfos(infos); return; } QList listIndexes; for (int i = 0 ; i < infos.size() ; ++i) { QModelIndex index = indexForImageId(infos.at(i).id(), extraValues.at(i)); if (index.isValid()) { listIndexes << index.row(); } } removeRowPairsWithCheck(ImageModelIncrementalUpdater::toContiguousPairs(listIndexes)); } void ImageModel::setSendRemovalSignals(bool send) { d->sendRemovalSignals = send; } template static bool pairsContain(const List& list, T value) { typename List::const_iterator middle; typename List::const_iterator begin = list.begin(); typename List::const_iterator end = list.end(); int n = int(end - begin); int half; while (n > 0) { half = n >> 1; middle = begin + half; if (middle->first <= value && middle->second >= value) { return true; } else if (middle->second < value) { begin = middle + 1; n -= half + 1; } else { n = half; } } return false; } void ImageModel::removeRowPairsWithCheck(const QList >& toRemove) { if (d->incrementalUpdater) { d->incrementalUpdater->aboutToBeRemovedInModel(toRemove); } removeRowPairs(toRemove); } void ImageModel::removeRowPairs(const QList >& toRemove) { if (toRemove.isEmpty()) { return; } // Remove old indexes // Keep in mind that when calling beginRemoveRows all structures announced to be removed // must still be valid, and this includes our hashes as well, which limits what we can optimize int removedRows = 0, offset = 0; typedef QPair IntPair; // to make foreach macro happy foreach(const IntPair& pair, toRemove) { const int begin = pair.first - offset; const int end = pair.second - offset; // inclusive removedRows = end - begin + 1; // when removing from the list, all subsequent indexes are affected offset += removedRows; QList removedInfos; if (d->sendRemovalSignals) { std::copy(d->infos.begin() + begin, d->infos.begin() + end, removedInfos.begin()); emit imageInfosAboutToBeRemoved(removedInfos); } imageInfosAboutToBeRemoved(begin, end); beginRemoveRows(QModelIndex(), begin, end); // update idHash - which points to indexes of d->infos, and these change now! QHash::iterator it; for (it = d->idHash.begin() ; it != d->idHash.end() ; ) { if (it.value() >= begin) { if (it.value() > end) { // after the removed interval: adjust index it.value() -= removedRows; } else { // in the removed interval it = d->idHash.erase(it); continue; } } ++it; } // remove from list d->infos.erase(d->infos.begin() + begin, d->infos.begin() + (end + 1)); if (!d->extraValues.isEmpty()) { d->extraValues.erase(d->extraValues.begin() + begin, d->extraValues.begin() + (end + 1)); } endRemoveRows(); if (d->sendRemovalSignals) { emit imageInfosRemoved(removedInfos); } } // tidy up: remove old indexes from file path hash now if (d->keepFilePathCache) { QHash::iterator it; for (it = d->filePathHash.begin() ; it != d->filePathHash.end() ; ) { if (pairsContain(toRemove, it.value())) { it = d->filePathHash.erase(it); } else { ++it; } } } } ImageModelIncrementalUpdater::ImageModelIncrementalUpdater(ImageModel::Private* d) { oldIds = d->idHash; oldExtraValues = d->extraValues; } void ImageModelIncrementalUpdater::appendInfos(const QList& infos, const QList& extraValues) { if (extraValues.isEmpty()) { foreach(const ImageInfo& info, infos) { - QHash::iterator it = oldIds.find(info.id()); + QHash::iterator it = oldIds.find(info.id()); if (it != oldIds.end()) { oldIds.erase(it); } else { newInfos << info; } } } else { for (int i = 0 ; i < infos.size() ; ++i) { const ImageInfo& info = infos.at(i); bool found = false; - QHash::iterator it; + QHash::iterator it; for (it = oldIds.find(info.id()) ; it != oldIds.end() && it.key() == info.id() ; ++it) { // first check is for bug #262596. Not sure if needed. if (it.value() < oldExtraValues.size() && extraValues.at(i) == oldExtraValues.at(it.value())) { found = true; break; } } if (found) { oldIds.erase(it); // do not erase from oldExtraValues - oldIds is a hash id -> index. } else { newInfos << info; newExtraValues << extraValues.at(i); } } } } void ImageModelIncrementalUpdater::aboutToBeRemovedInModel(const IntPairList& toRemove) { modelRemovals << toRemove; } QList > ImageModelIncrementalUpdater::oldIndexes() { // first, apply all changes to indexes by direct removal in model // while the updater was active foreach(const IntPairList& list, modelRemovals) { int removedRows = 0, offset = 0; foreach(const IntPair& pair, list) { const int begin = pair.first - offset; const int end = pair.second - offset; // inclusive removedRows = end - begin + 1; // when removing from the list, all subsequent indexes are affected offset += removedRows; // update idHash - which points to indexes of d->infos, and these change now! QHash::iterator it; for (it = oldIds.begin() ; it != oldIds.end() ; ) { if (it.value() >= begin) { if (it.value() > end) { // after the removed interval: adjust index it.value() -= removedRows; } else { // in the removed interval it = oldIds.erase(it); continue; } } ++it; } } } modelRemovals.clear(); return toContiguousPairs(oldIds.values()); } QList > ImageModelIncrementalUpdater::toContiguousPairs(const QList& unsorted) { // Take the given indices and return them as contiguous pairs [begin, end] QList > pairs; if (unsorted.isEmpty()) { return pairs; } QList indices(unsorted); std::sort(indices.begin(), indices.end()); QPair pair(indices.first(), indices.first()); for (int i = 1 ; i < indices.size() ; ++i) { const int &index = indices.at(i); if (index == pair.second + 1) { pair.second = index; continue; } pairs << pair; // insert last pair pair.first = index; pair.second = index; } pairs << pair; return pairs; } // ------------ QAbstractItemModel implementation ------------- QVariant ImageModel::data(const QModelIndex& index, int role) const { if (!d->isValid(index)) { return QVariant(); } switch (role) { case Qt::DisplayRole: case Qt::ToolTipRole: return d->infos.at(index.row()).name(); case ImageModelPointerRole: return QVariant::fromValue(const_cast(this)); case ImageModelInternalId: return index.row(); case CreationDateRole: return d->infos.at(index.row()).dateTime(); case ExtraDataRole: if (d->extraValueValid(index)) { return d->extraValues.at(index.row()); } else { return QVariant(); } case ExtraDataDuplicateCount: { qlonglong id = d->infos.at(index.row()).id(); return numberOfIndexesForImageId(id); } } return QVariant(); } QVariant ImageModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(section) Q_UNUSED(orientation) Q_UNUSED(role) return QVariant(); } int ImageModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } return d->infos.size(); } Qt::ItemFlags ImageModel::flags(const QModelIndex& index) const { if (!d->isValid(index)) { return 0; } Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled; f |= dragDropFlags(index); return f; } QModelIndex ImageModel::index(int row, int column, const QModelIndex& parent) const { if (column != 0 || row < 0 || parent.isValid() || row >= d->infos.size()) { return QModelIndex(); } return createIndex(row, 0); } // ------------ Database watch ------------- void ImageModel::slotImageChange(const ImageChangeset& changeset) { if (d->infos.isEmpty()) { return; } if (d->watchFlags & changeset.changes()) { QItemSelection items; foreach(const qlonglong& id, changeset.ids()) { QModelIndex index = indexForImageId(id); if (index.isValid()) { items.select(index, index); } } if (!items.isEmpty()) { emitDataChangedForSelection(items); emit imageChange(changeset, items); } } } void ImageModel::slotImageTagChange(const ImageTagChangeset& changeset) { if (d->infos.isEmpty()) { return; } QItemSelection items; foreach(const qlonglong& id, changeset.ids()) { QModelIndex index = indexForImageId(id); if (index.isValid()) { items.select(index, index); } } if (!items.isEmpty()) { emitDataChangedForSelection(items); emit imageTagChange(changeset, items); } } } // namespace Digikam diff --git a/core/libs/database/thumbsdb/thumbsdb.cpp b/core/libs/database/thumbsdb/thumbsdb.cpp index e9b6948e0d..13ae0fdd46 100644 --- a/core/libs/database/thumbsdb/thumbsdb.cpp +++ b/core/libs/database/thumbsdb/thumbsdb.cpp @@ -1,401 +1,407 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-05-29 * Description : Thumbnails database interface. * * Copyright (C) 2009 by Marcel Wiesweg * Copyright (C) 2009-2018 by Gilles Caulier * * 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, 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. * * ============================================================ */ #include "thumbsdb.h" // Qt includes #include #include #include // Local includes #include "digikam_debug.h" #include "collectionmanager.h" #include "collectionlocation.h" namespace Digikam { class ThumbsDb::Private { public: explicit Private() : db(0) { } ThumbsDbBackend* db; }; ThumbsDb::ThumbsDb(ThumbsDbBackend* const backend) : d(new Private) { d->db = backend; } ThumbsDb::~ThumbsDb() { delete d; } bool ThumbsDb::setSetting(const QString& keyword, const QString& value ) { QMap parameters; parameters.insert(QLatin1String(":keyword"), keyword); parameters.insert(QLatin1String(":value"), value); BdEngineBackend::QueryState queryStateResult = d->db->execDBAction(d->db->getDBAction(QLatin1String("ReplaceThumbnailSetting")), parameters); return (queryStateResult == BdEngineBackend::NoErrors); } QString ThumbsDb::getSetting(const QString& keyword) { QMap parameters; parameters.insert(QLatin1String(":keyword"), keyword); QList values; // TODO Should really check return status here BdEngineBackend::QueryState queryStateResult = d->db->execDBAction(d->db->getDBAction(QLatin1String("SelectThumbnailSetting")), parameters, &values); qCDebug(DIGIKAM_THUMBSDB_LOG) << "ThumbDB SelectThumbnailSetting val ret = " << (BdEngineBackend::QueryStateEnum)queryStateResult; if (values.isEmpty()) { return QString(); } else { return values.first().toString(); } } QString ThumbsDb::getLegacySetting(const QString& keyword) { QMap parameters; parameters.insert(QLatin1String(":keyword"), keyword); QList values; // TODO Should really check return status here BdEngineBackend::QueryState queryStateResult = d->db->execDBAction(d->db->getDBAction(QLatin1String("SelectThumbnailLegacySetting")), parameters, &values); qCDebug(DIGIKAM_THUMBSDB_LOG) << "ThumbDB SelectThumbnailLegacySetting val ret = " << (BdEngineBackend::QueryStateEnum)queryStateResult; if (values.isEmpty()) { return QString(); } else { return values.first().toString(); } } static void fillThumbnailInfo(const QList &values, ThumbsDbInfo& info) { if (values.isEmpty()) { return; } info.id = values.at(0).toInt(); info.type = (DatabaseThumbnail::Type)values.at(1).toInt(); info.modificationDate = values.at(2).toDateTime(); info.orientationHint = values.at(3).toInt(); info.data = values.at(4).toByteArray(); } ThumbsDbInfo ThumbsDb::findByHash(const QString& uniqueHash, qlonglong fileSize) { QList values; d->db->execSql(QLatin1String("SELECT id, type, modificationDate, orientationHint, data " "FROM UniqueHashes " " INNER JOIN Thumbnails ON thumbId = id " "WHERE uniqueHash=? AND fileSize=?;"), uniqueHash, fileSize, &values); ThumbsDbInfo info; fillThumbnailInfo(values, info); return info; } ThumbsDbInfo ThumbsDb::findByFilePath(const QString& path) { QList values; d->db->execSql(QLatin1String("SELECT id, type, modificationDate, orientationHint, data " "FROM FilePaths " " INNER JOIN Thumbnails ON thumbId = id " "WHERE path=?;"), path, &values); ThumbsDbInfo info; fillThumbnailInfo(values, info); return info; } ThumbsDbInfo ThumbsDb::findByFilePath(const QString& path, const QString& uniqueHash) { ThumbsDbInfo info = findByFilePath(path); if (uniqueHash.isNull()) { return info; } if (info.data.isNull()) { return info; } // double check that thumbnail is not referenced by a different hash QList values; d->db->execSql(QLatin1String("SELECT uniqueHash FROM UniqueHashes WHERE thumbId=?;"), info.id, &values); if (values.isEmpty()) { return info; } else { foreach(const QVariant& hash, values) { if (hash == uniqueHash) { return info; } } return ThumbsDbInfo(); } } ThumbsDbInfo ThumbsDb::findByCustomIdentifier(const QString& id) { QList values; d->db->execSql(QLatin1String("SELECT id, type, modificationDate, orientationHint, data " "FROM CustomIdentifiers " " INNER JOIN Thumbnails ON thumbId = id " "WHERE identifier=?;"), id, &values); ThumbsDbInfo info; fillThumbnailInfo(values, info); return info; } QList ThumbsDb::findAll() { QList values; d->db->execSql(QLatin1String("SELECT id FROM Thumbnails;"), &values); QList thumbIds; foreach(const QVariant& object, values) { thumbIds << object.toInt(); } return thumbIds; } QHash ThumbsDb::getFilePathsWithThumbnail() { DbEngineSqlQuery query = d->db->prepareQuery(QString::fromLatin1("SELECT path, id " "FROM FilePaths " " INNER JOIN Thumbnails ON FilePaths.thumbId=Thumbnails.id " "WHERE type BETWEEN %1 AND %2;") .arg(DatabaseThumbnail::PGF) .arg(DatabaseThumbnail::PNG)); if (!d->db->exec(query)) { return QHash(); } QHash filePaths; while (query.next()) { filePaths[query.value(0).toString()] = query.value(1).toInt(); } return filePaths; } BdEngineBackend::QueryState ThumbsDb::insertUniqueHash(const QString& uniqueHash, qlonglong fileSize, int thumbId) { return d->db->execSql(QLatin1String("REPLACE INTO UniqueHashes (uniqueHash, fileSize, thumbId) VALUES (?,?,?);"), uniqueHash, fileSize, thumbId); } BdEngineBackend::QueryState ThumbsDb::insertFilePath(const QString& path, int thumbId) { return d->db->execSql(QLatin1String("REPLACE INTO FilePaths (path, thumbId) VALUES (?,?);"), path, thumbId); } BdEngineBackend::QueryState ThumbsDb::insertCustomIdentifier(const QString& path, int thumbId) { return d->db->execSql(QLatin1String("REPLACE INTO CustomIdentifiers (identifier, thumbId) VALUES (?,?);"), path, thumbId); } BdEngineBackend::QueryState ThumbsDb::remove(int thumbId) { return d->db->execSql(QLatin1String("DELETE FROM Thumbnails WHERE id=?;"), thumbId); } BdEngineBackend::QueryState ThumbsDb::removeByUniqueHash(const QString& uniqueHash, qlonglong fileSize) { // UniqueHashes + FilePaths entries are removed by trigger QMap parameters; parameters.insert(QLatin1String(":uniqueHash"), uniqueHash); parameters.insert(QLatin1String(":filesize"), fileSize); return d->db->execDBAction(d->db->getDBAction(QLatin1String("Delete_Thumbnail_ByUniqueHashId")), parameters); } BdEngineBackend::QueryState ThumbsDb::removeByFilePath(const QString& path) { // UniqueHashes + FilePaths entries are removed by trigger QMap parameters; parameters.insert(QLatin1String(":path"), path); return d->db->execDBAction(d->db->getDBAction(QLatin1String("Delete_Thumbnail_ByPath")), parameters); } BdEngineBackend::QueryState ThumbsDb::removeByCustomIdentifier(const QString& id) { // UniqueHashes + FilePaths entries are removed by trigger QMap parameters; parameters.insert(QLatin1String(":identifier"), id); return d->db->execDBAction(d->db->getDBAction(QLatin1String("Delete_Thumbnail_ByCustomIdentifier")), parameters); } BdEngineBackend::QueryState ThumbsDb::insertThumbnail(const ThumbsDbInfo& info, QVariant* const lastInsertId) { QVariant id; BdEngineBackend::QueryState lastQueryState; lastQueryState = d->db->execSql(QLatin1String("INSERT INTO Thumbnails (type, modificationDate, orientationHint, data) VALUES (?, ?, ?, ?);"), info.type, info.modificationDate, info.orientationHint, info.data, 0, &id); if (BdEngineBackend::NoErrors == lastQueryState) { *lastInsertId = id.toInt(); } else { *lastInsertId = -1; } return lastQueryState; } +BdEngineBackend::QueryState ThumbsDb::renameByFilePath(const QString& oldPath, const QString& newPath) +{ + return d->db->execSql(QLatin1String("UPDATE FilePaths SET path=? WHERE path=?;"), + newPath, oldPath); +} + BdEngineBackend::QueryState ThumbsDb::replaceThumbnail(const ThumbsDbInfo& info) { return d->db->execSql(QLatin1String("REPLACE INTO Thumbnails (id, type, modificationDate, orientationHint, data) VALUES(?, ?, ?, ?, ?);"), QList() << info.id << info.type << info.modificationDate << info.orientationHint << info.data); } BdEngineBackend::QueryState ThumbsDb::updateModificationDate(int thumbId, const QDateTime& modificationDate) { return d->db->execSql(QLatin1String("UPDATE Thumbnails SET modificationDate=? WHERE id=?;"), modificationDate, thumbId); } void ThumbsDb::replaceUniqueHash(const QString& oldUniqueHash, int oldFileSize, const QString& newUniqueHash, int newFileSize) { d->db->execSql(QLatin1String("UPDATE UniqueHashes SET uniqueHash=?, fileSize=? WHERE uniqueHash=? AND fileSize=?;"), newUniqueHash, newFileSize, oldUniqueHash, oldFileSize); } bool ThumbsDb::integrityCheck() { QList values; d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("checkThumbnailsDbIntegrity")), &values); switch (d->db->databaseType()) { case BdEngineBackend::DbType::SQLite: // For SQLite the integrity check returns a single row with one string column "ok" on success and multiple rows on error. return values.size() == 1 && values.first().toString().toLower().compare(QLatin1String("ok")) == 0; case BdEngineBackend::DbType::MySQL: // For MySQL, for every checked table, the table name, operation (check), message type (status) and the message text (ok on success) // are returned. So we check if there are four elements and if yes, whether the fourth element is "ok". //qCDebug(DIGIKAM_DATABASE_LOG) << "MySQL check returned " << values.size() << " rows"; if ((values.size() % 4) != 0) { return false; } for (QList::iterator it = values.begin() ; it != values.end() ;) { QString tableName = (*it).toString(); ++it; QString operation = (*it).toString(); ++it; QString messageType = (*it).toString(); ++it; QString messageText = (*it).toString(); ++it; if (messageText.toLower().compare(QLatin1String("ok")) != 0) { qCDebug(DIGIKAM_DATABASE_LOG) << "Failed integrity check for table " << tableName << ". Reason:" << messageText; return false; } else { //qCDebug(DIGIKAM_DATABASE_LOG) << "Passed integrity check for table " << tableName; } } // No error conditions. Db passed the integrity check. return true; default: return false; } } void ThumbsDb::vacuum() { d->db->execDBAction(d->db->getDBAction(QString::fromUtf8("vacuumThumbnailsDB"))); } } // namespace Digikam diff --git a/core/libs/database/thumbsdb/thumbsdb.h b/core/libs/database/thumbsdb/thumbsdb.h index ee007d0d1b..87ea436597 100644 --- a/core/libs/database/thumbsdb/thumbsdb.h +++ b/core/libs/database/thumbsdb/thumbsdb.h @@ -1,157 +1,159 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-05-29 * Description : Thumbnails database interface. * * Copyright (C) 2009 by Marcel Wiesweg * Copyright (C) 2009-2018 by Gilles Caulier * * 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, 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. * * ============================================================ */ #ifndef DIGIKAM_THUMBS_DB_H #define DIGIKAM_THUMBS_DB_H // Qt includes #include #include #include #include #include #include #include // Local includes #include "dbenginesqlquery.h" #include "thumbsdbbackend.h" #include "thumbsdbaccess.h" #include "digikam_export.h" namespace Digikam { namespace DatabaseThumbnail { enum Type { UndefinedType = 0, NoThumbnail, PGF, JPEG, // Warning : no alpha channel support. Cannot be used as well. JPEG2000, PNG //FreeDesktopHash }; } // namespace DatabaseThumbnail class DIGIKAM_EXPORT ThumbsDbInfo { public: explicit ThumbsDbInfo() : id(-1), type(DatabaseThumbnail::UndefinedType), orientationHint(0) { } int id; DatabaseThumbnail::Type type; QDateTime modificationDate; int orientationHint; QByteArray data; }; // ------------------------------------------------------------------------------------------ class DIGIKAM_EXPORT ThumbsDb { public: bool setSetting(const QString& keyword, const QString& value); QString getSetting(const QString& keyword); QString getLegacySetting(const QString& keyword); ThumbsDbInfo findByHash(const QString& uniqueHash, qlonglong fileSize); ThumbsDbInfo findByFilePath(const QString& path); ThumbsDbInfo findByCustomIdentifier(const QString& id); /** This is findByFilePath with extra security: Pass the uniqueHash which you have. * If an entry is found by file path, and the entry is referenced by any uniqueHash, * which is different from the given hash, a null info is returned. * If uniqueHash is null, equivalent to the simple findByFilePath. */ ThumbsDbInfo findByFilePath(const QString& path, const QString& uniqueHash); /** Returns the thumbnail ids of all thumbnails in the database. */ QList findAll(); BdEngineBackend::QueryState insertUniqueHash(const QString& uniqueHash, qlonglong fileSize, int thumbId); BdEngineBackend::QueryState insertFilePath(const QString& path, int thumbId); BdEngineBackend::QueryState insertCustomIdentifier(const QString& id, int thumbId); BdEngineBackend::QueryState remove(int thumbId); /** Removes thumbnail data associated to the given uniqueHash/fileSize */ BdEngineBackend::QueryState removeByUniqueHash(const QString& uniqueHash, qlonglong fileSize); /** Removes thumbnail data associated to the given file path */ BdEngineBackend::QueryState removeByFilePath(const QString& path); BdEngineBackend::QueryState removeByCustomIdentifier(const QString& id); + BdEngineBackend::QueryState renameByFilePath(const QString& oldPath, const QString& newPath); + BdEngineBackend::QueryState insertThumbnail(const ThumbsDbInfo& info, QVariant* const lastInsertId = 0); BdEngineBackend::QueryState replaceThumbnail(const ThumbsDbInfo& info); QHash getFilePathsWithThumbnail(); void replaceUniqueHash(const QString& oldUniqueHash, int oldFileSize, const QString& newUniqueHash, int newFileSize); BdEngineBackend::QueryState updateModificationDate(int thumbId, const QDateTime& modificationDate); //QStringList getAllThumbnailPaths(); // ----------- Database shrinking methods ---------- /** * Returns true if the integrity of the database is preserved. */ bool integrityCheck(); /** * Shrinks the database. */ void vacuum(); private: explicit ThumbsDb(ThumbsDbBackend* const backend); ~ThumbsDb(); private: class Private; Private* const d; friend class ThumbsDbAccess; }; } // namespace Digikam #endif // DIGIKAM_THUMBS_DB_H diff --git a/core/libs/database/utils/dio.cpp b/core/libs/database/utils/dio.cpp index a6b73ebec1..36049f3165 100644 --- a/core/libs/database/utils/dio.cpp +++ b/core/libs/database/utils/dio.cpp @@ -1,613 +1,614 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2005-05-17 * Description : low level files management interface. * * Copyright (C) 2005 by Renchi Raju * Copyright (C) 2012-2013 by Marcel Wiesweg * Copyright (C) 2015 by Mohamed_Anwer * Copyright (C) 2018 by Maik Qualmann * * 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, 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. * * ============================================================ */ #include "dio.h" // Qt includes #include // Local includes #include "digikam_debug.h" #include "imageinfo.h" #include "albummanager.h" #include "coredb.h" #include "coredbaccess.h" #include "album.h" #include "dmetadata.h" -#include "loadingcacheinterface.h" #include "metadatasettings.h" #include "scancontroller.h" -#include "thumbnailloadthread.h" +#include "thumbsdb.h" +#include "thumbsdbaccess.h" #include "iojobsmanager.h" #include "collectionmanager.h" #include "dnotificationwrapper.h" +#include "loadingcacheinterface.h" #include "progressmanager.h" #include "digikamapp.h" #include "iojobdata.h" namespace Digikam { SidecarFinder::SidecarFinder(const QList& files) { foreach(const QUrl& url, files) { if (DMetadata::hasSidecar(url.toLocalFile())) { localFiles << DMetadata::sidecarUrl(url); localFileSuffixes << QLatin1String(".xmp"); qCDebug(DIGIKAM_DATABASE_LOG) << "Detected a sidecar" << localFiles.last(); } foreach(QString suffix, MetadataSettings::instance()->settings().sidecarExtensions) { suffix = QLatin1String(".") + suffix; QString sidecarName = url.toLocalFile() + suffix; if (QFileInfo::exists(sidecarName) && !localFiles.contains(QUrl::fromLocalFile(sidecarName))) { localFiles << QUrl::fromLocalFile(sidecarName); localFileSuffixes << suffix; qCDebug(DIGIKAM_DATABASE_LOG) << "Detected a sidecar" << localFiles.last(); } } localFiles << url; localFileSuffixes << QString(); } } // ------------------------------------------------------------------------------------------------ // TODO // Groups should not be resolved in dio, it should be handled in views. // This is already done for most things except for drag&drop, which is hard :) GroupedImagesFinder::GroupedImagesFinder(const QList& source) { QSet ids; foreach(const ImageInfo& info, source) { ids << info.id(); } infos.reserve(source.size()); foreach(const ImageInfo& info, source) { infos << info; if (info.hasGroupedImages()) { foreach(const ImageInfo& groupedImage, info.groupedImages()) { if (ids.contains(groupedImage.id())) { continue; } infos << groupedImage; ids << groupedImage.id(); } } } } // ------------------------------------------------------------------------------------------------ class DIOCreator { public: DIO object; }; Q_GLOBAL_STATIC(DIOCreator, creator) // ------------------------------------------------------------------------------------------------ DIO* DIO::instance() { return &creator->object; } DIO::DIO() { } DIO::~DIO() { } void DIO::cleanUp() { } // Album -> Album ----------------------------------------------------- void DIO::copy(PAlbum* const src, PAlbum* const dest) { if (!src || !dest) { return; } instance()->processJob(new IOJobData(IOJobData::CopyAlbum, src, dest)); } void DIO::move(PAlbum* const src, PAlbum* const dest) { if (!src || !dest) { return; } #ifdef Q_OS_WIN AlbumManager::instance()->removeWatchedPAlbums(src); #endif instance()->processJob(new IOJobData(IOJobData::MoveAlbum, src, dest)); } // Images -> Album ---------------------------------------------------- void DIO::copy(const QList& infos, PAlbum* const dest) { if (!dest) { return; } instance()->processJob(new IOJobData(IOJobData::CopyImage, infos, dest)); } void DIO::move(const QList& infos, PAlbum* const dest) { if (!dest) { return; } instance()->processJob(new IOJobData(IOJobData::MoveImage, infos, dest)); } // External files -> album -------------------------------------------- void DIO::copy(const QUrl& src, PAlbum* const dest) { copy(QList() << src, dest); } void DIO::copy(const QList& srcList, PAlbum* const dest) { if (!dest) { return; } instance()->processJob(new IOJobData(IOJobData::CopyFiles, srcList, dest)); } void DIO::move(const QUrl& src, PAlbum* const dest) { move(QList() << src, dest); } void DIO::move(const QList& srcList, PAlbum* const dest) { if (!dest) { return; } instance()->processJob(new IOJobData(IOJobData::MoveFiles, srcList, dest)); } // Rename -------------------------------------------------------------- void DIO::rename(const ImageInfo& info, const QString& newName, bool overwrite) { instance()->processJob(new IOJobData(IOJobData::Rename, info, newName, overwrite)); } // Delete -------------------------------------------------------------- void DIO::del(const QList& infos, bool useTrash) { instance()->processJob(new IOJobData(useTrash ? IOJobData::Trash : IOJobData::Delete, infos)); } void DIO::del(const ImageInfo& info, bool useTrash) { del(QList() << info, useTrash); } void DIO::del(PAlbum* const album, bool useTrash) { if (!album) { return; } #ifdef Q_OS_WIN AlbumManager::instance()->removeWatchedPAlbums(album); #endif instance()->createJob(new IOJobData(useTrash ? IOJobData::Trash : IOJobData::Delete, album)); } // ------------------------------------------------------------------------------------------------ void DIO::processJob(IOJobData* const data) { const int operation = data->operation(); if (operation == IOJobData::CopyImage || operation == IOJobData::MoveImage) { // this is a fast db operation, do here GroupedImagesFinder finder(data->imageInfos()); data->setImageInfos(finder.infos); QStringList filenames; QList ids; foreach(const ImageInfo& info, data->imageInfos()) { filenames << info.name(); ids << info.id(); } ScanController::instance()->hintAtMoveOrCopyOfItems(ids, data->destAlbum(), filenames); } else if (operation == IOJobData::CopyAlbum || operation == IOJobData::MoveAlbum) { ScanController::instance()->hintAtMoveOrCopyOfAlbum(data->srcAlbum(), data->destAlbum()); createJob(data); return; } else if (operation == IOJobData::Delete || operation == IOJobData::Trash) { qCDebug(DIGIKAM_DATABASE_LOG) << "Number of files to be deleted:" << data->sourceUrls().count(); } SidecarFinder finder(data->sourceUrls()); data->setSourceUrls(finder.localFiles); if (operation == IOJobData::Rename) { if (!data->imageInfos().isEmpty()) { ImageInfo info = data->imageInfos().first(); PAlbum* const album = AlbumManager::instance()->findPAlbum(info.albumId()); if (album) { ScanController::instance()->hintAtMoveOrCopyOfItem(info.id(), album, data->destUrl().fileName()); } for (int i = 0 ; i < finder.localFiles.length() ; ++i) { data->setDestUrl(finder.localFiles.at(i), QUrl::fromLocalFile(data->destUrl().toLocalFile() + finder.localFileSuffixes.at(i))); } } } createJob(data); } void DIO::createJob(IOJobData* const data) { if (data->sourceUrls().isEmpty()) { delete data; return; } ProgressItem* item = 0; QString itemString = getItemString(data); if (!itemString.isEmpty()) { item = ProgressManager::instance()->createProgressItem(itemString, QString(), true, false); item->setTotalItems(data->sourceUrls().count()); data->setProgressId(item->id()); } IOJobsThread* const jobThread = IOJobsManager::instance()->startIOJobs(data); connect(jobThread, SIGNAL(signalOneProccessed(QUrl)), this, SLOT(slotOneProccessed(QUrl)), Qt::QueuedConnection); connect(jobThread, SIGNAL(finished()), this, SLOT(slotResult())); if (data->operation() == IOJobData::Rename) { connect(jobThread, SIGNAL(signalRenameFailed(QUrl)), this, SIGNAL(signalRenameFailed(QUrl))); } if (item) { connect(item, SIGNAL(progressItemCanceled(ProgressItem*)), jobThread, SLOT(slotCancel())); connect(item, SIGNAL(progressItemCanceled(ProgressItem*)), this, SLOT(slotCancel(ProgressItem*))); } } void DIO::slotResult() { IOJobsThread* const jobThread = dynamic_cast(sender()); if (!jobThread || !jobThread->jobData()) { return; } IOJobData* const data = jobThread->jobData(); if (jobThread->hasErrors() && data->operation() != IOJobData::Rename) { // Pop-up a message about the error. QString errors = jobThread->errorsList().join(QLatin1String("\n")); DNotificationWrapper(QString(), errors, DigikamApp::instance(), DigikamApp::instance()->windowTitle()); } slotCancel(getProgressItem(data)); } void DIO::slotOneProccessed(const QUrl& url) { IOJobsThread* const jobThread = dynamic_cast(sender()); if (!jobThread || !jobThread->jobData()) { return; } IOJobData* const data = jobThread->jobData(); const int operation = data->operation(); if (operation == IOJobData::MoveImage) { ImageInfo info = data->findImageInfo(url); if (!info.isNull() && data->destAlbum()) { CoreDbAccess().db()->moveItem(info.albumId(), info.name(), data->destAlbum()->id(), info.name()); } } else if (operation == IOJobData::Delete) { // Mark the images as obsolete and remove them // from their album and from the grouped PAlbum* const album = data->srcAlbum(); CoreDbAccess access; if (album && album->fileUrl() == url) { // get all deleted albums QList albumsToDelete; QList imagesToRemove; addAlbumChildrenToList(albumsToDelete, album); foreach(int albumId, albumsToDelete) { imagesToRemove << access.db()->getItemIDsInAlbum(albumId); } foreach(const qlonglong& imageId, imagesToRemove) { access.db()->removeAllImageRelationsFrom(imageId, DatabaseRelation::Grouped); } access.db()->removeItemsPermanently(imagesToRemove, albumsToDelete); } else { ImageInfo info = data->findImageInfo(url); if (!info.isNull()) { CoreDbAccess access; access.db()->removeAllImageRelationsFrom(info.id(), DatabaseRelation::Grouped); access.db()->removeItemsPermanently(QList() << info.id(), QList() << info.albumId()); } } } else if (operation == IOJobData::Trash) { ImageInfo info = data->findImageInfo(url); if (!info.isNull()) { CoreDbAccess().db()->removeItems(QList() << info.id(), QList() << info.albumId()); } } else if (operation == IOJobData::Rename) { - // If we rename a file, the name changes. This is equivalent to a move. - // Do this in database, too. ImageInfo info = data->findImageInfo(url); if (!info.isNull()) { - CoreDbAccess().db()->moveItem(info.albumId(), info.name(), - info.albumId(), data->destUrl(url).fileName()); - - // delete thumbnail - ThumbnailLoadThread::deleteThumbnail(url.toLocalFile()); - - QString destPath = data->destUrl(url).toLocalFile(); + QString oldPath = url.toLocalFile(); + QString newName = data->destUrl(url).fileName(); + QString newPath = data->destUrl(url).toLocalFile(); if (data->overwrite()) { - ThumbnailLoadThread::deleteThumbnail(destPath); + ThumbsDbAccess().db()->removeByFilePath(newPath); + LoadingCacheInterface::fileChanged(newPath, false); + CoreDbAccess().db()->deleteItem(info.albumId(), newName); } - LoadingCacheInterface::fileChanged(destPath); + ThumbsDbAccess().db()->renameByFilePath(oldPath, newPath); + // Remove old thumbnails and images from the cache + LoadingCacheInterface::fileChanged(oldPath, false); + // Rename in ImageInfo and database + info.setName(newName); } emit signalRenameSucceeded(url); } // Scan folders for changes QString scanPath; if (operation == IOJobData::CopyImage || operation == IOJobData::CopyAlbum || operation == IOJobData::CopyFiles || operation == IOJobData::MoveImage || operation == IOJobData::MoveAlbum || operation == IOJobData::MoveFiles) { scanPath = data->destUrl().toLocalFile(); } else if (operation == IOJobData::Delete || operation == IOJobData::Trash) { PAlbum* const album = data->srcAlbum(); if (album) { PAlbum* const parent = dynamic_cast(album->parent()); if (parent) { scanPath = parent->fileUrl().toLocalFile(); } } else { scanPath = url.adjusted(QUrl::RemoveFilename).toLocalFile(); } } if (!scanPath.isEmpty()) { ScanController::instance()->scheduleCollectionScanRelaxed(scanPath); } ProgressItem* const item = getProgressItem(data); if (item) { item->advance(1); } } QString DIO::getItemString(IOJobData* const data) const { switch (data->operation()) { case IOJobData::CopyAlbum: return i18n("Copy Album"); case IOJobData::CopyImage: return i18n("Copy Images"); case IOJobData::CopyFiles: return i18n("Copy Files"); case IOJobData::MoveAlbum: return i18n("Move Album"); case IOJobData::MoveImage: return i18n("Move Images"); case IOJobData::MoveFiles: return i18n("Move Files"); case IOJobData::Trash: return i18n("Trash"); case IOJobData::Delete: return i18n("Delete"); default: break; } return QString(); } ProgressItem* DIO::getProgressItem(IOJobData* const data) const { QString itemId = data->getProgressId(); if (itemId.isEmpty()) { return 0; } return ProgressManager::instance()->findItembyId(itemId); } void DIO::slotCancel(ProgressItem* item) { if (item) { item->setComplete(); } } void DIO::addAlbumChildrenToList(QList& list, Album* const album) { // simple recursive helper function if (album) { if (!list.contains(album->id())) { list.append(album->id()); } AlbumIterator it(album); while (it.current()) { addAlbumChildrenToList(list, *it); ++it; } } } } // namespace Digikam diff --git a/core/libs/dmetadata/dmetadata_ffmpeg.cpp b/core/libs/dmetadata/dmetadata_ffmpeg.cpp index 7a68fd5735..7d375d898e 100644 --- a/core/libs/dmetadata/dmetadata_ffmpeg.cpp +++ b/core/libs/dmetadata/dmetadata_ffmpeg.cpp @@ -1,1613 +1,1611 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2018-02-26 * Description : metadata extraction with FFMpeg (libav) * * References : * * FFMpeg metadata review: https://wiki.multimedia.cx/index.php/FFmpeg_Metadata * FFMpeg MP4 parser : https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/mov.c#L298 * Exiv2 XMP video : https://github.com/Exiv2/exiv2/blob/master/src/properties.cpp#L1331 * Exiv2 RIFF tags : https://github.com/Exiv2/exiv2/blob/master/src/riffvideo.cpp#L83 * Apple metadata desc : https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html * Matroska metadata desc: https://matroska.org/technical/specs/tagging/index.html * FFMpeg metadata writer: https://github.com/kritzikratzi/ofxAvCodec/blob/master/src/ofxAvUtils.cpp#L61 * * FFMpeg tags names origin: * * Generic : common tags generated by FFMpeg codecs. * RIFF files : Resource Interchange File Format tags (as AVI). * MKV files : Matroska container tags. * QT files : Quicktime container tags (Apple). * * Copyright (C) 2018 by Gilles Caulier * * 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, 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. * * ============================================================ */ #include "dmetadata.h" // C Ansi includes #include // Qt includes -#include -#include #include #include #include #include // KDE includes #include // Local incudes #include "captionvalues.h" #include "digikam_debug.h" #include "digikam_config.h" #ifdef HAVE_MEDIAPLAYER // Libav includes extern "C" { #include #include #include #include } #endif namespace Digikam { /** Search first occurrence of string in 'map' with keys given by 'lst'. * Return the string match. * If 'xmpTags' is not empty, register XMP tags value with string. */ QString s_setXmpTagStringFromEntry(DMetadata* const meta, const QStringList& lst, const DMetadata::MetaDataMap& map, const QStringList& xmpTags=QStringList()) { foreach (const QString& tag, lst) { DMetadata::MetaDataMap::const_iterator it = map.find(tag); if (it != map.end()) { if (meta && // Protection. !xmpTags.isEmpty()) // If xmpTags is empty, we only return the matching value from the map. { foreach (const QString& tag, xmpTags) { // Only register the tag value if it doesn't exists yet. if (meta->getXmpTagString(tag.toLatin1().data()).isNull()) { meta->setXmpTagString(tag.toLatin1().data(), it.value()); } } } return it.value(); } } return QString(); } QStringList s_keywordsSeparation(const QString& data) { QStringList keywords = data.split(QLatin1String("/")); if (keywords.isEmpty()) { keywords = data.split(QLatin1String(",")); if (keywords.isEmpty()) { keywords = data.split(QLatin1String(" ")); } } return keywords; } qint64 s_secondsSinceJanuary1904(const QDateTime dt) { QDateTime dt1904(QDate(1904, 1, 1), QTime(0, 0, 0)); return dt1904.secsTo(dt); } #ifdef HAVE_MEDIAPLAYER QString s_convertFFMpegFormatToXMP(int format) { QString data; switch (format) { case AV_SAMPLE_FMT_U8: case AV_SAMPLE_FMT_U8P: data = QLatin1String("8Int"); break; case AV_SAMPLE_FMT_S16: case AV_SAMPLE_FMT_S16P: data = QLatin1String("16Int"); break; case AV_SAMPLE_FMT_S32: case AV_SAMPLE_FMT_S32P: data = QLatin1String("32Int"); break; case AV_SAMPLE_FMT_FLT: case AV_SAMPLE_FMT_FLTP: data = QLatin1String("32Float"); break; case AV_SAMPLE_FMT_DBL: // Not supported by XMP spec. case AV_SAMPLE_FMT_DBLP: // Not supported by XMP spec. case AV_SAMPLE_FMT_S64: // Not supported by XMP spec. case AV_SAMPLE_FMT_S64P: // Not supported by XMP spec. case AV_SAMPLE_FMT_NONE: case AV_SAMPLE_FMT_NB: default: data = QLatin1String("Other"); break; // NOTE: where are 'Compressed' and 'Packed' type from XMP spec into FFMPEG ? } return data; } DMetadata::MetaDataMap s_extractFFMpegMetadataEntriesFromDictionary(AVDictionary* const dict) { AVDictionaryEntry* entry = 0; DMetadata::MetaDataMap meta; do { entry = av_dict_get(dict, "", entry, AV_DICT_IGNORE_SUFFIX); if (entry) { if (QString::fromUtf8(entry->key) != QLatin1String("creation_time") || QDateTime::fromString(QString::fromUtf8(entry->value), Qt::ISODate).toMSecsSinceEpoch() != 0) { meta.insert(QString::fromUtf8(entry->key), QString::fromUtf8(entry->value)); } } } while (entry); return meta; } #endif bool DMetadata::loadUsingFFmpeg(const QString& filePath) { #ifdef HAVE_MEDIAPLAYER qCDebug(DIGIKAM_METAENGINE_LOG) << "Parse metadada with FFMpeg:" << filePath; av_register_all(); AVFormatContext* fmt_ctx = avformat_alloc_context(); int ret = avformat_open_input(&fmt_ctx, filePath.toUtf8().data(), NULL, NULL); if (ret < 0) { qCDebug(DIGIKAM_METAENGINE_LOG) << "avformat_open_input error: " << ret; return false; } ret = avformat_find_stream_info(fmt_ctx, NULL); if (ret < 0) { qCDebug(DIGIKAM_METAENGINE_LOG) << "avform_find_stream_info error: " << ret; return false; } QString data; setXmpTagString("Xmp.video.duration", QString::number((int)(1000.0 * (double)fmt_ctx->duration / (double)AV_TIME_BASE))); setXmpTagString("Xmp.xmpDM.duration", QString::number((int)(1000.0 * (double)fmt_ctx->duration / (double)AV_TIME_BASE))); if (fmt_ctx->bit_rate > 0) setXmpTagString("Xmp.video.MaxBitRate", QString::number(fmt_ctx->bit_rate)); setXmpTagString("Xmp.video.StreamCount", QString::number(fmt_ctx->nb_streams)); // To only register one video, one audio stream, and one subtitle stream in XMP metadata. bool vstream = false; bool astream = false; bool sstream = false; for (uint i = 0 ; i < fmt_ctx->nb_streams ; i++) { const AVStream* const stream = fmt_ctx->streams[i]; AVCodecParameters* const codec = stream->codecpar; // ----------------------------------------- // Audio stream parsing // ----------------------------------------- if (!astream && codec->codec_type == AVMEDIA_TYPE_AUDIO) { astream = true; const char* cname = avcodec_get_name(codec->codec_id); setXmpTagString("Xmp.audio.Codec", QString::fromUtf8(cname)); setXmpTagString("Xmp.audio.CodecDescription", QString::fromUtf8(avcodec_descriptor_get_by_name(cname)->long_name)); setXmpTagString("Xmp.audio.SampleRate", QString::number(codec->sample_rate)); setXmpTagString("Xmp.xmpDM.audioSampleRate", QString::number(codec->sample_rate)); // See XMP Dynamic Media properties from Adobe. // Audio Channel type is a limited untranslated string choice depending of amount of audio channels data = QString(); switch (codec->channels) { case 0: break; case 1: data = QLatin1String("Mono"); break; case 2: data = QLatin1String("Stereo"); break; case 6: data = QLatin1String("5.1"); break; case 8: data = QLatin1String("7.1"); break; case 16: data = QLatin1String("16 Channel"); break; default: data = QLatin1String("Other"); break; } if (!data.isEmpty()) { setXmpTagString("Xmp.audio.ChannelType", data); setXmpTagString("Xmp.xmpDM.audioChannelType", data); } setXmpTagString("Xmp.audio.Format", QString::fromUtf8(av_get_sample_fmt_name((AVSampleFormat)codec->format))); // See XMP Dynamic Media properties from Adobe. // Audio Sample type is a limited untranslated string choice depending of amount of audio samples data = s_convertFFMpegFormatToXMP(codec->format); if (!data.isEmpty()) { setXmpTagString("Xmp.audio.SampleType", data); setXmpTagString("Xmp.xmpDM.audioSampleType", data); } // -------------- MetaDataMap ameta = s_extractFFMpegMetadataEntriesFromDictionary(stream->metadata); qCDebug(DIGIKAM_METAENGINE_LOG) << "-- FFMpeg audio stream metadata entries :"; qCDebug(DIGIKAM_METAENGINE_LOG) << ameta; qCDebug(DIGIKAM_METAENGINE_LOG) << "-----------------------------------------"; // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("language"), // Generic. ameta, QStringList() << QLatin1String("Xmp.audio.TrackLang")); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("creation_time"), // Generic. ameta); if (!data.isEmpty()) { - QDateTime dt = QDateTime::fromString(data, Qt::ISODate); + QDateTime dt = QDateTime::fromString(data, Qt::ISODate).toLocalTime(); setXmpTagString("Xmp.audio.TrackCreateDate", QString::number(s_secondsSinceJanuary1904(dt))); } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("handler_name"), // Generic. ameta, QStringList() << QLatin1String("Xmp.audio.HandlerDescription")); } // ----------------------------------------- // Video stream parsing // ----------------------------------------- if (!vstream && codec->codec_type == AVMEDIA_TYPE_VIDEO) { vstream = true; - const char* cname = avcodec_get_name(codec->codec_id); + const char* cname = avcodec_get_name(codec->codec_id); setXmpTagString("Xmp.video.Codec", QString::fromUtf8(cname)); setXmpTagString("Xmp.video.CodecDescription", QString::fromUtf8(avcodec_descriptor_get_by_name(cname)->long_name)); setXmpTagString("Xmp.video.Format", QString::fromUtf8(av_get_pix_fmt_name((AVPixelFormat)codec->format))); // Store in this tag the full description off FFMPEG video color space. setXmpTagString("Xmp.video.ColorMode", QString::fromUtf8(av_color_space_name((AVColorSpace)codec->color_space))); VIDEOCOLORMODEL cm = VIDEOCOLORMODEL_OTHER; switch (codec->color_space) { case AVCOL_SPC_RGB: cm = VIDEOCOLORMODEL_SRGB; break; case AVCOL_SPC_BT470BG: case AVCOL_SPC_SMPTE170M: case AVCOL_SPC_SMPTE240M: cm = VIDEOCOLORMODEL_BT601; break; case AVCOL_SPC_BT709: cm = VIDEOCOLORMODEL_BT709; break; case AVCOL_SPC_UNSPECIFIED: case AVCOL_SPC_RESERVED: case AVCOL_SPC_NB: cm = VIDEOCOLORMODEL_UNKNOWN; break; default: break; } // See XMP Dynamic Media properties from Adobe. // Video Color Space is a limited untranslated string choice depending of video color space value. data = videoColorModelToString(cm); if (!data.isEmpty()) { setXmpTagString("Xmp.video.ColorSpace", data); setXmpTagString("Xmp.xmpDM.videoColorSpace", data); } // ---------- QString fo; switch (codec->field_order) { case AV_FIELD_PROGRESSIVE: fo = QLatin1String("Progressive"); break; case AV_FIELD_TT: // Top coded first, top displayed first case AV_FIELD_BT: // Bottom coded first, top displayed first fo = QLatin1String("Upper"); break; case AV_FIELD_BB: // Bottom coded first, bottom displayed first case AV_FIELD_TB: // Top coded first, bottom displayed first fo = QLatin1String("Lower"); break; default: break; } if (!fo.isEmpty()) { setXmpTagString("Xmp.xmpDM.FieldOrder", fo); } // ---------- QString aspectRatio; int frameRate = -1.0; if (codec->sample_aspect_ratio.num != 0) // Check if undefined by ffmpeg { AVRational displayAspectRatio; av_reduce(&displayAspectRatio.num, &displayAspectRatio.den, codec->width * (int64_t)codec->sample_aspect_ratio.num, codec->height * (int64_t)codec->sample_aspect_ratio.den, 1024 * 1024); aspectRatio = QString::fromLatin1("%1/%2").arg(displayAspectRatio.num) .arg(displayAspectRatio.den); } else if (codec->height) { aspectRatio = QString::fromLatin1("%1/%2").arg(codec->width) .arg(codec->height); } if (stream->avg_frame_rate.den) { frameRate = (double)stream->avg_frame_rate.num / (double)stream->avg_frame_rate.den; } setXmpTagString("Xmp.video.Width", QString::number(codec->width)); setXmpTagString("Xmp.video.FrameWidth", QString::number(codec->width)); setXmpTagString("Xmp.video.SourceImageWidth", QString::number(codec->width)); setXmpTagString("Xmp.video.Height", QString::number(codec->height)); setXmpTagString("Xmp.video.FrameHeight", QString::number(codec->height)); setXmpTagString("Xmp.video.SourceImageHeight", QString::number(codec->height)); setXmpTagString("Xmp.video.FrameSize", QString::fromLatin1("w:%1, h:%2, unit:pixels").arg(codec->width).arg(codec->height)); setXmpTagString("Xmp.xmpDM.videoFrameSize", QString::fromLatin1("w:%1, h:%2, unit:pixels").arg(codec->width).arg(codec->height)); // Backport size in Exif and Iptc setImageDimensions(QSize(codec->width, codec->height)); if (!aspectRatio.isEmpty()) { setXmpTagString("Xmp.video.AspectRatio", aspectRatio); setXmpTagString("Xmp.xmpDM.videoPixelAspectRatio", aspectRatio); } if (frameRate != -1.0) { setXmpTagString("Xmp.video.FrameRate", QString::number(frameRate)); // See XMP Dynamic Media properties from Adobe. // Video Color Space is a limited untranslated string choice depending of video frame rate. // https://documentation.apple.com/en/finalcutpro/usermanual/index.html#chapter=D%26section=4%26tasks=true - + data = QLatin1String("Other"); - + if (frameRate == 24.0) data = QLatin1String("24"); else if (frameRate == 23.98 || frameRate == 29.97 || frameRate == 30.0 || frameRate == 59.94) data = QLatin1String("NTSC"); else if (frameRate == 25 || frameRate == 50) data = QLatin1String("PAL"); - + setXmpTagString("Xmp.xmpDM.videoFrameRate", data); } setXmpTagString("Xmp.video.BitDepth", QString::number(codec->bits_per_coded_sample)); - + // See XMP Dynamic Media properties from Adobe. // Video Pixel Depth is a limited untranslated string choice depending of amount of samples format. data = s_convertFFMpegFormatToXMP(codec->format); if (!data.isEmpty()) setXmpTagString("Xmp.xmpDM.videoPixelDepth", data); // ----------------------------------------- MetaDataMap vmeta = s_extractFFMpegMetadataEntriesFromDictionary(stream->metadata); qCDebug(DIGIKAM_METAENGINE_LOG) << "-- FFMpeg video stream metadata entries :"; qCDebug(DIGIKAM_METAENGINE_LOG) << vmeta; qCDebug(DIGIKAM_METAENGINE_LOG) << "-----------------------------------------"; // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("rotate"), // Generic. vmeta); if (!data.isEmpty()) { bool b = false; int val = data.toInt(&b); ImageOrientation ori = ORIENTATION_UNSPECIFIED; if (b) { switch (val) { case 0: ori = ORIENTATION_NORMAL; break; case 90: ori = ORIENTATION_ROT_90; break; case 180: ori = ORIENTATION_ROT_180; break; case 270: ori = ORIENTATION_ROT_270; break; default: break; } setXmpTagString("Xmp.video.Orientation", QString::number(ori)); // Backport orientation in Exif setImageOrientation(ori); } } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("language") // Generic. << QLatin1String("ILNG") // RIFF files. << QLatin1String("LANG"), // RIFF files. vmeta, QStringList() << QLatin1String("Xmp.video.Language")); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("creation_time") // Generic. << QLatin1String("_STATISTICS_WRITING_DATE_UTC"), // MKV files. vmeta); if (!data.isEmpty()) { - QDateTime dt = QDateTime::fromString(data, Qt::ISODate); + QDateTime dt = QDateTime::fromString(data, Qt::ISODate).toLocalTime(); setXmpTagString("Xmp.video.TrackCreateDate", QString::number(s_secondsSinceJanuary1904(dt))); setXmpTagString("Xmp.xmpDM.shotDate", dt.toString()); } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("handler_name"), // Generic. vmeta, QStringList() << QLatin1String("Xmp.video.HandlerDescription")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("TVER") // RIFF files. << QLatin1String("_STATISTICS_WRITING_APP"), // MKV files. vmeta, QStringList() << QLatin1String("Xmp.video.SoftwareVersion")); } // ----------------------------------------- // Subtitle stream parsing // ----------------------------------------- if (!sstream && codec->codec_type == AVMEDIA_TYPE_SUBTITLE) { sstream = true; - const char* cname = avcodec_get_name(codec->codec_id); + const char* cname = avcodec_get_name(codec->codec_id); setXmpTagString("Xmp.video.SubTCodec", QString::fromUtf8(cname)); setXmpTagString("Xmp.video.SubTCodecInfo", QString::fromUtf8(avcodec_descriptor_get_by_name(cname)->long_name)); // ----------------------------------------- MetaDataMap smeta = s_extractFFMpegMetadataEntriesFromDictionary(stream->metadata); qCDebug(DIGIKAM_METAENGINE_LOG) << "-- FFMpeg subtitle stream metadata entries :"; qCDebug(DIGIKAM_METAENGINE_LOG) << smeta; qCDebug(DIGIKAM_METAENGINE_LOG) << "--------------------------------------------"; // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("subtitle") // Generic. << QLatin1String("title"), // Generic. smeta, QStringList() << QLatin1String("Xmp.video.Subtitle")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("language"), // Generic. smeta, QStringList() << QLatin1String("Xmp.video.SubTLang")); } } // ----------------------------------------- // Root container parsing // ----------------------------------------- MetaDataMap rmeta = s_extractFFMpegMetadataEntriesFromDictionary(fmt_ctx->metadata); qCDebug(DIGIKAM_METAENGINE_LOG) << "-- FFMpeg root container metadata entries :"; qCDebug(DIGIKAM_METAENGINE_LOG) << rmeta; qCDebug(DIGIKAM_METAENGINE_LOG) << "------------------------------------------"; // ---------------------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("major_brand"), // Generic. rmeta, QStringList() << QLatin1String("Xmp.video.MajorBrand")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("compatible_brands"), // Generic. rmeta, QStringList() << QLatin1String("Xmp.video.CompatibleBrands")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("minor_version"), // Generic. rmeta, QStringList() << QLatin1String("Xmp.video.MinorVersion")); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("keywords") // Generic. << QLatin1String("IMIT") // RIFF files. << QLatin1String("KEYWORDS") // MKV files. << QLatin1String("com.apple.quicktime.keywords"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.InfoText")); if (!data.isEmpty()) { QStringList keywords = s_keywordsSeparation(data); if (!keywords.isEmpty()) { setXmpKeywords(keywords); setIptcKeywords(QStringList(), keywords); } } // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("category") // Generic. << QLatin1String("ISBJ") // RIFF files. << QLatin1String("SUBJECT"), // MKV files. rmeta, QStringList() << QLatin1String("Xmp.video.Subject")); if (!data.isEmpty()) { QStringList categories = s_keywordsSeparation(data); if (!categories.isEmpty()) { setXmpSubCategories(categories); setIptcSubCategories(QStringList(), categories); } } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("premiere_version") // Generic. << QLatin1String("quicktime_version") // Generic. << QLatin1String("ISFT") // Riff files << QLatin1String("com.apple.quicktime.software"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.SoftwareVersion")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("firmware") // Generic. << QLatin1String("com.apple.proapps.serialno"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.FirmwareVersion")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("composer") // Generic. << QLatin1String("COMPOSER"), // MKV files rmeta, QStringList() << QLatin1String("Xmp.video.Composer") << QLatin1String("Xmp.xmpDM.composer")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("com.apple.quicktime.displayname"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Name")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("playback_requirements"), // Generic. rmeta, QStringList() << QLatin1String("Xmp.video.Requirements")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("lyrics"), // Generic. rmeta, QStringList() << QLatin1String("Xmp.video.Lyrics")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("filename"), // Generic. rmeta, QStringList() << QLatin1String("Xmp.video.FileName")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("disk"), // Generic. rmeta, QStringList() << QLatin1String("Xmp.xmpDM.discNumber")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("performers"), // Generic. rmeta, QStringList() << QLatin1String("Xmp.video.Performers")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("producer") // Generic. << QLatin1String("PRODUCER") // MKV files. << QLatin1String("com.apple.quicktime.producer"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Producer")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("artist") // Generic. << QLatin1String("album_artist") // Generic. << QLatin1String("original_artist") // Generic. << QLatin1String("com.apple.quicktime.artist") // QT files. << QLatin1String("IART") // RIFF files. << QLatin1String("ARTIST") // MKV files. << QLatin1String("author") // Generic. << QLatin1String("com.apple.quicktime.author"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Artist") << QLatin1String("Xmp.xmpDM.artist")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("director") // Generic. << QLatin1String("DIRC") // RIFF files. << QLatin1String("DIRECTOR") // MKV files. << QLatin1String("com.apple.quicktime.director"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Director") << QLatin1String("Xmp.xmpDM.director")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("media_type") // Generic. << QLatin1String("IMED") // RIFF files. << QLatin1String("ORIGINAL_MEDIA_TYPE"), // MKV files. rmeta, QStringList() << QLatin1String("Xmp.video.Medium")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("grouping"), // Generic. rmeta, QStringList() << QLatin1String("Xmp.video.Grouping")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("BPS"), // MKV files rmeta, QStringList() << QLatin1String("Xmp.video.MaxBitRate")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ISRC"), // MKV files rmeta, QStringList() << QLatin1String("Xmp.video.ISRCCode")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("CONTENT_TYPE"), // MKV files rmeta, QStringList() << QLatin1String("Xmp.video.ExtendedContentDescription")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("FPS"), // MKV files. rmeta, QStringList() << QLatin1String("Xmp.video.videoFrameRate") << QLatin1String("Xmp.xmpDM.FrameRate")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("encoder") // Generic. << QLatin1String("ENCODER"), // MKV files. rmeta, QStringList() << QLatin1String("Xmp.video.Encoder")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("com.apple.proapps.clipID"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.FileID")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("original_source") // Generic. << QLatin1String("ISRC") // Riff files << QLatin1String("com.apple.proapps.cameraName"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Source")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("original_format") // Generic. << QLatin1String("com.apple.proapps.originalFormat"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Format")); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("rating") // Generic. << QLatin1String("IRTD") // RIFF files. << QLatin1String("RATE") // RIFF files. << QLatin1String("RATING") // MKV files. << QLatin1String("com.apple.quicktime.rating.user"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Rating") << QLatin1String("Xmp.video.Rate")); if (!data.isEmpty()) { // Backport rating in Exif and Iptc bool b = false; int rating = data.toInt(&b); if (b) { setImageRating(rating); } } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("make") // Generic. << QLatin1String("com.apple.quicktime.make"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Make")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("model") // Generic. << QLatin1String("com.apple.quicktime.model"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Model") << QLatin1String("Xmp.xmpDM.cameraModel")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("URL") // Generic. << QLatin1String("TURL"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.URL")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("title") // Generic. << QLatin1String("INAM") // RIFF files. << QLatin1String("TITL") // RIFF files. << QLatin1String("TITLE") // MKV files. << QLatin1String("com.apple.quicktime.title"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Title") << QLatin1String("Xmp.xmpDM.shotName")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("copyright") // Generic. << QLatin1String("ICOP") // RIFF files. << QLatin1String("COPYRIGHT") // MKV files. << QLatin1String("com.apple.quicktime.copyright"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Copyright") << QLatin1String("Xmp.xmpDM.copyright")); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("comment") // Generic. << QLatin1String("description") // Generic. << QLatin1String("CMNT") // Riff Files. << QLatin1String("COMN") // Riff Files. << QLatin1String("ICMT") // Riff Files. << QLatin1String("COMMENT") // MKV Files. << QLatin1String("DESCRIPTION") // MKV Files. << QLatin1String("com.apple.quicktime.description"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Comment") << QLatin1String("Xmp.xmpDM.logComment")); if (!data.isEmpty()) { // Backport comment in Exif and Iptc CaptionsMap capMap; MetaEngine::AltLangMap comMap; comMap.insert(QLatin1String("x-default"), data); capMap.setData(comMap, MetaEngine::AltLangMap(), QString(), MetaEngine::AltLangMap()); setImageComments(capMap); } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("synopsis") // Generic. << QLatin1String("SUMMARY") // MKV files. << QLatin1String("SYNOPSIS"), // MKV files. rmeta, QStringList() << QLatin1String("Xmp.video.Information")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("lyrics") // Generic. << QLatin1String("LYRICS"), // MKV files. rmeta, QStringList() << QLatin1String("Xmp.xmpDM.lyrics")); // -------------- for (int i = 1 ; i <= 9 ; i++) { s_setXmpTagStringFromEntry(this, QStringList() << QString::fromLatin1("IAS%1").arg(i), // RIFF files. rmeta, QStringList() << QString::fromLatin1("Xmp.video.Edit%1").arg(i)); } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("encoded_by") // Generic. << QLatin1String("CODE") // RIFF files. << QLatin1String("IECN") // RIFF files. << QLatin1String("ENCODED_BY"), // MKV files. rmeta, QStringList() << QLatin1String("Xmp.video.EncodedBy")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("DISP"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.SchemeTitle")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("AGES") // RIFF files. << QLatin1String("ICRA") // MKV files. << QLatin1String("LAW_RATING"), // MKV files. rmeta, QStringList() << QLatin1String("Xmp.video.Rated")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IBSU"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.BaseURL")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ICAS"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.DefaultStream")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ICDS"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.CostumeDesigner")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ICMS"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Commissioned")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ICNM"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Cinematographer")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ICNT"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Country")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IARL"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.ArchivalLocation")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ICRP"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Cropped")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IDIM"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Dimensions")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IDPI"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.DotsPerInch")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IDST") // RIFF files. << QLatin1String("DISTRIBUTED_BY"), // MKV files. rmeta, QStringList() << QLatin1String("Xmp.video.DistributedBy")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IEDT"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.EditedBy")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IENG"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Engineer") << QLatin1String("Xmp.xmpDM.engineer")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IKEY"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.PerformerKeywords")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ILGT"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Lightness")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ILGU"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.LogoURL")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ILIU"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.LogoIconURL")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IMBI"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.InfoBannerImage")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IMBU"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.InfoBannerURL")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IMIU"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.InfoURL")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IMUS"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.MusicBy")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IPDS"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.ProductionDesigner")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IPLT"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.NumOfColors")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IPRD"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Product")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IPRO"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.ProducedBy")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IRIP"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.RippedBy")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ISGN"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.SecondaryGenre")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ISHP"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Sharpness")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ISRF"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.SourceForm")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ISTD"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.ProductionStudio")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ISTR") // RIFF files. << QLatin1String("STAR"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Starring")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ITCH"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Technician")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IWMU"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.WatermarkURL")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("IWRI"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.WrittenBy")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("PRT1"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Part")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("PRT2"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.NumOfParts")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("STAT"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Statistics")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("TAPE"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.TapeName")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("TCDO"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.EndTimecode")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("TCOD"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.StartTimecode")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("TLEN"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Length")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("TORG"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.Organization")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("VMAJ"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.VegasVersionMajor")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("VMIN"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.VegasVersionMinor")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("LOCA"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.LocationInfo") << QLatin1String("Xmp.xmpDM.shotLocation")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("album") // Generic. << QLatin1String("com.apple.quicktime.album"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Album") << QLatin1String("Xmp.xmpDM.album")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("genre") // Generic. << QLatin1String("GENR") // RIFF files. << QLatin1String("IGNR") // RIFF files. << QLatin1String("GENRE") // MKV files. << QLatin1String("com.apple.quicktime.genre"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.Genre") << QLatin1String("Xmp.xmpDM.genre")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("track") // Generic. << QLatin1String("TRCK"), // RIFF files. rmeta, QStringList() << QLatin1String("Xmp.video.TrackNumber") << QLatin1String("Xmp.xmpDM.trackNumber")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("year") // Generic. << QLatin1String("YEAR") // RIFF files. << QLatin1String("com.apple.quicktime.year"), rmeta, QStringList() << QLatin1String("Xmp.video.Year")); // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("ICRD") // Riff files << QLatin1String("DATE_DIGITIZED"), // MKV files rmeta, QStringList() << QLatin1String("Xmp.video.DateTimeDigitized")); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("creation_time") // Generic. << QLatin1String("DTIM") // RIFF files. << QLatin1String("DATE_RECORDED") // MKV files. << QLatin1String("com.apple.quicktime.creationdate"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.DateTimeOriginal")); if (!data.isEmpty()) { // Backport date in Exif and Iptc. - QDateTime dt = QDateTime::fromString(data, Qt::ISODate); + QDateTime dt = QDateTime::fromString(data, Qt::ISODate).toLocalTime(); setImageDateTime(dt, true); } // -------------- s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("edit_date"), // Generic. rmeta, QStringList() << QLatin1String("Xmp.video.ModificationDate") << QLatin1String("Xmp.xmpDM.videoModDate")); // -------------- data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("date") // Generic. << QLatin1String("DATE_RELEASED"), // MKV files. rmeta); if (!data.isEmpty()) { - QDateTime dt = QDateTime::fromString(data, Qt::ISODate); + QDateTime dt = QDateTime::fromString(data, Qt::ISODate).toLocalTime(); setXmpTagString("Xmp.video.MediaCreateDate", QString::number(s_secondsSinceJanuary1904(dt))); } // -------------- // GPS info as string. ex: "+44.8511-000.6229/" // Defined in ISO 6709:2008. // Notes: altitude can be passed as 3rd values. // each value is separated from others by '-' or '+'. // '/' is always the terminaison character. data = s_setXmpTagStringFromEntry(this, QStringList() << QLatin1String("location") // Generic. << QLatin1String("RECORDING_LOCATION") // MKV files. << QLatin1String("com.apple.quicktime.location.ISO6709"), // QT files. rmeta, QStringList() << QLatin1String("Xmp.video.GPSCoordinates") << QLatin1String("Xmp.xmpDM.shotLocation")); if (!data.isEmpty()) { // Backport location to Exif. QList digits; for (int i = 0 ; i < data.length() ; i++) { QChar c = data[i]; if (c == QLatin1Char('+') || c == QLatin1Char('-') || c == QLatin1Char('/')) { digits << i; } } QString coord; double lattitude = 0.0; double longitude = 0.0; double altitude = 0.0; bool b1 = false; bool b2 = false; bool b3 = false; if (digits.size() > 1) { coord = data.mid(digits[0], digits[1] - digits[0]); lattitude = coord.toDouble(&b1); } if (digits.size() > 2) { coord = data.mid(digits[1], digits[2] - digits[1]); longitude = coord.toDouble(&b2); } if (digits.size() > 3) { coord = data.mid(digits[2], digits[3] - digits[2]); altitude = coord.toDouble(&b3); } if (b1 && b2) { if (b3) { // All GPS values are available. setGPSInfo(altitude, lattitude, longitude); setXmpTagString("Xmp.video.GPSAltitude", getXmpTagString("Xmp.exif.GPSAltitude")); setXmpTagString("Xmp.exif.GPSAltitude", getXmpTagString("Xmp.exif.GPSAltitude")); } else { // No altitude available. double* alt = 0; setGPSInfo(alt, lattitude, longitude); } setXmpTagString("Xmp.video.GPSLatitude", getXmpTagString("Xmp.exif.GPSLatitude")); setXmpTagString("Xmp.video.GPSLongitude", getXmpTagString("Xmp.exif.GPSLongitude")); setXmpTagString("Xmp.video.GPSMapDatum", getXmpTagString("Xmp.exif.GPSMapDatum")); setXmpTagString("Xmp.video.GPSVersionID", getXmpTagString("Xmp.exif.GPSVersionID")); setXmpTagString("Xmp.exif.GPSLatitude", getXmpTagString("Xmp.exif.GPSLatitude")); setXmpTagString("Xmp.exif.GPSLongitude", getXmpTagString("Xmp.exif.GPSLongitude")); setXmpTagString("Xmp.exif.GPSMapDatum", getXmpTagString("Xmp.exif.GPSMapDatum")); setXmpTagString("Xmp.exif.GPSVersionID", getXmpTagString("Xmp.exif.GPSVersionID")); } } avformat_close_input(&fmt_ctx); QFileInfo fi(filePath); if (getXmpTagString("Xmp.video.FileName").isNull()) setXmpTagString("Xmp.video.FileName", fi.fileName()); if (getXmpTagString("Xmp.video.FileSize").isNull()) setXmpTagString("Xmp.video.FileSize", QString::number(fi.size() / (1024*1024))); if (getXmpTagString("Xmp.video.FileType").isNull()) setXmpTagString("Xmp.video.FileType", fi.suffix()); if (getXmpTagString("Xmp.video.MimeType").isNull()) setXmpTagString("Xmp.video.MimeType", QMimeDatabase().mimeTypeForFile(filePath).name()); return true; #else Q_UNUSED(filePath); return false; #endif } QString DMetadata::videoColorModelToString(VIDEOCOLORMODEL videoColorModel) { QString cs; switch (videoColorModel) { case VIDEOCOLORMODEL_SRGB: cs = QLatin1String("sRGB"); break; case VIDEOCOLORMODEL_BT601: cs = QLatin1String("CCIR-601"); break; case VIDEOCOLORMODEL_BT709: cs = QLatin1String("CCIR-709"); break; case VIDEOCOLORMODEL_OTHER: cs = QLatin1String("Other"); break; default: // VIDEOCOLORMODEL_UNKNOWN break; } return cs; } } // namespace Digikam diff --git a/core/libs/models/imagealbummodel.cpp b/core/libs/models/imagealbummodel.cpp index 0267d5b59c..cbd0a60cb5 100644 --- a/core/libs/models/imagealbummodel.cpp +++ b/core/libs/models/imagealbummodel.cpp @@ -1,767 +1,768 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-03-08 * Description : Qt item model for database entries, listing done with ioslave * * Copyright (C) 2009-2011 by Marcel Wiesweg * Copyright (C) 2015 by Mohamed_Anwer * * 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, 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. * * ============================================================ */ #include "imagealbummodel.h" // Qt includes #include // Local includes #include "digikam_debug.h" #include "albummanager.h" #include "coredbaccess.h" #include "coredbchangesets.h" #include "facetagsiface.h" #include "coredbwatch.h" #include "coredburl.h" #include "imageinfo.h" #include "imageinfolist.h" #include "imagelister.h" #include "dnotificationwrapper.h" #include "digikamapp.h" #include "dbjobsmanager.h" #include "dbjobsthread.h" namespace Digikam { class ImageAlbumModel::Private { public: explicit Private() { jobThread = 0; refreshTimer = 0; incrementalTimer = 0; recurseAlbums = false; recurseTags = false; listOnlyAvailableImages = false; extraValueJob = false; } QList currentAlbums; DBJobsThread* jobThread; QTimer* refreshTimer; QTimer* incrementalTimer; bool recurseAlbums; bool recurseTags; bool listOnlyAvailableImages; QString specialListing; bool extraValueJob; }; ImageAlbumModel::ImageAlbumModel(QObject* const parent) : ImageThumbnailModel(parent), d(new Private) { qRegisterMetaType>("QList"); d->refreshTimer = new QTimer(this); d->refreshTimer->setSingleShot(true); d->incrementalTimer = new QTimer(this); d->incrementalTimer->setSingleShot(true); connect(d->refreshTimer, SIGNAL(timeout()), this, SLOT(slotNextRefresh())); connect(d->incrementalTimer, SIGNAL(timeout()), this, SLOT(slotNextIncrementalRefresh())); connect(this, SIGNAL(readyForIncrementalRefresh()), this, SLOT(incrementalRefresh())); connect(CoreDbAccess::databaseWatch(), SIGNAL(collectionImageChange(CollectionImageChangeset)), this, SLOT(slotCollectionImageChange(CollectionImageChangeset))); connect(CoreDbAccess::databaseWatch(), SIGNAL(searchChange(SearchChangeset)), this, SLOT(slotSearchChange(SearchChangeset))); connect(AlbumManager::instance(), SIGNAL(signalAlbumAdded(Album*)), this, SLOT(slotAlbumAdded(Album*))); connect(AlbumManager::instance(), SIGNAL(signalAlbumDeleted(Album*)), this, SLOT(slotAlbumDeleted(Album*))); connect(AlbumManager::instance(), SIGNAL(signalAlbumRenamed(Album*)), this, SLOT(slotAlbumRenamed(Album*))); connect(AlbumManager::instance(), SIGNAL(signalAlbumsCleared()), this, SLOT(slotAlbumsCleared())); connect(AlbumManager::instance(), SIGNAL(signalShowOnlyAvailableAlbumsChanged(bool)), this, SLOT(setListOnlyAvailableImages(bool))); } ImageAlbumModel::~ImageAlbumModel() { if (d->jobThread) { d->jobThread->cancel(); d->jobThread = 0; } delete d; } QList ImageAlbumModel::currentAlbums() const { return d->currentAlbums; } void ImageAlbumModel::setRecurseAlbums(bool recursiveListing) { if (d->recurseAlbums != recursiveListing) { d->recurseAlbums = recursiveListing; refresh(); } } void ImageAlbumModel::setRecurseTags(bool recursiveListing) { if (d->recurseTags != recursiveListing) { d->recurseTags = recursiveListing; refresh(); } } void ImageAlbumModel::setListOnlyAvailableImages(bool onlyAvailable) { if (d->listOnlyAvailableImages!= onlyAvailable) { d->listOnlyAvailableImages = onlyAvailable; refresh(); } } bool ImageAlbumModel::isRecursingAlbums() const { return d->recurseAlbums; } bool ImageAlbumModel::isRecursingTags() const { return d->recurseTags; } bool ImageAlbumModel::isListingOnlyAvailableImages() const { return d->listOnlyAvailableImages; } void ImageAlbumModel::setSpecialTagListing(const QString& specialListing) { if (d->specialListing != specialListing) { d->specialListing = specialListing; refresh(); } } -void ImageAlbumModel::openAlbum(QList albums) +void ImageAlbumModel::openAlbum(const QList& albums) { if (d->currentAlbums == albums) { return; } d->currentAlbums.clear(); /** * Extra safety, ensure that no null pointers are added */ foreach(Album* const a, albums) { if (a) { d->currentAlbums << a; } } //emit listedAlbumChanged(d->currentAlbums); refresh(); } void ImageAlbumModel::refresh() { if (d->jobThread) { d->jobThread->cancel(); d->jobThread = 0; } clearImageInfos(); if (d->currentAlbums.isEmpty()) { return; } /** TODO: Figure out how to deal with root album if (d->currentAlbum->isRoot()) { return; } */ startRefresh(); startListJob(d->currentAlbums); } void ImageAlbumModel::incrementalRefresh() { // The path to this method is: // scheduleIncrementalRefresh -> incrementalTimer waits 100ms -> slotNextIncrementalRefresh // -> ImageModel::requestIncrementalRefresh -> waits until model is ready, maybe immediately // -> to this method via SIGNAL(readyForIncrementalRefresh()) if (d->currentAlbums.isEmpty()) { return; } if (d->jobThread) { d->jobThread->cancel(); d->jobThread = 0; } startIncrementalRefresh(); startListJob(d->currentAlbums); } bool ImageAlbumModel::hasScheduledRefresh() const { return d->refreshTimer->isActive() || d->incrementalTimer->isActive() || hasIncrementalRefreshPending(); } void ImageAlbumModel::scheduleRefresh() { if (!d->refreshTimer->isActive()) { d->incrementalTimer->stop(); d->refreshTimer->start(100); } } void ImageAlbumModel::scheduleIncrementalRefresh() { if (!hasScheduledRefresh()) { d->incrementalTimer->start(100); } } void ImageAlbumModel::slotNextRefresh() { // Refresh, unless job is running, then postpone restart until job is finished // Rationale: Let the job run, don't stop it possibly several times if (d->jobThread) { d->refreshTimer->start(50); } else { refresh(); } } void ImageAlbumModel::slotNextIncrementalRefresh() { if (d->jobThread) { d->incrementalTimer->start(50); } else { requestIncrementalRefresh(); } } -void ImageAlbumModel::startListJob(QList albums) +void ImageAlbumModel::startListJob(const QList& albums) { if (albums.isEmpty()) { return; } if (d->jobThread) { d->jobThread->cancel(); d->jobThread = 0; } CoreDbUrl url; QList ids; if (albums.first()->isTrashAlbum()) { return; } if (albums.first()->type() == Album::TAG || albums.first()->type() == Album::SEARCH) { - for (QList::iterator it = albums.begin() ; it != albums.end() ; ++it) + for (QList::const_iterator it = albums.constBegin() ; it != albums.constEnd() ; ++it) { ids << (*it)->id(); } if (albums.first()->type() == Album::TAG) { url = CoreDbUrl::fromTagIds(ids); } } else { url = albums.first()->databaseUrl(); } if (albums.first()->type() == Album::DATE) { d->extraValueJob = false; DatesDBJobInfo jobInfo; jobInfo.setStartDate(url.startDate()); jobInfo.setEndDate(url.endDate()); if (d->recurseAlbums) jobInfo.setRecursive(); if (d->listOnlyAvailableImages) jobInfo.setListAvailableImagesOnly(); d->jobThread = DBJobsManager::instance()->startDatesJobThread(jobInfo); } else if (albums.first()->type() == Album::TAG) { d->extraValueJob = false; TagsDBJobInfo jobInfo; if (d->listOnlyAvailableImages) jobInfo.setListAvailableImagesOnly(); if (d->recurseTags) jobInfo.setRecursive(); jobInfo.setTagsIds(ids); if (!d->specialListing.isNull()) { jobInfo.setSpecialTag(d->specialListing); d->extraValueJob = true; } d->jobThread = DBJobsManager::instance()->startTagsJobThread(jobInfo); } else if (albums.first()->type() == Album::PHYSICAL) { d->extraValueJob = false; AlbumsDBJobInfo jobInfo; if (d->recurseAlbums) jobInfo.setRecursive(); if (d->listOnlyAvailableImages) jobInfo.setListAvailableImagesOnly(); jobInfo.setAlbumRootId(url.albumRootId()); jobInfo.setAlbum( url.album() ); d->jobThread = DBJobsManager::instance()->startAlbumsJobThread(jobInfo); } else if (albums.first()->type() == Album::SEARCH) { d->extraValueJob = false; SearchesDBJobInfo jobInfo; if (d->listOnlyAvailableImages) jobInfo.setListAvailableImagesOnly(); jobInfo.setSearchIds(ids); d->jobThread = DBJobsManager::instance()->startSearchesJobThread(jobInfo); } connect(d->jobThread, SIGNAL(finished()), this, SLOT(slotResult())); connect(d->jobThread, SIGNAL(data(QList)), this, SLOT(slotData(QList))); } void ImageAlbumModel::slotResult() { if (d->jobThread != sender()) { return; } if (d->jobThread->hasErrors()) { qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to list url: " << d->jobThread->errorsList().first(); // Pop-up a message about the error. DNotificationWrapper(QString(), d->jobThread->errorsList().first(), DigikamApp::instance(), DigikamApp::instance()->windowTitle()); } d->jobThread->cancel(); d->jobThread = 0; // either of the two finishRefresh(); finishIncrementalRefresh(); } -void ImageAlbumModel::slotData(const QList &records) +void ImageAlbumModel::slotData(const QList& records) { if (d->jobThread != sender()) { return; } if (records.isEmpty()) { qCDebug(DIGIKAM_GENERAL_LOG) << "Data From DBJobsThread is null: " << records.isEmpty(); return; } ImageInfoList newItemsList; if (d->extraValueJob) { QList extraValues; foreach(const ImageListerRecord& record, records) { ImageInfo info(record); newItemsList << info; if (d->specialListing == QLatin1String("faces")) { FaceTagsIface face = FaceTagsIface::fromListing(info.id(), record.extraValues); extraValues << face.toVariant(); } else { // default handling: just pass extraValue if (record.extraValues.isEmpty()) { extraValues << QVariant(); } else if (record.extraValues.size() == 1) { extraValues << record.extraValues.first(); } else { extraValues << QVariant(record.extraValues); // uh-uh. List in List. } } } addImageInfos(newItemsList, extraValues); } else { foreach(const ImageListerRecord& record, records) { ImageInfo info(record); newItemsList << info; } addImageInfos(newItemsList); } } void ImageAlbumModel::slotImageChange(const ImageChangeset& changeset) { if (d->currentAlbums.isEmpty()) { return; } // already scheduled to refresh? if (hasScheduledRefresh()) { return; } // this is for the case that _only_ the status changes, i.e., explicit setVisible() if ((DatabaseFields::Images)changeset.changes() == DatabaseFields::Status) { scheduleIncrementalRefresh(); } // If we list a search, a change to a property may alter the search result QList::iterator it; /** * QList is designed for multiple selection, for now, only tags are supported * for SAlbum it will be a list with one element */ for (it = d->currentAlbums.begin() ; it != d->currentAlbums.end() ; ++it) { if ((*it)->type() == Album::SEARCH) { - SAlbum* const salbum = static_cast(*it); - + SAlbum* const salbum = static_cast(*it); bool needCheckRefresh = false; + if (salbum->isNormalSearch()) { // For searches any touched field can require a refresh. // We cannot easily find out which fields are searched for, so we refresh for any change. needCheckRefresh = true; } else if (salbum->isTimelineSearch()) { if (changeset.changes() & DatabaseFields::CreationDate) { needCheckRefresh = true; } } else if (salbum->isMapSearch()) { if (changeset.changes() & DatabaseFields::ImagePositionsAll) { needCheckRefresh = true; } } if (needCheckRefresh) { foreach(const qlonglong& id, changeset.ids()) { // if one matching image id is found, trigger a refresh if (hasImage(id)) { scheduleIncrementalRefresh(); break; } } } } } ImageModel::slotImageChange(changeset); } void ImageAlbumModel::slotImageTagChange(const ImageTagChangeset& changeset) { if (d->currentAlbums.isEmpty()) { return; } bool doRefresh = false; QList::iterator it; for (it = d->currentAlbums.begin() ; it != d->currentAlbums.end() ; ++it) { if ((*it)->type() == Album::TAG) { doRefresh = changeset.containsTag((*it)->id()); if (!doRefresh && d->recurseTags) { foreach(int tagId, changeset.tags()) { Album* const a = AlbumManager::instance()->findTAlbum(tagId); if (a && (*it)->isAncestorOf(a)) { doRefresh = true; break; } } } } if (doRefresh) { break; } } if (doRefresh) { scheduleIncrementalRefresh(); } ImageModel::slotImageTagChange(changeset); } void ImageAlbumModel::slotCollectionImageChange(const CollectionImageChangeset& changeset) { if (d->currentAlbums.isEmpty()) { return; } // already scheduled to refresh? if (d->refreshTimer->isActive()) { return; } bool doRefresh = false; QList::iterator it; - for(it = d->currentAlbums.begin(); it != d->currentAlbums.end(); ++it) + for (it = d->currentAlbums.begin() ; it != d->currentAlbums.end() ; ++it) { switch (changeset.operation()) { case CollectionImageChangeset::Added: { switch ((*it)->type()) { case Album::PHYSICAL: // that's easy: try if our album is affected doRefresh = changeset.containsAlbum((*it)->id()); if (!doRefresh && d->recurseAlbums) { foreach(int albumId, changeset.albums()) { Album* const a = AlbumManager::instance()->findPAlbum(albumId); if (a && (*it)->isAncestorOf(a)) { doRefresh = true; break; } } } break; default: // we cannot easily know if we are affected doRefresh = true; break; } break; } + case CollectionImageChangeset::Deleted: case CollectionImageChangeset::Removed: case CollectionImageChangeset::RemovedAll: // is one of our images affected? foreach(const qlonglong& id, changeset.ids()) { // if one matching image id is found, trigger a refresh if (hasImage(id)) { doRefresh = true; break; } } break; default: break; } - if(doRefresh) + if (doRefresh) break; } if (doRefresh) { scheduleIncrementalRefresh(); } } void ImageAlbumModel::slotSearchChange(const SearchChangeset& changeset) { if (d->currentAlbums.isEmpty()) { return; } if (changeset.operation() != SearchChangeset::Changed) { return; } SAlbum* const album = AlbumManager::instance()->findSAlbum(changeset.searchId()); QList::iterator it; for (it = d->currentAlbums.begin() ; it != d->currentAlbums.end() ; ++it) { if (album && (*it) == album) { scheduleIncrementalRefresh(); } } } void ImageAlbumModel::slotAlbumAdded(Album* /*album*/) { } void ImageAlbumModel::slotAlbumDeleted(Album* album) { if (d->currentAlbums.contains(album)) { d->currentAlbums.removeOne(album); clearImageInfos(); return; } // display changed tags if (album->type() == Album::TAG) { emitDataChangedForAll(); } } void ImageAlbumModel::slotAlbumRenamed(Album* album) { // display changed names if (d->currentAlbums.contains(album)) { emitDataChangedForAll(); } } void ImageAlbumModel::slotAlbumsCleared() { d->currentAlbums.clear(); clearImageInfos(); } } // namespace Digikam diff --git a/core/libs/models/imagealbummodel.h b/core/libs/models/imagealbummodel.h index 678f326d78..4aed30fff0 100644 --- a/core/libs/models/imagealbummodel.h +++ b/core/libs/models/imagealbummodel.h @@ -1,114 +1,114 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-03-08 * Description : Qt item model for database entries, listing done with ioslave * * Copyright (C) 2009-2011 by Marcel Wiesweg * Copyright (C) 2015 by Mohamed_Anwer * * 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, 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. * * ============================================================ */ #ifndef DIGIKAM_IMAGE_ALBUM_MODEL_H #define DIGIKAM_IMAGE_ALBUM_MODEL_H // Local includes #include "imagethumbnailmodel.h" #include "album.h" namespace Digikam { class ImageChangeset; class CollectionImageChangeset; class SearchChangeset; class Album; class ImageAlbumModel : public ImageThumbnailModel { Q_OBJECT public: explicit ImageAlbumModel(QObject* const parent = 0); ~ImageAlbumModel(); QList currentAlbums() const; bool hasScheduledRefresh() const; bool isRecursingAlbums() const; bool isRecursingTags() const; bool isListingOnlyAvailableImages() const; public Q_SLOTS: /** * Call this method to populate the model with data from the given album. * If called with 0, the model will be empty. * Opening the same album again is a no-op. */ - void openAlbum(QList album); + void openAlbum(const QList& albums); /** Reloads the current album */ void refresh(); void setRecurseAlbums(bool recursiveListing); void setRecurseTags(bool recursiveListing); void setListOnlyAvailableImages(bool onlyAvailable); void setSpecialTagListing(const QString& specialListing); //Q_SIGNALS: //void listedAlbumChanged(QList album); protected Q_SLOTS: void scheduleRefresh(); void scheduleIncrementalRefresh(); void slotResult(); - void slotData(const QList &records); + void slotData(const QList& records); void slotNextRefresh(); void slotNextIncrementalRefresh(); void slotCollectionImageChange(const CollectionImageChangeset& changeset); void slotSearchChange(const SearchChangeset& changeset); void slotAlbumAdded(Album* album); void slotAlbumDeleted(Album* album); void slotAlbumRenamed(Album* album); void slotAlbumsCleared(); void incrementalRefresh(); virtual void slotImageChange(const ImageChangeset& changeset); virtual void slotImageTagChange(const ImageTagChangeset& changeset); protected: - void startListJob(QList album); + void startListJob(const QList& albums); private: class Private; Private* const d; }; } // namespace Digikam #endif // DIGIKAM_IMAGE_ALBUM_MODEL_H diff --git a/core/libs/threadimageio/loadingcache.cpp b/core/libs/threadimageio/loadingcache.cpp index 6cc14cd1c8..3fa60a025b 100644 --- a/core/libs/threadimageio/loadingcache.cpp +++ b/core/libs/threadimageio/loadingcache.cpp @@ -1,519 +1,522 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2006-01-11 * Description : shared image loading and caching * * Copyright (C) 2005-2011 by Marcel Wiesweg * * 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, 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. * * ============================================================ */ #include "loadingcache.h" // Qt includes #include #include #include #include // Local includes #include "digikam_debug.h" #include "iccsettings.h" #include "kmemoryinfo.h" #include "dmetadata.h" #include "thumbnailsize.h" namespace Digikam { class LoadingCache::Private { public: explicit Private(LoadingCache* const q) : q(q) { // Note: Don't make the mutex recursive, we need to use a wait condition on it watch = 0; } void mapImageFilePath(const QString& filePath, const QString& cacheKey); void mapThumbnailFilePath(const QString& filePath, const QString& cacheKey); void cleanUpImageFilePathHash(); void cleanUpThumbnailFilePathHash(); LoadingCacheFileWatch* fileWatch() const; public: QCache imageCache; QCache thumbnailImageCache; QCache thumbnailPixmapCache; QMultiMap imageFilePathHash; QMultiMap thumbnailFilePathHash; QMap loadingDict; QMutex mutex; QWaitCondition condVar; LoadingCacheFileWatch* watch; LoadingCache* q; }; LoadingCacheFileWatch* LoadingCache::Private::fileWatch() const { // install default watch if no watch is set yet if (!watch) { q->setFileWatch(new ClassicLoadingCacheFileWatch); } return watch; } void LoadingCache::Private::mapImageFilePath(const QString& filePath, const QString& cacheKey) { if (imageFilePathHash.size() > 5*imageCache.size()) { cleanUpImageFilePathHash(); } imageFilePathHash.insert(filePath, cacheKey); } void LoadingCache::Private::mapThumbnailFilePath(const QString& filePath, const QString& cacheKey) { if (thumbnailFilePathHash.size() > 5*(thumbnailImageCache.size() + thumbnailPixmapCache.size())) { cleanUpThumbnailFilePathHash(); } thumbnailFilePathHash.insert(filePath, cacheKey); } void LoadingCache::Private::cleanUpImageFilePathHash() { // Remove all entries from hash whose value is no longer a key in the cache QSet keys = imageCache.keys().toSet(); QMultiMap::iterator it; for (it = imageFilePathHash.begin(); it != imageFilePathHash.end(); ) { if (!keys.contains(it.value())) { it = imageFilePathHash.erase(it); } else { ++it; } } } void LoadingCache::Private::cleanUpThumbnailFilePathHash() { QSet keys; keys += thumbnailImageCache.keys().toSet(); keys += thumbnailPixmapCache.keys().toSet(); QMultiMap::iterator it; for (it = thumbnailFilePathHash.begin(); it != thumbnailFilePathHash.end(); ) { if (!keys.contains(it.value())) { it = thumbnailFilePathHash.erase(it); } else { ++it; } } } LoadingCache* LoadingCache::m_instance = 0; LoadingCache* LoadingCache::cache() { if (!m_instance) { m_instance = new LoadingCache; } return m_instance; } void LoadingCache::cleanUp() { delete m_instance; } LoadingCache::LoadingCache() : d(new Private(this)) { KMemoryInfo memory = KMemoryInfo::currentInfo(); setCacheSize(qBound(60, int(memory.megabytes(KMemoryInfo::TotalRam)*0.05), 200)); setThumbnailCacheSize(5, 100); // the pixmap number should not be based on system memory, it's graphics memory // good place to call it here as LoadingCache is a singleton qRegisterMetaType("LoadingDescription"); qRegisterMetaType("DImg"); qRegisterMetaType("DMetadata"); connect(IccSettings::instance(), SIGNAL(settingsChanged(ICCSettingsContainer,ICCSettingsContainer)), this, SLOT(iccSettingsChanged(ICCSettingsContainer,ICCSettingsContainer))); } LoadingCache::~LoadingCache() { delete d->watch; delete d; m_instance = 0; } DImg* LoadingCache::retrieveImage(const QString& cacheKey) const { return d->imageCache[cacheKey]; } bool LoadingCache::putImage(const QString& cacheKey, DImg* img, const QString& filePath) const { bool successfulyInserted; int cost = img->numBytes(); successfulyInserted = d->imageCache.insert(cacheKey, img, cost); if (successfulyInserted && !filePath.isEmpty()) { d->mapImageFilePath(filePath, cacheKey); d->fileWatch()->addedImage(filePath); } return successfulyInserted; } void LoadingCache::removeImage(const QString& cacheKey) { d->imageCache.remove(cacheKey); } void LoadingCache::removeImages() { d->imageCache.clear(); } bool LoadingCache::isCacheable(const DImg* img) const { // return whether image fits in cache return (uint)d->imageCache.maxCost() >= img->numBytes(); } void LoadingCache::addLoadingProcess(LoadingProcess* process) { d->loadingDict[process->cacheKey()] = process; } LoadingProcess* LoadingCache::retrieveLoadingProcess(const QString& cacheKey) const { return d->loadingDict.value(cacheKey); } void LoadingCache::removeLoadingProcess(LoadingProcess* process) { d->loadingDict.remove(process->cacheKey()); } void LoadingCache::notifyNewLoadingProcess(LoadingProcess* process, const LoadingDescription& description) { for (QMap::const_iterator it = d->loadingDict.constBegin(); it != d->loadingDict.constEnd(); ++it) { it.value()->notifyNewLoadingProcess(process, description); } } void LoadingCache::setCacheSize(int megabytes) { qCDebug(DIGIKAM_GENERAL_LOG) << "Allowing a cache size of" << megabytes << "MB"; d->imageCache.setMaxCost(megabytes * 1024 * 1024); } // --- Thumbnails ---- const QImage* LoadingCache::retrieveThumbnail(const QString& cacheKey) const { return d->thumbnailImageCache[cacheKey]; } const QPixmap* LoadingCache::retrieveThumbnailPixmap(const QString& cacheKey) const { return d->thumbnailPixmapCache[cacheKey]; } bool LoadingCache::hasThumbnailPixmap(const QString& cacheKey) const { return d->thumbnailPixmapCache.contains(cacheKey); } void LoadingCache::putThumbnail(const QString& cacheKey, const QImage& thumb, const QString& filePath) { int cost = thumb.byteCount(); if (d->thumbnailImageCache.insert(cacheKey, new QImage(thumb), cost)) { d->mapThumbnailFilePath(filePath, cacheKey); d->fileWatch()->addedThumbnail(filePath); } } void LoadingCache::putThumbnail(const QString& cacheKey, const QPixmap& thumb, const QString& filePath) { int cost = thumb.width() * thumb.height() * thumb.depth() / 8; if (d->thumbnailPixmapCache.insert(cacheKey, new QPixmap(thumb), cost)) { d->mapThumbnailFilePath(filePath, cacheKey); d->fileWatch()->addedThumbnail(filePath); } } void LoadingCache::removeThumbnail(const QString& cacheKey) { d->thumbnailImageCache.remove(cacheKey); d->thumbnailPixmapCache.remove(cacheKey); } void LoadingCache::removeThumbnails() { d->thumbnailImageCache.clear(); d->thumbnailPixmapCache.clear(); } void LoadingCache::setThumbnailCacheSize(int numberOfQImages, int numberOfQPixmaps) { d->thumbnailImageCache.setMaxCost(numberOfQImages * ThumbnailSize::maxThumbsSize() * ThumbnailSize::maxThumbsSize() * 4); d->thumbnailPixmapCache.setMaxCost(numberOfQPixmaps * ThumbnailSize::maxThumbsSize() * ThumbnailSize::maxThumbsSize() * QPixmap::defaultDepth() / 8); } void LoadingCache::setFileWatch(LoadingCacheFileWatch* watch) { delete d->watch; d->watch = watch; d->watch->m_cache = this; } QStringList LoadingCache::imageFilePathsInCache() const { d->cleanUpImageFilePathHash(); return d->imageFilePathHash.uniqueKeys(); } QStringList LoadingCache::thumbnailFilePathsInCache() const { d->cleanUpThumbnailFilePathHash(); return d->thumbnailFilePathHash.uniqueKeys(); } -void LoadingCache::notifyFileChanged(const QString& filePath) +void LoadingCache::notifyFileChanged(const QString& filePath, bool notify) { QList keys = d->imageFilePathHash.values(filePath); foreach(const QString& cacheKey, keys) { - if (d->imageCache.remove(cacheKey)) + if (d->imageCache.remove(cacheKey) && notify) { emit fileChanged(filePath, cacheKey); } } keys = d->thumbnailFilePathHash.values(filePath); foreach(const QString& cacheKey, keys) { bool removedImage = d->thumbnailImageCache.remove(cacheKey); bool removedPixmap = d->thumbnailPixmapCache.remove(cacheKey); - if (removedImage || removedPixmap) + if ((removedImage || removedPixmap) && notify) { emit fileChanged(filePath, cacheKey); } } - emit fileChanged(filePath); + if (notify) + { + emit fileChanged(filePath); + } } void LoadingCache::iccSettingsChanged(const ICCSettingsContainer& current, const ICCSettingsContainer& previous) { if (current.enableCM != previous.enableCM || current.useManagedPreviews != previous.useManagedPreviews || current.monitorProfile != previous.monitorProfile) { LoadingCache::CacheLock lock(this); removeImages(); removeThumbnails(); } } //--------------------------------------------------------------------------------------------------- LoadingCacheFileWatch::~LoadingCacheFileWatch() { if (m_cache) { LoadingCache::CacheLock lock(m_cache); if (m_cache->d->watch == this) { m_cache->d->watch = 0; } } } void LoadingCacheFileWatch::notifyFileChanged(const QString& filePath) { if (m_cache) { LoadingCache::CacheLock lock(m_cache); m_cache->notifyFileChanged(filePath); } } void LoadingCacheFileWatch::addedImage(const QString&) { // default: do nothing } void LoadingCacheFileWatch::addedThumbnail(const QString&) { // default: do nothing } //--------------------------------------------------------------------------------------------------- ClassicLoadingCacheFileWatch::ClassicLoadingCacheFileWatch() { if (thread() != QCoreApplication::instance()->thread()) { moveToThread(QCoreApplication::instance()->thread()); } m_watch = new QFileSystemWatcher; connect(m_watch, SIGNAL(fileChanged(QString)), this, SLOT(slotFileDirty(QString))); // Make sure the signal gets here directly from the event loop. // If putImage is called from the main thread, with CacheLock, // a deadlock would result (mutex is not recursive) connect(this, SIGNAL(signalUpdateDirWatch()), this, SLOT(slotUpdateDirWatch()), Qt::QueuedConnection); } ClassicLoadingCacheFileWatch::~ClassicLoadingCacheFileWatch() { delete m_watch; } void ClassicLoadingCacheFileWatch::addedImage(const QString& filePath) { Q_UNUSED(filePath) // schedule update of file watch // QFileSystemWatch can only be accessed from main thread! emit signalUpdateDirWatch(); } void ClassicLoadingCacheFileWatch::addedThumbnail(const QString& filePath) { Q_UNUSED(filePath); // ignore, we do not watch thumbnails } void ClassicLoadingCacheFileWatch::slotFileDirty(const QString& path) { // Signal comes from main thread qCDebug(DIGIKAM_GENERAL_LOG) << "LoadingCache slotFileDirty " << path; // This method acquires a lock itself notifyFileChanged(path); // No need for locking here, we are in main thread m_watch->removePath(path); m_watchedFiles.remove(path); } void ClassicLoadingCacheFileWatch::slotUpdateDirWatch() { // Event comes from main thread, we need to lock ourselves. LoadingCache::CacheLock lock(m_cache); // get a list of files in cache that need watch QSet toBeAdded; QSet toBeRemoved = m_watchedFiles; QList filePaths = m_cache->imageFilePathsInCache(); foreach(const QString& m_watchPath, filePaths) { if (!m_watchPath.isEmpty()) { if (!m_watchedFiles.contains(m_watchPath)) { toBeAdded.insert(m_watchPath); } toBeRemoved.remove(m_watchPath); } } foreach(const QString& watchedItem, toBeRemoved) { //qCDebug(DIGIKAM_GENERAL_LOG) << "removing watch for " << *it; m_watch->removePath(watchedItem); m_watchedFiles.remove(watchedItem); } foreach(const QString& watchedItem, toBeAdded) { //qCDebug(DIGIKAM_GENERAL_LOG) << "adding watch for " << *it; m_watch->addPath(watchedItem); m_watchedFiles.insert(watchedItem); } } //--------------------------------------------------------------------------------------------------- LoadingCache::CacheLock::CacheLock(LoadingCache* cache) : m_cache(cache) { m_cache->d->mutex.lock(); } LoadingCache::CacheLock::~CacheLock() { m_cache->d->mutex.unlock(); } void LoadingCache::CacheLock::wakeAll() { // obviously the mutex is locked when this function is called m_cache->d->condVar.wakeAll(); } void LoadingCache::CacheLock::timedWait() { // same as above, the mutex is certainly locked m_cache->d->condVar.wait(&m_cache->d->mutex, 1000); } } // namespace Digikam diff --git a/core/libs/threadimageio/loadingcache.h b/core/libs/threadimageio/loadingcache.h index 76ccaadbb6..ce3e4fe7fb 100644 --- a/core/libs/threadimageio/loadingcache.h +++ b/core/libs/threadimageio/loadingcache.h @@ -1,313 +1,313 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2006-01-11 * Description : shared image loading and caching * * Copyright (C) 2005-2011 by Marcel Wiesweg * * 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, 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. * * ============================================================ */ #ifndef DIGIKAM_LOADING_CACHE_H #define DIGIKAM_LOADING_CACHE_H // Qt includes #include #include #include // Local includes #include "dimg.h" #include "loadsavethread.h" #include "digikam_export.h" namespace Digikam { class ICCSettingsContainer; class LoadingProcessListener { public: virtual ~LoadingProcessListener() {}; virtual bool querySendNotifyEvent() = 0; virtual void setResult(const LoadingDescription& loadingDescription, const DImg& img) = 0; virtual LoadSaveNotifier* loadSaveNotifier() = 0; virtual LoadSaveThread::AccessMode accessMode() = 0; }; // -------------------------------------------------------------------------------------------------------------- class LoadingProcess { public: virtual ~LoadingProcess() {}; virtual bool completed() = 0; virtual QString filePath() = 0; virtual QString cacheKey() = 0; virtual void addListener(LoadingProcessListener* listener) = 0; virtual void removeListener(LoadingProcessListener* listener) = 0; virtual void notifyNewLoadingProcess(LoadingProcess* process, const LoadingDescription& description) = 0; }; // -------------------------------------------------------------------------------------------------------------- class DIGIKAM_EXPORT LoadingCacheFileWatch { public: virtual ~LoadingCacheFileWatch(); /// Called by the thread when a new entry is added to the cache virtual void addedImage(const QString& filePath); virtual void addedThumbnail(const QString& filePath); protected: /** * Convenience method. * Call this to tell the cache to remove stored images for filePath from the cache. * Calling this method is fast, you do not need to check if the file is contained in the cache. * Do not hold the CacheLock when calling this method. */ void notifyFileChanged(const QString& filePath); protected: friend class LoadingCache; class LoadingCache* m_cache; }; // -------------------------------------------------------------------------------------------------------------- class DIGIKAM_EXPORT ClassicLoadingCacheFileWatch : public QObject, public LoadingCacheFileWatch { Q_OBJECT /** Reference implementation */ public: ClassicLoadingCacheFileWatch(); ~ClassicLoadingCacheFileWatch(); virtual void addedImage(const QString& filePath); virtual void addedThumbnail(const QString& filePath); private Q_SLOTS: void slotFileDirty(const QString& path); void slotUpdateDirWatch(); Q_SIGNALS: void signalUpdateDirWatch(); protected: QFileSystemWatcher* m_watch; QSet m_watchedFiles; }; // -------------------------------------------------------------------------------------------------------------- class DIGIKAM_EXPORT LoadingCache : public QObject { Q_OBJECT public: static LoadingCache* cache(); static void cleanUp(); virtual ~LoadingCache(); /// !! All methods of LoadingCache shall only be called when a CacheLock is held !! class DIGIKAM_EXPORT CacheLock { public: explicit CacheLock(LoadingCache* cache); ~CacheLock(); void wakeAll(); void timedWait(); private: LoadingCache* m_cache; }; /** * Retrieves an image for the given string from the cache, * or 0 if no image is found. */ DImg* retrieveImage(const QString& cacheKey) const; /// Returns whether the given DImg fits in the cache. bool isCacheable(const DImg* img) const; /** Put image into for given string into the cache. * Returns true if image has been put in the cache, false otherwise. * Ownership of the DImg instance is passed to the cache. * When it cannot be put in the cache it is deleted. * The third parameter specifies a file path that will be watched. * If this file changes, the object will be removed from the cache. */ bool putImage(const QString& cacheKey, DImg* img, const QString& filePath) const; /** * Remove entries for the given cacheKey from the cache */ void removeImage(const QString& cacheKey); /** * Remove all entries from the cache */ void removeImages(); // ------- Loading process management ----------------------------------- /** * Find the loading process for given cacheKey, or 0 if not found */ LoadingProcess* retrieveLoadingProcess(const QString& cacheKey) const; /** * Add a loading process to the list. Only one loading process * for the same cache key is registered at a time. */ void addLoadingProcess(LoadingProcess* process); /** * Remove loading process for given cache key */ void removeLoadingProcess(LoadingProcess* process); /** * Notify all currently registered loading processes */ void notifyNewLoadingProcess(LoadingProcess* process, const LoadingDescription& description); /** * Sets the cache size in megabytes. * The thumbnail cache is not affected and setThumbnailCacheSize takes the maximum number. */ void setCacheSize(int megabytes); // ------- Thumbnail cache ----------------------------------- /// The LoadingCache support both the caching of QImage and QPixmap objects. /// QPixmaps can only be accessed from the main thread, so the tasks cannot access this cache. /** * Retrieves a thumbnail for the given filePath from the thumbnail cache, * or a 0 if the thumbnail is not found. */ const QImage* retrieveThumbnail(const QString& cacheKey) const; const QPixmap* retrieveThumbnailPixmap(const QString& cacheKey) const; bool hasThumbnailPixmap(const QString& cacheKey) const; /** * Puts a thumbnail into the thumbnail cache. */ void putThumbnail(const QString& cacheKey, const QImage& thumb, const QString& filePath); void putThumbnail(const QString& cacheKey, const QPixmap& thumb, const QString& filePath); /** * Remove the thumbnail for the given file path from the thumbnail cache */ void removeThumbnail(const QString& cacheKey); /** * Remove all thumbnails */ void removeThumbnails(); /** * Sets the size of the thumbnail cache * @param numberOfQImages The maximum number of thumbnails of max possible size in QImage format that will be cached. If the size of the images is smaller, a larger number will be cached. * @param numberOfQPixmaps The maximum number of thumbnails of max possible size in QPixmap format that will be cached. If the size of the images is smaller, a larger number will be cached. * Note: The main cache is unaffected by this method, * and setCacheSize takes megabytes as parameter. * Note: A good caching strategy will be to set one of the numbers to 0 * Default values: (0, 100) */ void setThumbnailCacheSize(int numberOfQImages, int numberOfQPixmaps); // ------- File Watch Management ----------------------------------- /** * Sets a LoadingCacheFileWatch to watch the files contained in this cache. * Ownership of this object is transferred to the cache. */ void setFileWatch(LoadingCacheFileWatch* watch); /** * Returns a list of all possible file paths in cache. */ QStringList imageFilePathsInCache() const; QStringList thumbnailFilePathsInCache() const; /** * Remove all entries from cache that were loaded from filePath. - * Emits relevant signals. + * Emits relevant signals if notify = true. */ - void notifyFileChanged(const QString& filePath); + void notifyFileChanged(const QString& filePath, bool notify = true); Q_SIGNALS: /** * This signal is emitted when the cache is notified that a file was changed. * There is no information in this signal if the file was ever contained in the cache. * The signal may be emitted under CacheLock. Strongly consider a queued connection. */ void fileChanged(const QString& filePath); /** * This signal is emitted when the cache is notified that a file was changed, * and the given cache key was removed from the cache. * The signal may be emitted under CacheLock. Strongly consider a queued connection. */ void fileChanged(const QString& filePath, const QString& cacheKey); private Q_SLOTS: void iccSettingsChanged(const ICCSettingsContainer& current, const ICCSettingsContainer& previous); private: LoadingCache(); friend class LoadingCacheFileWatch; friend class CacheLock; private: static LoadingCache* m_instance; class Private; Private* const d; }; } // namespace Digikam #endif // DIGIKAM_LOADING_CACHE_H diff --git a/core/libs/threadimageio/loadingcacheinterface.cpp b/core/libs/threadimageio/loadingcacheinterface.cpp index 378d7f1646..94d36a30e1 100644 --- a/core/libs/threadimageio/loadingcacheinterface.cpp +++ b/core/libs/threadimageio/loadingcacheinterface.cpp @@ -1,101 +1,94 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2006-02-06 * Description : shared image loading and caching * * Copyright (C) 2005-2011 by Marcel Wiesweg * * 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, 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. * * ============================================================ */ #include "loadingcacheinterface.h" // Local includes #include "loadingcache.h" namespace Digikam { void LoadingCacheInterface::initialize() { LoadingCache::cache(); } void LoadingCacheInterface::cleanUp() { LoadingCache::cleanUp(); } -void LoadingCacheInterface::fileChanged(const QString& filePath) +void LoadingCacheInterface::fileChanged(const QString& filePath, bool notify) { LoadingCache* cache = LoadingCache::cache(); LoadingCache::CacheLock lock(cache); - cache->notifyFileChanged(filePath); -/* NOTE: old implementation - QStringList possibleCacheKeys = LoadingDescription::possibleCacheKeys(filePath); - for (QStringList::iterator it = possibleCacheKeys.begin(); it != possibleCacheKeys.end(); ++it) - { - cache->removeImage(*it); - } -*/ + cache->notifyFileChanged(filePath, notify); } void LoadingCacheInterface::connectToSignalFileChanged(QObject* object, const char* slot) { LoadingCache* cache = LoadingCache::cache(); QObject::connect(cache, SIGNAL(fileChanged(QString)), object, slot, Qt::QueuedConnection); // make it a queued connection because the signal is emitted when the CacheLock is held! } void LoadingCacheInterface::cleanCache() { LoadingCache* cache = LoadingCache::cache(); LoadingCache::CacheLock lock(cache); cache->removeImages(); } void LoadingCacheInterface::cleanThumbnailCache() { LoadingCache* cache = LoadingCache::cache(); LoadingCache::CacheLock lock(cache); cache->removeThumbnails(); } void LoadingCacheInterface::putImage(const QString& filePath, const DImg& img) { LoadingCache* cache = LoadingCache::cache(); LoadingCache::CacheLock lock(cache); if (cache->isCacheable(&img)) { DImg* copy = new DImg(img); copy->detach(); cache->putImage(filePath, copy, filePath); } } void LoadingCacheInterface::setCacheOptions(int cacheSize) { LoadingCache* cache = LoadingCache::cache(); LoadingCache::CacheLock lock(cache); cache->setCacheSize(cacheSize); } } // namespace Digikam diff --git a/core/libs/threadimageio/loadingcacheinterface.h b/core/libs/threadimageio/loadingcacheinterface.h index c464d5a4ac..a6b7d0d0dd 100644 --- a/core/libs/threadimageio/loadingcacheinterface.h +++ b/core/libs/threadimageio/loadingcacheinterface.h @@ -1,87 +1,87 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2006-02-06 * Description : shared image loading and caching * * Copyright (C) 2005-2011 by Marcel Wiesweg * * 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, 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. * * ============================================================ */ #ifndef DIGIKAM_LOADING_CACHE_INTERFACE_H #define DIGIKAM_LOADING_CACHE_INTERFACE_H // Qt includes #include // Local includes #include "digikam_export.h" #include "dimg.h" namespace Digikam { class DIGIKAM_EXPORT LoadingCacheInterface { public: static void initialize(); /** clean up cache at shutdown */ static void cleanUp(); /** * Remove an image from the cache * because it may have changed on disk */ - static void fileChanged(const QString& filePath); + static void fileChanged(const QString& filePath, bool notify = true); /** * Connect the given object/slot to the signal * void fileChanged(const QString& filePath); * which is emitted when the cache gains knowledge about a possible * change of this file on disk. */ static void connectToSignalFileChanged(QObject* object, const char* slot); /** * remove all images from the cache * (e.g. when loading settings changed) * Does not affect thumbnails. */ static void cleanCache(); /** * Remove all thumbnails from the thumbnail cache. * Does not affect main image cache. */ static void cleanThumbnailCache(); /** add a copy of the image to cache */ static void putImage(const QString& filePath, const DImg& img); /** * Set cache size in Megabytes. * Set to 0 to disable caching. */ static void setCacheOptions(int cacheSize); }; } // namespace Digikam #endif // DIGIKAM_LOADING_CACHE_INTERFACE_H diff --git a/core/libs/threadimageio/thumbnailtask.cpp b/core/libs/threadimageio/thumbnailtask.cpp index f2cabc059f..d7ec430a05 100644 --- a/core/libs/threadimageio/thumbnailtask.cpp +++ b/core/libs/threadimageio/thumbnailtask.cpp @@ -1,272 +1,277 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2007-06-05 * Description : Multithreaded loader for thumbnails * * Copyright (C) 2006-2008 by Marcel Wiesweg * Copyright (C) 2006-2018 by Gilles Caulier * * 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, 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. * * ============================================================ */ #include "thumbnailtask.h" // C++ includes #include // Qt includes #include #include #include #include // Local includes #include "drawdecoder.h" #include "digikam_debug.h" #include "dmetadata.h" #include "iccmanager.h" #include "jpegutils.h" #include "metadatasettings.h" #include "thumbnailloadthread.h" #include "thumbnailcreator.h" namespace Digikam { ThumbnailLoadingTask::ThumbnailLoadingTask(LoadSaveThread* const thread, const LoadingDescription& description) : SharedLoadingTask(thread, description, LoadSaveThread::AccessModeRead, LoadingTaskStatusLoading) { // Thread must be a ThumbnailLoadThread, crashes otherwise. // Not a clean but pragmatic solution. ThumbnailLoadThread* const thumbThread = static_cast(thread); m_creator = thumbThread->thumbnailCreator(); } void ThumbnailLoadingTask::execute() { if (m_loadingTaskStatus == LoadingTaskStatusStopping) { return; } if (m_loadingDescription.previewParameters.onlyPregenerate()) { setupCreator(); switch (m_loadingDescription.previewParameters.type) { case LoadingDescription::PreviewParameters::Thumbnail: m_creator->pregenerate(m_loadingDescription.thumbnailIdentifier()); break; case LoadingDescription::PreviewParameters::DetailThumbnail: m_creator->pregenerateDetail(m_loadingDescription.thumbnailIdentifier(), m_loadingDescription.previewParameters.extraParameter.toRect()); break; default: break; } m_thread->taskHasFinished(); // do not emit any signal return; } LoadingCache* const cache = LoadingCache::cache(); { LoadingCache::CacheLock lock(cache); // find possible cached images const QImage* const cachedImage = cache->retrieveThumbnail(m_loadingDescription.cacheKey()); if (cachedImage) { m_qimage = QImage(*cachedImage); } if (m_qimage.isNull()) { // find possible running loading process m_usedProcess = cache->retrieveLoadingProcess(m_loadingDescription.cacheKey()); // do not wait on other loading processes? //m_usedProcess = cache->retrieveLoadingProcess(m_loadingDescription.cacheKey()); if (m_usedProcess) { // Other process is right now loading this image. // Add this task to the list of listeners and // attach this thread to the other thread, wait until loading // has finished. m_usedProcess->addListener(this); // break loop when either the loading has completed, or this task is being stopped - while ( !m_usedProcess->completed() && m_loadingTaskStatus != LoadingTaskStatusStopping ) + while (!m_usedProcess->completed() && m_loadingTaskStatus != LoadingTaskStatusStopping) { lock.timedWait(); } // remove listener from process if (m_usedProcess) { m_usedProcess->removeListener(this); } // wake up the process which is waiting until all listeners have removed themselves lock.wakeAll(); // set to 0, as checked in setStatus m_usedProcess = 0; } else { // Neither in cache, nor currently loading in different thread. // Load it here and now, add this LoadingProcess to cache list. cache->addLoadingProcess(this); // Add this to the list of listeners addListener(this); // for use in setStatus m_usedProcess = this; // Notify other processes that we are now loading this image. // They might be interested - see notifyNewLoadingProcess below cache->notifyNewLoadingProcess(this, m_loadingDescription); } } } if (!m_qimage.isNull()) { // following the golden rule to avoid deadlocks, do this when CacheLock is not held postProcess(); m_thread->taskHasFinished(); m_thread->thumbnailLoaded(m_loadingDescription, m_qimage); return; } // Load or create thumbnail setupCreator(); switch (m_loadingDescription.previewParameters.type) { case LoadingDescription::PreviewParameters::Thumbnail: m_qimage = m_creator->load(m_loadingDescription.thumbnailIdentifier()); break; case LoadingDescription::PreviewParameters::DetailThumbnail: m_qimage = m_creator->loadDetail(m_loadingDescription.thumbnailIdentifier(), m_loadingDescription.previewParameters.extraParameter.toRect()); break; default: break; } + if (m_loadingTaskStatus == LoadingTaskStatusStopping) + { + return; + } + { LoadingCache::CacheLock lock(cache); // put (valid) image into cache of loaded images if (!m_qimage.isNull()) { cache->putThumbnail(m_loadingDescription.cacheKey(), m_qimage, m_loadingDescription.filePath); } // remove this from the list of loading processes in cache cache->removeLoadingProcess(this); } { LoadingCache::CacheLock lock(cache); // indicate that loading has finished so that listeners can stop waiting m_completed = true; // dispatch image to all listeners, including this - for (int i=0; i(m_listeners.at(i)); if (task) { task->setResult(m_loadingDescription, m_qimage); } } // remove myself from list of listeners removeListener(this); // wake all listeners waiting on cache condVar, so that they remove themselves lock.wakeAll(); // wait until all listeners have removed themselves while (m_listeners.count() != 0) { lock.timedWait(); } // set to 0, as checked in setStatus m_usedProcess = 0; } // again: following the golden rule to avoid deadlocks, do this when CacheLock is not held postProcess(); m_thread->taskHasFinished(); m_thread->thumbnailLoaded(m_loadingDescription, m_qimage); } void ThumbnailLoadingTask::setupCreator() { m_creator->setThumbnailSize(m_loadingDescription.previewParameters.size); m_creator->setExifRotate(MetadataSettings::instance()->settings().exifRotate); m_creator->setLoadingProperties(this, m_loadingDescription.rawDecodingSettings); } void ThumbnailLoadingTask::setResult(const LoadingDescription& loadingDescription, const QImage& qimage) { // this is called from another process's execute while this task is waiting on m_usedProcess. // Note that loadingDescription need not equal m_loadingDescription (may be superior) m_resultLoadingDescription = loadingDescription; // these are taken from our own description m_resultLoadingDescription.postProcessingParameters = m_loadingDescription.postProcessingParameters; m_qimage = qimage; } void ThumbnailLoadingTask::postProcess() { m_loadingDescription.postProcessingParameters.profile().description(); switch (m_loadingDescription.postProcessingParameters.colorManagement) { case LoadingDescription::NoColorConversion: { break; } case LoadingDescription::ConvertToSRGB: { // Thumbnails are stored in sRGB break; } case LoadingDescription::ConvertForDisplay: { IccManager::transformForDisplay(m_qimage, m_loadingDescription.postProcessingParameters.profile()); break; } default: { qCDebug(DIGIKAM_GENERAL_LOG) << "Unsupported postprocessing parameter for thumbnail loading:" - << m_loadingDescription.postProcessingParameters.colorManagement; + << m_loadingDescription.postProcessingParameters.colorManagement; break; } } } } // namespace Digikam diff --git a/core/libs/widgets/itemview/ditemtooltip.cpp b/core/libs/widgets/itemview/ditemtooltip.cpp index b624149af8..a6824b94af 100644 --- a/core/libs/widgets/itemview/ditemtooltip.cpp +++ b/core/libs/widgets/itemview/ditemtooltip.cpp @@ -1,413 +1,413 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2008-12-10 * Description : tool tip widget for iconview, thumbbar, and folderview items * * Copyright (C) 2008-2018 by Gilles Caulier * * 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, 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. * * ============================================================ */ #include "ditemtooltip.h" // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE includes #include namespace Digikam { DToolTipStyleSheet::DToolTipStyleSheet(const QFont& font) : maxStringLength(30) { unavailable = i18n("unavailable"); QString fontSize = (font.pointSize() == -1) ? QString::fromUtf8("font-size: %1px;").arg(font.pixelSize()) : QString::fromUtf8("font-size: %1pt;").arg(font.pointSize()); tipHeader = QLatin1String(""); tipFooter = QLatin1String("
"); headBeg = QString::fromLatin1("" "

") .arg(qApp->palette().color(QPalette::Base).name()) .arg(qApp->palette().color(QPalette::Text).name()) .arg(font.family()) .arg(fontSize); headEnd = QLatin1String("

"); cellBeg = QString::fromLatin1("

") .arg(qApp->palette().color(QPalette::ToolTipText).name()) .arg(font.family()) .arg(fontSize); cellMid = QString::fromLatin1("

") .arg(qApp->palette().color(QPalette::ToolTipText).name()) .arg(font.family()) .arg(fontSize); cellEnd = QLatin1String("

"); cellSpecBeg = QString::fromLatin1("

") .arg(qApp->palette().color(QPalette::ToolTipText).name()) .arg(font.family()) .arg(fontSize); cellSpecMid = QString::fromLatin1("

") .arg(qApp->palette().color(QPalette::ToolTipText).name()) .arg(font.family()) .arg(fontSize); cellSpecEnd = QLatin1String("

"); } QString DToolTipStyleSheet::breakString(const QString& input) const { QString str = input.simplified(); str = str.toHtmlEscaped(); if (str.length() <= maxStringLength) { return str; } QString br; int i = 0; int count = 0; while (i < str.length()) { if (count >= maxStringLength && str.at(i).isSpace()) { count = 0; br.append(QLatin1String("
")); } else { br.append(str.at(i)); } ++i; ++count; } return br; } QString DToolTipStyleSheet::elidedText(const QString& str, Qt::TextElideMode elideMode) const { if (str.length() <= maxStringLength) { return str; } switch (elideMode) { case Qt::ElideLeft: return QLatin1String("...") + str.right(maxStringLength-3); case Qt::ElideRight: return str.left(maxStringLength-3) + QLatin1String("..."); case Qt::ElideMiddle: return str.left(maxStringLength / 2 - 2) + QLatin1String("...") + str.right(maxStringLength / 2 - 1); case Qt::ElideNone: return str.left(maxStringLength); default: return str; } } QString DToolTipStyleSheet::imageAsBase64(const QImage& img) const { QByteArray byteArray; QBuffer buffer(&byteArray); img.save(&buffer, "PNG"); QString iconBase64 = QString::fromLatin1(byteArray.toBase64().data()); return QString::fromLatin1("").arg(iconBase64); } // -------------------------------------------------------------------------------------------------- class DItemToolTip::Private { public: explicit Private() : tipBorder(5) { corner = 0; } const int tipBorder; int corner; QPixmap corners[4]; }; DItemToolTip::DItemToolTip(QWidget* const parent) : QLabel(parent, Qt::ToolTip), d(new Private) { hide(); + setForegroundRole(QPalette::ToolTipText); setBackgroundRole(QPalette::ToolTipBase); - setPalette(QToolTip::palette()); ensurePolished(); const int fwidth = qMax(d->tipBorder, 1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, 0, this)); setContentsMargins(fwidth, fwidth, fwidth, fwidth); setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, 0, this) / 255.0); setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); setFrameStyle(QFrame::StyledPanel); /* Old-style box: setFrameStyle(QFrame::Plain | QFrame::Box); setLineWidth(1); */ renderArrows(); } DItemToolTip::~DItemToolTip() { delete d; } void DItemToolTip::updateToolTip() { renderArrows(); QString contents = tipContents(); //setWordWrap(Qt::mightBeRichText(contents)); setText(contents); resize(sizeHint()); } bool DItemToolTip::toolTipIsEmpty() const { return(text().isEmpty()); } void DItemToolTip::reposition() { QRect rect = repositionRect(); if (rect.isNull()) { return; } QPoint pos = rect.center(); // d->corner: // 0: upperleft // 1: upperright // 2: lowerleft // 3: lowerright d->corner = 0; // should the tooltip be shown to the left or to the right of the ivi ? QRect desk = QApplication::desktop()->screenGeometry(rect.center()); if (rect.center().x() + width() > desk.right()) { // to the left if (pos.x() - width() < 0) { pos.setX(0); d->corner = 4; } else { pos.setX( pos.x() - width() ); d->corner = 1; } } // should the tooltip be shown above or below the ivi ? if (rect.bottom() + height() > desk.bottom()) { // above pos.setY( rect.top() - height() - 5); d->corner += 2; } else { pos.setY( rect.bottom() + 5 ); } move( pos ); } void DItemToolTip::renderArrows() { int w = d->tipBorder; // -- left top arrow ------------------------------------- QPixmap& pix0 = d->corners[0]; pix0 = QPixmap(w, w); pix0.fill(Qt::transparent); QPainter p0(&pix0); p0.setPen(QPen(qApp->palette().color(QPalette::Text), 1)); for (int j=0; jcorners[1]; pix1 = QPixmap(w, w); pix1.fill(Qt::transparent); QPainter p1(&pix1); p1.setPen(QPen(qApp->palette().color(QPalette::Text), 1)); for (int j=0; jcorners[2]; pix2 = QPixmap(w, w); pix2.fill(Qt::transparent); QPainter p2(&pix2); p2.setPen(QPen(qApp->palette().color(QPalette::Text), 1)); for (int j=0; jcorners[3]; pix3 = QPixmap(w, w); pix3.fill(Qt::transparent); QPainter p3(&pix3); p3.setPen(QPen(qApp->palette().color(QPalette::Text), 1)); for (int j=0; jtype() ) { case QEvent::Leave: case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: case QEvent::FocusIn: case QEvent::FocusOut: case QEvent::Wheel: hide(); break; default: break; } return QFrame::event(e); } void DItemToolTip::resizeEvent(QResizeEvent* e) { QStyleHintReturnMask frameMask; QStyleOption option; option.init(this); if (style()->styleHint(QStyle::SH_ToolTip_Mask, &option, this, &frameMask)) { setMask(frameMask.region); } update(); QLabel::resizeEvent(e); } void DItemToolTip::paintEvent(QPaintEvent* e) { { QStylePainter p(this); QStyleOptionFrame opt; opt.init(this); p.drawPrimitive(QStyle::PE_PanelTipLabel, opt); } QLabel::paintEvent(e); QPainter p(this); if (d->corner >= 4) { return; } QPixmap& pix = d->corners[d->corner]; switch (d->corner) { case 0: p.drawPixmap( 3, 3, pix ); break; case 1: p.drawPixmap( width() - pix.width() - 3, 3, pix ); break; case 2: p.drawPixmap( 3, height() - pix.height() - 3, pix ); break; case 3: p.drawPixmap( width() - pix.width() - 3, height() - pix.height() - 3, pix ); break; } } } // namespace Digikam diff --git a/core/libs/widgets/itemview/itemviewtooltip.cpp b/core/libs/widgets/itemview/itemviewtooltip.cpp index e33d04729b..90a3facbc1 100644 --- a/core/libs/widgets/itemview/itemviewtooltip.cpp +++ b/core/libs/widgets/itemview/itemviewtooltip.cpp @@ -1,210 +1,210 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-04-24 * Description : A DItemToolTip prepared for use in QAbstractItemViews * * Copyright (C) 2009-2011 by Marcel Wiesweg * * 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, 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. * * ============================================================ */ #include "itemviewtooltip.h" // Qt includes #include #include // Local includes #include "digikam_debug.h" namespace Digikam { class ItemViewToolTip::Private { public: explicit Private() { view = 0; filterInstalled = false; } QAbstractItemView* view; QModelIndex index; QRect rect; QString text; bool filterInstalled; }; -ItemViewToolTip::ItemViewToolTip(QAbstractItemView* view) - : DItemToolTip(view), d(new Private) +ItemViewToolTip::ItemViewToolTip(QAbstractItemView* const view) + : DItemToolTip(view), + d(new Private) { d->view = view; + setForegroundRole(QPalette::ToolTipText); setBackgroundRole(QPalette::ToolTipBase); - setPalette(QToolTip::palette()); setMouseTracking(true); } ItemViewToolTip::~ItemViewToolTip() { delete d; } QAbstractItemView* ItemViewToolTip::view() const { return d->view; } QAbstractItemModel* ItemViewToolTip::model() const { return d->view ? d->view->model() : 0; } QModelIndex ItemViewToolTip::currentIndex() const { return d->index; } void ItemViewToolTip::show(const QStyleOptionViewItem& option, const QModelIndex& index) { d->index = index; d->rect = option.rect; d->rect.moveTopLeft(d->view->viewport()->mapToGlobal(d->rect.topLeft())); updateToolTip(); reposition(); if (isHidden() && !toolTipIsEmpty()) { if (!d->filterInstalled) { qApp->installEventFilter(this); d->filterInstalled = true; } DItemToolTip::show(); } } void ItemViewToolTip::setTipContents(const QString& tipContents) { d->text = tipContents; updateToolTip(); } QString ItemViewToolTip::tipContents() { return d->text; } QRect ItemViewToolTip::repositionRect() { return d->rect; } void ItemViewToolTip::hideEvent(QHideEvent*) { d->rect = QRect(); d->index = QModelIndex(); if (d->filterInstalled) { d->filterInstalled = false; qApp->removeEventFilter(this); } } // The following code is inspired by qtooltip.cpp, // Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies). bool ItemViewToolTip::eventFilter(QObject* o, QEvent* e) { switch (e->type()) { #ifdef Q_OS_OSX case QEvent::KeyPress: case QEvent::KeyRelease: { int key = static_cast(e)->key(); Qt::KeyboardModifiers mody = static_cast(e)->modifiers(); if (!(mody & Qt::KeyboardModifierMask) && key != Qt::Key_Shift && key != Qt::Key_Control && key != Qt::Key_Alt && key != Qt::Key_Meta) { hide(); } break; } #endif // Q_OS_OSX case QEvent::Leave: hide(); // could add a 300ms timer here, like Qt break; case QEvent::WindowActivate: case QEvent::WindowDeactivate: case QEvent::MouseButtonPress: - case QEvent::MouseButtonRelease: case QEvent::MouseButtonDblClick: case QEvent::FocusIn: case QEvent::FocusOut: case QEvent::Wheel: hide(); break; case QEvent::MouseMove: { // needs mouse tracking, obviously if (o == d->view->viewport() && !d->rect.isNull() && !d->rect.contains(static_cast(e)->globalPos())) { hide(); } break; } default: break; } return false; } void ItemViewToolTip::mouseMoveEvent(QMouseEvent* e) { if (d->rect.isNull()) { return; } QPoint pos = e->globalPos(); pos = d->view->viewport()->mapFromGlobal(pos); if (!d->rect.contains(pos)) { hide(); } DItemToolTip::mouseMoveEvent(e); } } // namespace Digikam diff --git a/core/libs/widgets/itemview/itemviewtooltip.h b/core/libs/widgets/itemview/itemviewtooltip.h index f917e6f340..ed140b75ff 100644 --- a/core/libs/widgets/itemview/itemviewtooltip.h +++ b/core/libs/widgets/itemview/itemviewtooltip.h @@ -1,81 +1,81 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-04-24 * Description : A DItemToolTip prepared for use in QAbstractItemViews * * Copyright (C) 2009-2011 by Marcel Wiesweg * * 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, 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. * * ============================================================ */ #ifndef DIGIKAM_ITEM_VIEW_TOOL_TIP_H #define DIGIKAM_ITEM_VIEW_TOOL_TIP_H // Qt includes #include // Local includes #include "ditemtooltip.h" #include "digikam_export.h" namespace Digikam { class DIGIKAM_EXPORT ItemViewToolTip : public DItemToolTip { public: - explicit ItemViewToolTip(QAbstractItemView* view); + explicit ItemViewToolTip(QAbstractItemView* const view); ~ItemViewToolTip(); QAbstractItemView* view() const; QAbstractItemModel* model() const; QModelIndex currentIndex() const; /** * Show the tooltip for the given item. * The rect of the given option is taken as area for which * the tooltip is shown. */ void show(const QStyleOptionViewItem& option, const QModelIndex& index); void setTipContents(const QString& tipContents); /** * Default implementation is based on setTipContents(). * Reimplement if you dynamically provide the contents. */ virtual QString tipContents(); protected: virtual QRect repositionRect(); bool eventFilter(QObject* o, QEvent* e); void hideEvent(QHideEvent*); void mouseMoveEvent(QMouseEvent* e); private: class Private; Private* const d; }; } // namespace Digikam #endif // DIGIKAM_ITEM_VIEW_TOOL_TIP_H diff --git a/core/libs/widgets/mainview/dcursortracker.cpp b/core/libs/widgets/mainview/dcursortracker.cpp index a9661c9668..83263ee489 100644 --- a/core/libs/widgets/mainview/dcursortracker.cpp +++ b/core/libs/widgets/mainview/dcursortracker.cpp @@ -1,233 +1,232 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2007-23-03 * Description : A tool tip widget which follows cursor movements. * Tool tip content is displayed without delay. * * Copyright (C) 2007-2018 by Gilles Caulier * Copyright (C) 2009-2010 by Andi Clemens * * 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, 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. * * ============================================================ */ #include "dcursortracker.h" // Qt includes #include #include #include #include #include #include #include #include #include #include namespace Digikam { class DCursorTracker::Private { public: explicit Private() { keepOpen = false; enable = true; autoHideTimer = 0; parent = 0; } Qt::Alignment alignment; bool enable; bool keepOpen; QTimer* autoHideTimer; QPointer parent; }; DCursorTracker::DCursorTracker(const QString& txt, QWidget* const parent, Qt::Alignment align) : QLabel(txt, parent, Qt::ToolTip | Qt::BypassGraphicsProxyWidget), d(new Private) { setForegroundRole(QPalette::ToolTipText); setBackgroundRole(QPalette::ToolTipBase); - setPalette(QToolTip::palette()); ensurePolished(); const int fwidth = 1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, 0, this); setContentsMargins(fwidth, fwidth, fwidth, fwidth); setFrameStyle(QFrame::NoFrame); setAlignment(Qt::AlignLeft | Qt::AlignTop); setIndent(1); setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, 0, this) / 255.0); d->alignment = align; d->parent = parent; d->parent->setMouseTracking(true); d->parent->installEventFilter(this); d->autoHideTimer = new QTimer(this); d->autoHideTimer->setSingleShot(true); connect(d->autoHideTimer, SIGNAL(timeout()), this, SLOT(slotAutoHide())); } DCursorTracker::~DCursorTracker() { delete d; } /** * Overload to make sure the widget size is correct */ void DCursorTracker::setText(const QString& txt) { QLabel::setText(txt); adjustSize(); } void DCursorTracker::setEnable(bool b) { d->enable = b; } void DCursorTracker::setKeepOpen(bool b) { d->keepOpen = b; } void DCursorTracker::setTrackerAlignment(Qt::Alignment alignment) { d->alignment = alignment; } void DCursorTracker::triggerAutoShow(int timeout) { if (canBeDisplayed()) { show(); moveToParent(d->parent); d->autoHideTimer->start(timeout); } } void DCursorTracker::refresh() { moveToParent(d->parent); } void DCursorTracker::slotAutoHide() { hide(); } bool DCursorTracker::eventFilter(QObject* object, QEvent* e) { QWidget* const widget = static_cast(object); switch (e->type()) { case QEvent::MouseMove: { QMouseEvent* event = static_cast(e); if (canBeDisplayed() && (widget->rect().contains(event->pos()) || (event->buttons() & Qt::LeftButton))) { show(); moveToParent(widget); } else if (!d->keepOpen) { hide(); } break; } case QEvent::Leave: { if (!d->keepOpen) { hide(); } break; } default: break; } return false; } void DCursorTracker::moveToParent(QWidget* const parent) { if (!parent) { return; } switch (d->alignment) { case Qt::AlignLeft: { QPoint p = parent->mapToGlobal(QPoint(0, 0)); int y = p.y() - height(); move(p.x(), (y < 0) ? (p.y() + parent->height()) : y); break; } case Qt::AlignRight: { QPoint p = parent->mapToGlobal(QPoint(parent->width(), 0)); int y = p.y() - height(); move(p.x()-width(), (y < 0) ? (p.y() + parent->height()) : y); break; } case Qt::AlignCenter: default: { QPoint p = parent->mapToGlobal(QPoint(parent->width()/2, 0)); int y = p.y() - height(); move(p.x()-width()/2, (y < 0) ? (p.y() + parent->height()) : y); break; } } } void DCursorTracker::paintEvent(QPaintEvent* e) { QStylePainter p(this); QStyleOptionFrame opt; opt.init(this); p.drawPrimitive(QStyle::PE_PanelTipLabel, opt); p.end(); QLabel::paintEvent(e); } bool DCursorTracker::canBeDisplayed() { return d->enable && d->parent->isVisible(); } } // namespace Digikam diff --git a/core/libs/widgets/metadata/metadatalistviewitem.cpp b/core/libs/widgets/metadata/metadatalistviewitem.cpp index dc3ab8ed01..8a0551f286 100644 --- a/core/libs/widgets/metadata/metadatalistviewitem.cpp +++ b/core/libs/widgets/metadata/metadatalistviewitem.cpp @@ -1,101 +1,101 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2006-02-21 * Description : a generic list view item widget to * display metadata * * Copyright (C) 2006-2018 by Gilles Caulier * * 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, 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. * * ============================================================ */ #include "metadatalistviewitem.h" // Qt includes #include #include #include // KDE includes #include // Local includes #include "ditemtooltip.h" namespace Digikam { MetadataListViewItem::MetadataListViewItem(QTreeWidgetItem* const parent, const QString& key, - const QString& title, const QString& value) + const QString& title, const QString& value) : QTreeWidgetItem(parent), m_key(key) { setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator); setText(0, title); setToolTip(0, title); setDisabled(false); QString tagVal = value.simplified(); if (tagVal.length() > 512) { tagVal.truncate(512); tagVal.append(QLatin1String("...")); } setText(1, tagVal); DToolTipStyleSheet cnt; setToolTip(1, QLatin1String("

") + cnt.breakString(tagVal) + QLatin1String("

")); } MetadataListViewItem::MetadataListViewItem(QTreeWidgetItem* const parent, const QString& key, - const QString& title) + const QString& title) : QTreeWidgetItem(parent), m_key(key) { setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator); setText(0, title); setToolTip(0, title); setDisabled(true); setText(1, i18n("Unavailable")); QFont fnt = font(1); fnt.setItalic(true); setFont(1, fnt); } MetadataListViewItem::~MetadataListViewItem() { } QString MetadataListViewItem::getKey() { return m_key; } QString MetadataListViewItem::getTitle() { return text(0); } QString MetadataListViewItem::getValue() { return text(1); } } // namespace Digikam diff --git a/core/utilities/queuemanager/views/queuelist.cpp b/core/utilities/queuemanager/views/queuelist.cpp index 39e20b7cac..507528d94c 100644 --- a/core/utilities/queuemanager/views/queuelist.cpp +++ b/core/utilities/queuemanager/views/queuelist.cpp @@ -1,1197 +1,1198 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2008-11-21 * Description : Batch Queue Manager items list. * * Copyright (C) 2008-2018 by Gilles Caulier * Copyright (C) 2014 by Mohamed_Anwer * * 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, 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. * * ============================================================ */ #include "queuelist.h" // Qt includes #include #include #include #include #include #include #include #include #include #include // KDE includes #include #include // Local includes #include "digikam_debug.h" #include "coredb.h" #include "coredbchangesets.h" #include "coredbwatch.h" #include "ddragobjects.h" #include "defaultrenameparser.h" #include "queuemgrwindow.h" #include "queuesettings.h" #include "queuetooltip.h" #include "thumbnailloadthread.h" #include "thumbnailsize.h" #include "workflowmanager.h" #include "dlayoutbox.h" #include "dworkingpixmap.h" namespace Digikam { class QueueListViewItem::Private { public: explicit Private() : isBusy(false), done(false), hasThumb(false), progressIndex(0), view(0) { } bool isBusy; bool done; bool hasThumb; int progressIndex; QString destFileName; QPixmap preview; QueueListView* view; ImageInfo info; }; QueueListViewItem::QueueListViewItem(QueueListView* const view, const ImageInfo& info) : QTreeWidgetItem(view), d(new Private) { d->view = view; setThumb(QIcon::fromTheme(QLatin1String("view-preview")).pixmap(48, QIcon::Disabled), false); setInfo(info); } QueueListViewItem::~QueueListViewItem() { delete d; } bool QueueListViewItem::hasValidThumbnail() const { return d->hasThumb; } void QueueListViewItem::setInfo(const ImageInfo& info) { d->info = info; setText(1, d->info.name()); } ImageInfo QueueListViewItem::info() const { return d->info; } void QueueListViewItem::setPixmap(const QPixmap& pix) { QIcon icon = QIcon(pix); // We make sure the preview icon stays the same regardless of the role icon.addPixmap(pix, QIcon::Selected, QIcon::On); icon.addPixmap(pix, QIcon::Selected, QIcon::Off); icon.addPixmap(pix, QIcon::Active, QIcon::On); icon.addPixmap(pix, QIcon::Active, QIcon::Off); icon.addPixmap(pix, QIcon::Normal, QIcon::On); icon.addPixmap(pix, QIcon::Normal, QIcon::Off); setIcon(0, icon); } void QueueListViewItem::setThumb(const QPixmap& pix, bool hasThumb) { QSize iSize = treeWidget()->iconSize(); QPixmap pixmap(iSize.width() + 2, iSize.height() + 2); pixmap.fill(Qt::transparent); QPainter p(&pixmap); p.drawPixmap((pixmap.width() / 2) - (pix.width() / 2), (pixmap.height() / 2) - (pix.height() / 2), pix); d->preview = pixmap; setPixmap(d->preview); d->hasThumb = hasThumb; } void QueueListViewItem::animProgress() { QPixmap icon(d->view->progressPixmapForIndex(d->progressIndex)); d->progressIndex++; if (d->view->progressPixmapForIndex(d->progressIndex).isNull()) { d->progressIndex = 0; } QPixmap preview = d->preview; QPixmap mask(preview.size()); mask.fill(QColor(128, 128, 128, 192)); QPainter p(&preview); p.drawPixmap(0, 0, mask); p.drawPixmap((preview.width() / 2) - (icon.width() / 2), (preview.height() / 2) - (icon.height() / 2), icon); setPixmap(preview); } void QueueListViewItem::setCanceled() { setPixmap(d->preview); setIcon(1, QIcon::fromTheme(QLatin1String("dialog-cancel"))); d->done = false; d->isBusy = false; d->progressIndex = 0; } void QueueListViewItem::setFailed() { setPixmap(d->preview); setIcon(1, QIcon::fromTheme(QLatin1String("dialog-error"))); d->done = false; d->isBusy = false; d->progressIndex = 0; } void QueueListViewItem::setDone() { setPixmap(d->preview); setIcon(1, QIcon::fromTheme(QLatin1String("dialog-ok-apply"))); d->done = true; d->isBusy = false; d->progressIndex = 0; } bool QueueListViewItem::isDone() const { return d->done; } void QueueListViewItem::reset() { setPixmap(d->preview); setIcon(1, QIcon()); d->done = false; d->isBusy = false; d->progressIndex = 0; } void QueueListViewItem::setBusy() { d->isBusy = true; } bool QueueListViewItem::isBusy() const { return d->isBusy; } void QueueListViewItem::setDestFileName(const QString& str) { d->destFileName = str; setText(2, d->destFileName); } QString QueueListViewItem::destFileName() const { return d->destFileName; } QString QueueListViewItem::destBaseName() const { QFileInfo fi(d->destFileName); return fi.completeBaseName(); } QString QueueListViewItem::destSuffix() const { QFileInfo fi(d->destFileName); return fi.suffix(); } // --------------------------------------------------------------------------- class QueueListView::Private { public: enum RemoveItemsType { ItemsSelected = 0, ItemsDone, ItemsAll }; public: explicit Private() : iconSize(64) { showTips = false; toolTipTimer = 0; progressTimer = 0; toolTip = 0; toolTipItem = 0; thumbLoadThread = ThumbnailLoadThread::defaultThread(); progressPix = DWorkingPixmap(); } bool showTips; const int iconSize; QTimer* toolTipTimer; QTimer* progressTimer; ThumbnailLoadThread* thumbLoadThread; QueueSettings settings; AssignedBatchTools toolsList; QueueToolTip* toolTip; QueueListViewItem* toolTipItem; DWorkingPixmap progressPix; }; QueueListView::QueueListView(QWidget* const parent) : QTreeWidget(parent), d(new Private) { setIconSize(QSize(d->iconSize, d->iconSize)); setSelectionMode(QAbstractItemView::ExtendedSelection); setWhatsThis(i18n("This is the list of images to batch process.")); setAcceptDrops(true); viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setDragEnabled(true); viewport()->setMouseTracking(true); setSortingEnabled(true); sortByColumn(1,Qt::AscendingOrder); setAllColumnsShowFocus(true); setRootIsDecorated(false); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setColumnCount(3); setContextMenuPolicy(Qt::CustomContextMenu); QStringList titles; titles.append(i18n("Thumbnail")); titles.append(i18n("Original")); titles.append(i18n("Target")); setHeaderLabels(titles); header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); header()->setSectionResizeMode(1, QHeaderView::Stretch); header()->setSectionResizeMode(2, QHeaderView::Stretch); d->toolTip = new QueueToolTip(this); d->toolTipTimer = new QTimer(this); d->progressTimer = new QTimer(this); // ----------------------------------------------------------- connect(CoreDbAccess::databaseWatch(), SIGNAL(collectionImageChange(CollectionImageChangeset)), this, SLOT(slotCollectionImageChange(CollectionImageChangeset)), Qt::QueuedConnection); connect(d->thumbLoadThread, SIGNAL(signalThumbnailLoaded(LoadingDescription,QPixmap)), this, SLOT(slotThumbnailLoaded(LoadingDescription,QPixmap))); connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotContextMenu())); connect(d->toolTipTimer, SIGNAL(timeout()), this, SLOT(slotToolTip())); connect(d->progressTimer, SIGNAL(timeout()), this, SLOT(slotProgressTimerDone())); } QueueListView::~QueueListView() { delete d->toolTip; delete d; } QPixmap QueueListView::progressPixmapForIndex(int index) const { if (index >= 0 && index < d->progressPix.frameCount()) return (d->progressPix.frameAt(index)); return QPixmap(); } Qt::DropActions QueueListView::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QMimeData* QueueListView::mimeData(const QList items) const { QList urls; QList albumIDs; QList imageIDs; foreach(QTreeWidgetItem* const itm, items) { QueueListViewItem* const vitem = dynamic_cast(itm); if (vitem) { urls.append(vitem->info().fileUrl()); albumIDs.append(vitem->info().albumId()); imageIDs.append(vitem->info().id()); } } DItemDrag* const mimeData = new DItemDrag(urls, albumIDs, imageIDs); return mimeData; } void QueueListView::startDrag(Qt::DropActions /*supportedActions*/) { QList items = selectedItems(); if (items.isEmpty()) { return; } QPixmap icon(QIcon::fromTheme(QLatin1String("image-jpeg")).pixmap(48)); int w = icon.width(); int h = icon.height(); QPixmap pix(w + 4, h + 4); QString text(QString::number(items.count())); QPainter p(&pix); p.fillRect(0, 0, pix.width() - 1, pix.height() - 1, QColor(Qt::white)); p.setPen(QPen(Qt::black, 1)); p.drawRect(0, 0, pix.width() - 1, pix.height() - 1); p.drawPixmap(2, 2, icon); QRect r = p.boundingRect(2, 2, w, h, Qt::AlignLeft | Qt::AlignTop, text); r.setWidth(qMax(r.width(), r.height())); r.setHeight(qMax(r.width(), r.height())); p.fillRect(r, QColor(0, 80, 0)); p.setPen(Qt::white); QFont f(font()); f.setBold(true); p.setFont(f); p.drawText(r, Qt::AlignCenter, text); p.end(); QDrag* const drag = new QDrag(this); drag->setMimeData(mimeData(items)); drag->setPixmap(pix); drag->exec(); } void QueueListView::dragEnterEvent(QDragEnterEvent* e) { QTreeWidget::dragEnterEvent(e); e->accept(); } void QueueListView::dragMoveEvent(QDragMoveEvent* e) { int albumID; QList albumIDs; QList imageIDs; QList urls; if (DItemDrag::decode(e->mimeData(), urls, albumIDs, imageIDs) || DAlbumDrag::decode(e->mimeData(), urls, albumID) || DTagListDrag::canDecode(e->mimeData())) { if (DItemDrag::decode(e->mimeData(), urls, albumIDs, imageIDs)) { ImageInfoList imageInfoList; for (QList::const_iterator it = imageIDs.constBegin(); it != imageIDs.constEnd(); ++it) { ImageInfo info(*it); if (!findItemByInfo(info)) { imageInfoList.append(info); } } if (!imageInfoList.isEmpty()) { QTreeWidget::dragMoveEvent(e); e->accept(); return; } } } else if (e->mimeData()->formats().contains(QLatin1String("digikam/workflow"))) { QTreeWidget::dragMoveEvent(e); e->accept(); return; } e->ignore(); } void QueueListView::dropEvent(QDropEvent* e) { int albumID; QList albumIDs; QList imageIDs; QList urls; if (DItemDrag::decode(e->mimeData(), urls, albumIDs, imageIDs)) { ImageInfoList imageInfoList; for (QList::const_iterator it = imageIDs.constBegin(); it != imageIDs.constEnd(); ++it) { ImageInfo info(*it); if (!findItemByInfo(info)) { imageInfoList.append(info); } } if (!imageInfoList.isEmpty()) { slotAddItems(imageInfoList); e->acceptProposedAction(); QueueListView* const vitem = dynamic_cast(e->source()); if (vitem && vitem != this) { foreach(const ImageInfo& info, imageInfoList) { vitem->removeItemByInfo(info); } } } } else if (DAlbumDrag::decode(e->mimeData(), urls, albumID)) { QList itemIDs = CoreDbAccess().db()->getItemIDsInAlbum(albumID); ImageInfoList imageInfoList; for (QList::const_iterator it = itemIDs.constBegin(); it != itemIDs.constEnd(); ++it) { ImageInfo info(*it); if (!findItemByInfo(info)) { imageInfoList.append(info); } } if (!imageInfoList.isEmpty()) { slotAddItems(imageInfoList); e->acceptProposedAction(); } } else if (DTagListDrag::canDecode(e->mimeData())) { QList tagIDs; if (!DTagListDrag::decode(e->mimeData(), tagIDs)) { return; } QList itemIDs = CoreDbAccess().db()->getItemIDsInTag(tagIDs.first(), true); ImageInfoList imageInfoList; for (QList::const_iterator it = itemIDs.constBegin(); it != itemIDs.constEnd(); ++it) { ImageInfo info(*it); if (!findItemByInfo(info)) { imageInfoList.append(info); } } if (!imageInfoList.isEmpty()) { slotAddItems(imageInfoList); e->acceptProposedAction(); } } else if (e->mimeData()->formats().contains(QLatin1String("digikam/workflow"))) { QByteArray ba = e->mimeData()->data(QLatin1String("digikam/workflow")); if (ba.size()) { QDataStream ds(ba); QString title; ds >> title; QueueMgrWindow::queueManagerWindow()->slotAssignQueueSettings(title); e->acceptProposedAction(); } } else { e->ignore(); } emit signalQueueContentsChanged(); } void QueueListView::setEnableToolTips(bool val) { d->showTips = val; if (!val) { hideToolTip(); } } void QueueListView::hideToolTip() { d->toolTipItem = 0; d->toolTipTimer->stop(); slotToolTip(); } bool QueueListView::acceptToolTip(const QPoint& pos) const { if (columnAt(pos.x()) == 0) { return true; } return false; } void QueueListView::slotToolTip() { d->toolTip->setQueueItem(d->toolTipItem); } void QueueListView::mouseMoveEvent(QMouseEvent* e) { if (e->buttons() == Qt::NoButton) { QueueListViewItem* item = dynamic_cast(itemAt(e->pos())); if (d->showTips) { if (!isActiveWindow()) { hideToolTip(); return; } if (item != d->toolTipItem) { hideToolTip(); if (acceptToolTip(e->pos())) { d->toolTipItem = item; d->toolTipTimer->setSingleShot(true); d->toolTipTimer->start(500); } } if (item == d->toolTipItem && !acceptToolTip(e->pos())) { hideToolTip(); } } return; } hideToolTip(); QTreeWidget::mouseMoveEvent(e); } void QueueListView::wheelEvent(QWheelEvent* e) { hideToolTip(); QTreeWidget::wheelEvent(e); } void QueueListView::keyPressEvent(QKeyEvent* e) { hideToolTip(); QTreeWidget::keyPressEvent(e); } void QueueListView::focusOutEvent(QFocusEvent* e) { hideToolTip(); QTreeWidget::focusOutEvent(e); } void QueueListView::leaveEvent(QEvent* e) { hideToolTip(); QTreeWidget::leaveEvent(e); } void QueueListView::slotAddItems(const ImageInfoList& list) { if (list.count() == 0) { return; } for (ImageInfoList::ConstIterator it = list.begin(); it != list.end(); ++it) { ImageInfo info = *it; // Check if the new item already exist in the list. bool find = false; QueueListViewItem* item = 0; QTreeWidgetItemIterator iter(this); while (*iter) { item = dynamic_cast(*iter); if (item && item->info() == info) { find = true; } ++iter; } if (!find) { new QueueListViewItem(this, info); } } updateDestFileNames(); emit signalQueueContentsChanged(); } void QueueListView::drawRow(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& index) const { QueueListViewItem* const item = dynamic_cast(itemFromIndex(index)); if (item && !item->hasValidThumbnail()) { ImageInfo info = item->info(); d->thumbLoadThread->find(ThumbnailIdentifier(info.fileUrl().toLocalFile())); } QTreeWidget::drawRow(p, opt, index); } void QueueListView::slotThumbnailLoaded(const LoadingDescription& desc, const QPixmap& pix) { QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item && item->info().fileUrl() == QUrl::fromLocalFile(desc.filePath)) { if (pix.isNull()) { item->setThumb(QIcon::fromTheme(QLatin1String("view-preview")).pixmap(d->iconSize, QIcon::Disabled)); } else { item->setThumb(pix.scaled(d->iconSize, d->iconSize, Qt::KeepAspectRatio)); } return; } ++it; } } void QueueListView::slotClearList() { removeItems(Private::ItemsAll); emit signalQueueContentsChanged(); } void QueueListView::slotRemoveSelectedItems() { removeItems(Private::ItemsSelected); emit signalQueueContentsChanged(); } void QueueListView::slotRemoveItemsDone() { removeItems(Private::ItemsDone); emit signalQueueContentsChanged(); } void QueueListView::removeItems(int removeType) { hideToolTip(); bool find; do { find = false; QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item) { switch (removeType) { case Private::ItemsSelected: { if (item->isSelected()) { delete item; find = true; } break; } case Private::ItemsDone: { if (item->isDone()) { delete item; find = true; } break; } default: // ItemsAll { delete item; find = true; break; } } } ++it; } } while (find); emit signalQueueContentsChanged(); } void QueueListView::removeItemByInfo(const ImageInfo& info) { removeItemById(info.id()); } void QueueListView::removeItemById(qlonglong id) { hideToolTip(); bool find; do { find = false; QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item && item->info().id() == id) { delete item; find = true; break; } ++it; } } while (find); emit signalQueueContentsChanged(); } bool QueueListView::findItemByInfo(const ImageInfo& info) { return (findItemById(info.id()) ? true : false); } QueueListViewItem* QueueListView::findItemById(qlonglong id) { QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item && item->info().id() == id) { return item; } ++it; } return 0; } QueueListViewItem* QueueListView::findItemByUrl(const QUrl& url) { QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item && item->info().fileUrl() == url) { return item; } ++it; } return 0; } int QueueListView::itemsCount() { int count = 0; QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item) { ++count; } ++it; } return count; } void QueueListView::cancelItems() { QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item && item->isBusy()) { item->setCanceled(); } ++it; } } ImageInfoList QueueListView::pendingItemsList() { ImageInfoList list; QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item && !item->isDone()) { list.append(item->info()); } ++it; } return list; } int QueueListView::pendingItemsCount() { return pendingItemsList().count(); } int QueueListView::pendingTasksCount() { int count = 0; QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item && !item->isDone()) { count += assignedTools().m_toolsList.count(); } ++it; } return count; } void QueueListView::setSettings(const QueueSettings& settings) { d->settings = settings; resetQueue(); updateDestFileNames(); } QueueSettings QueueListView::settings() const { return d->settings; } void QueueListView::setAssignedTools(const AssignedBatchTools& tools) { d->toolsList = tools; resetQueue(); updateDestFileNames(); } AssignedBatchTools QueueListView::assignedTools() const { return d->toolsList; } void QueueListView::slotAssignedToolsChanged(const AssignedBatchTools& tools) { setAssignedTools(tools); } void QueueListView::resetQueue() { //reset all items QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item) { item->reset(); } it++; } } void QueueListView::updateDestFileNames() { QMap renamingResults; if (settings().renamingRule == QueueSettings::CUSTOMIZE) { AdvancedRenameManager manager; ParseSettings psettings; psettings.parseString = settings().renamingParser; QList files; QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item) { // Update base name using queue renaming rules. ImageInfo info = item->info(); QFileInfo fi(info.filePath()); ParseSettings ps; ps.fileUrl = QUrl::fromLocalFile(fi.absoluteFilePath()); files << ps; } ++it; } manager.addFiles(files); manager.parseFiles(psettings); renamingResults = manager.newFileList(); } AssignedBatchTools tools = assignedTools(); QTreeWidgetItemIterator it(this); while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item) { // Update base name using queue renaming rules. ImageInfo info = item->info(); QFileInfo fi(info.filePath()); // Update suffix using assigned batch tool rules. bool extensionSet = false; tools.m_itemUrl = item->info().fileUrl(); QString newSuffix = tools.targetSuffix(&extensionSet); QString newName = QString::fromUtf8("%1.%2").arg(fi.completeBaseName()).arg(newSuffix); if (settings().renamingRule == QueueSettings::CUSTOMIZE && !renamingResults.isEmpty()) { QFileInfo fi2(renamingResults[fi.absoluteFilePath()]); if (extensionSet) { newName = QString::fromUtf8("%1.%2").arg(fi2.completeBaseName()) .arg(newSuffix); } else { newName = fi2.fileName(); } } item->setDestFileName(newName); } ++it; } } void QueueListView::slotContextMenu() { if (!viewport()->isEnabled()) { return; } KActionCollection* const acol = QueueMgrWindow::queueManagerWindow()->actionCollection(); QMenu popmenu(this); popmenu.addAction(acol->action(QLatin1String("queuemgr_removeitemssel"))); popmenu.addSeparator(); popmenu.addAction(acol->action(QLatin1String("queuemgr_clearlist"))); popmenu.exec(QCursor::pos()); } void QueueListView::slotCollectionImageChange(const CollectionImageChangeset& changeset) { if (QueueMgrWindow::queueManagerWindow()->isBusy()) { return; } switch (changeset.operation()) { + case CollectionImageChangeset::Deleted: case CollectionImageChangeset::Removed: case CollectionImageChangeset::RemovedAll: { foreach(const qlonglong& id, changeset.ids()) { removeItemById(id); } break; } default: { break; } } } void QueueListView::reloadThumbs(const QUrl& url) { d->thumbLoadThread->find(ThumbnailIdentifier(url.toLocalFile())); } void QueueListView::setItemBusy(qlonglong id) { QueueListViewItem* const item = findItemById(id); if (item) { item->setBusy(); d->progressTimer->start(300); } } void QueueListView::slotProgressTimerDone() { QTreeWidgetItemIterator it(this); int active = 0; while (*it) { QueueListViewItem* const item = dynamic_cast(*it); if (item && item->isBusy()) { item->animProgress(); active++; } it++; } if (!active) d->progressTimer->stop(); } } // namespace Digikam diff --git a/project/reports/krazy.sh b/project/reports/krazy.sh index 30826c8fa0..bcd684ce17 100755 --- a/project/reports/krazy.sh +++ b/project/reports/krazy.sh @@ -1,70 +1,69 @@ #!/bin/bash # Copyright (c) 2013-2018, Gilles Caulier, # # Run Krazy static analyzer on whole digiKam source code. # https://github.com/Krazy-collection/krazy # Dependencies: # - Perl:Tie::IxHash and Perl:XML::LibXML modules at run-time. # - Saxon java xml parser to export report as HTML. # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. # . ./common.sh checksCPUCores ORIG_WD="`pwd`" REPORT_DIR="${ORIG_WD}/report.krazy" WEBSITE_DIR="${ORIG_WD}/site" # Get active git branches to create report description string TITLE="digiKam-$(parseGitBranch)$(parseGitHash)" echo "Krazy Static Analyzer task name: $TITLE" rm -fr $REPORT_DIR rm -fr $WEBSITE_DIR # Compute static analyzer output as XML TITLE_EXT=$TITLE+" This is the static analyzis processed with Extra checkers. See EBN for standard analyzis." krazy2all --export xml \ --title $TITLE \ --no-brief \ --strict all \ --priority all \ --verbose \ --topdir ../../ \ - --check multiclasses \ --outfile ./report.krazy.xml # Clean up XML file sed -i "s/repo-rev value=\"unknown\"/repo-rev value=\"$(parseGitBranch)$(parseGitHash)\"/g" ./report.krazy.xml DROP_PATH=$(echo $ORIG_WD | rev | cut -d'/' -f3- | rev | sed 's_/_\\/_g') sed -i "s/$DROP_PATH//g" ./report.krazy.xml mkdir -p $REPORT_DIR # Process XML file to generate HTML java -jar /usr/share/java/saxon/saxon.jar \ -o:$REPORT_DIR/index.html \ -im:krazy2ebn \ ./report.krazy.xml \ ./krazy/krazy-main.xsl \ module=graphics \ submodule=digikam \ component=extragear cp ./krazy/style.css $REPORT_DIR/ # update www.digikam.org report section. updateReportToWebsite "krazy" $REPORT_DIR $TITLE $(parseGitBranch) cd $ORIG_DIR