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