diff --git a/core/app/dragdrop/albumdragdrop.cpp b/core/app/dragdrop/albumdragdrop.cpp index 91fad00751..721956fa9c 100644 --- a/core/app/dragdrop/albumdragdrop.cpp +++ b/core/app/dragdrop/albumdragdrop.cpp @@ -1,432 +1,433 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-04-16 * Description : Qt Model for Albums - drag and drop handling * * Copyright (C) 2005-2006 by Joern Ahrens * Copyright (C) 2006-2018 by Gilles Caulier * Copyright (C) 2009-2011 by Marcel Wiesweg * Copyright (C) 2009 by Andi Clemens * 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 "albumdragdrop.h" // Qt includes #include #include #include // KDE includes #include // Local includes #include "digikam_debug.h" #include "albummanager.h" +#include "albumpointer.h" #include "importui.h" #include "ddragobjects.h" #include "dio.h" #include "imageinfo.h" #include "imageinfolist.h" namespace Digikam { AlbumDragDropHandler::AlbumDragDropHandler(AlbumModel* const model) : AlbumModelDragDropHandler(model) { } AlbumModel* AlbumDragDropHandler::model() const { return static_cast(m_model); } bool AlbumDragDropHandler::dropEvent(QAbstractItemView* view, const QDropEvent* e, const QModelIndex& droppedOn) { if (accepts(e, droppedOn) == Qt::IgnoreAction) { return false; } AlbumPointer destAlbum = model()->albumForIndex(droppedOn); if (!destAlbum) { return false; } if (DAlbumDrag::canDecode(e->mimeData())) { QList urls; int albumId = 0; if (!DAlbumDrag::decode(e->mimeData(), urls, albumId)) { return false; } AlbumPointer droppedAlbum = AlbumManager::instance()->findPAlbum(albumId); if (!droppedAlbum) { return false; } // TODO Copy? QMenu popMenu(view); QAction* const moveAction = popMenu.addAction(QIcon::fromTheme(QLatin1String("go-jump")), i18n("&Move Here")); popMenu.addSeparator(); popMenu.addAction(QIcon::fromTheme(QLatin1String("dialog-cancel")), i18n("C&ancel")); popMenu.setMouseTracking(true); QAction* const choice = popMenu.exec(QCursor::pos()); if (!droppedAlbum || !destAlbum) { return false; } if (choice == moveAction) { DIO::move(droppedAlbum, destAlbum); } return true; } else if (DItemDrag::canDecode(e->mimeData())) { QList urls; QList albumIDs; QList imageIDs; if (!DItemDrag::decode(e->mimeData(), urls, albumIDs, imageIDs)) { return false; } if (urls.isEmpty() || albumIDs.isEmpty() || imageIDs.isEmpty()) { return false; } // Check if items dropped come from outside current album. // This can be the case with recursive content album mode. ImageInfoList extImgInfList; for (QList::const_iterator it = imageIDs.constBegin(); it != imageIDs.constEnd(); ++it) { ImageInfo info(*it); if (info.albumId() != destAlbum->id()) { extImgInfList << info; } } if (extImgInfList.isEmpty()) { // Setting the dropped image as the album thumbnail // If the ctrl key is pressed, when dropping the image, the // thumbnail is set without a popup menu bool set = false; if (e->keyboardModifiers() == Qt::ControlModifier) { set = true; } else { QMenu popMenu(view); QAction* setAction = 0; if (imageIDs.count() == 1) { setAction = popMenu.addAction(i18n("Set as Album Thumbnail")); } popMenu.addSeparator(); popMenu.addAction(QIcon::fromTheme(QLatin1String("dialog-cancel")), i18n("C&ancel")); popMenu.setMouseTracking(true); QAction* const choice = popMenu.exec(QCursor::pos()); set = (setAction == choice); } if (set && destAlbum) { QString errMsg; AlbumManager::instance()->updatePAlbumIcon(destAlbum, imageIDs.first(), errMsg); } return true; } // If shift key is pressed while dragging, move the drag object without // displaying popup menu -> move bool move = false; bool copy = false; bool setThumbnail = false; if (e->keyboardModifiers() == Qt::ShiftModifier) { move = true; } // If ctrl key is pressed while dragging, copy the drag object without // displaying popup menu -> copy else if (e->keyboardModifiers() == Qt::ControlModifier) { copy = true; } else { QMenu popMenu(view); QAction* const moveAction = popMenu.addAction(QIcon::fromTheme(QLatin1String("go-jump")), i18n("&Move Here")); QAction* const copyAction = popMenu.addAction(QIcon::fromTheme(QLatin1String("edit-copy")), i18n("&Copy Here")); QAction* thumbnailAction = 0; if (imageIDs.count() == 1) { thumbnailAction = popMenu.addAction(i18n("Set as Album Thumbnail")); } popMenu.addSeparator(); popMenu.addAction(QIcon::fromTheme(QLatin1String("dialog-cancel")), i18n("C&ancel")); popMenu.setMouseTracking(true); QAction* const choice = popMenu.exec(QCursor::pos()); if (choice) { if (choice == moveAction) { move = true; } else if (choice == copyAction) { copy = true; } else if (choice == thumbnailAction) { setThumbnail = true; } } } if (!destAlbum) { return false; } if (move) { DIO::move(extImgInfList, destAlbum); } else if (copy) { DIO::copy(extImgInfList, destAlbum); } else if (setThumbnail) { QString errMsg; AlbumManager::instance()->updatePAlbumIcon(destAlbum, extImgInfList.first().id(), errMsg); } return true; } // -- DnD from Camera GUI ---------------------------- else if (DCameraItemListDrag::canDecode(e->mimeData())) { ImportUI* const ui = dynamic_cast(e->source()); if (ui) { QMenu popMenu(view); QAction* const downAction = popMenu.addAction(QIcon::fromTheme(QLatin1String("file-export")), i18n("Download From Camera")); QAction* const downDelAction = popMenu.addAction(QIcon::fromTheme(QLatin1String("file-export")), i18n("Download && Delete From Camera")); popMenu.addSeparator(); popMenu.addAction(QIcon::fromTheme(QLatin1String("dialog-cancel")), i18n("C&ancel")); popMenu.setMouseTracking(true); QAction* const choice = popMenu.exec(QCursor::pos()); if (choice && destAlbum) { if (choice == downAction) { ui->slotDownload(true, false, destAlbum); } else if (choice == downDelAction) { ui->slotDownload(true, true, destAlbum); } } } } // -- DnD from an external source --------------------- else if (e->mimeData()->hasUrls()) { QList srcURLs = e->mimeData()->urls(); bool move = false; bool copy = false; // If shift key is pressed while dropping, move the drag object without // displaying popup menu -> move if (e->keyboardModifiers() == Qt::ShiftModifier) { move = true; } // If ctrl key is pressed while dropping, copy the drag object without // displaying popup menu -> copy else if (e->keyboardModifiers() == Qt::ControlModifier) { copy = true; } else { QMenu popMenu(view); QAction* const moveAction = popMenu.addAction(QIcon::fromTheme(QLatin1String("go-jump")), i18n("&Move Here")); QAction* const copyAction = popMenu.addAction(QIcon::fromTheme(QLatin1String("edit-copy")), i18n("&Copy Here")); popMenu.addSeparator(); popMenu.addAction(QIcon::fromTheme(QLatin1String("dialog-cancel")), i18n("C&ancel")); popMenu.setMouseTracking(true); QAction* const choice = popMenu.exec(QCursor::pos()); if (choice == copyAction) { copy = true; } else if (choice == moveAction) { move = true; } } if (!destAlbum) { return false; } if (move) { DIO::move(srcURLs, destAlbum); } else if (copy) { DIO::copy(srcURLs, destAlbum); } return true; } return false; } Qt::DropAction AlbumDragDropHandler::accepts(const QDropEvent* e, const QModelIndex& dropIndex) { PAlbum* const destAlbum = model()->albumForIndex(dropIndex); if (!destAlbum) { return Qt::IgnoreAction; } // Dropping on root is not allowed and // Dropping on trash is not implemented yet if (destAlbum->isRoot() || destAlbum->isTrashAlbum()) { return Qt::IgnoreAction; } if (DAlbumDrag::canDecode(e->mimeData())) { QList urls; int albumId = 0; if (!DAlbumDrag::decode(e->mimeData(), urls, albumId)) { return Qt::IgnoreAction; } PAlbum* const droppedAlbum = AlbumManager::instance()->findPAlbum(albumId); if (!droppedAlbum) { return Qt::IgnoreAction; } // Dragging an item on itself makes no sense if (droppedAlbum == destAlbum) { return Qt::IgnoreAction; } // Dragging a parent on its child makes no sense if (droppedAlbum->isAncestorOf(destAlbum)) { return Qt::IgnoreAction; } return Qt::MoveAction; } else if (DItemDrag::canDecode(e->mimeData()) || DCameraItemListDrag::canDecode(e->mimeData()) || e->mimeData()->hasUrls()) { return Qt::MoveAction; } return Qt::IgnoreAction; } QStringList AlbumDragDropHandler::mimeTypes() const { QStringList mimeTypes; mimeTypes << DAlbumDrag::mimeTypes() << DItemDrag::mimeTypes() << DCameraItemListDrag::mimeTypes() << QLatin1String("text/uri-list"); return mimeTypes; } QMimeData* AlbumDragDropHandler::createMimeData(const QList& albums) { if (albums.isEmpty()) { return 0; } if (albums.size() > 1) { qCWarning(DIGIKAM_GENERAL_LOG) << "Dragging multiple albums is not implemented"; } PAlbum* const palbum = dynamic_cast(albums.first()); // Root and Trash Albums are not dragable if (!palbum || palbum->isRoot() || palbum->isTrashAlbum()) { return 0; } return (new DAlbumDrag(albums.first()->databaseUrl(), albums.first()->id(), palbum->fileUrl())); } } // namespace Digikam diff --git a/core/app/utils/contextmenuhelper.cpp b/core/app/utils/contextmenuhelper.cpp index 21bfcce550..688ab6348c 100644 --- a/core/app/utils/contextmenuhelper.cpp +++ b/core/app/utils/contextmenuhelper.cpp @@ -1,1159 +1,1160 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-02-15 * Description : contextmenu helper class * * Copyright (C) 2009-2011 by Andi Clemens * Copyright (C) 2010-2018 by Gilles Caulier * * This program is free software; you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; * either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #include "contextmenuhelper.h" // Qt includes #include #include #include #include #include #include #include #include #include #include // KDE includes #include #include #ifdef HAVE_KIO # include #endif // Local includes #include "digikam_debug.h" #include "album.h" #include "coredb.h" #include "albummanager.h" +#include "albumpointer.h" #include "albummodificationhelper.h" #include "abstractalbummodel.h" #include "coredbaccess.h" #include "digikamapp.h" #include "dfileoperations.h" #include "imageinfo.h" #include "imagefiltermodel.h" #include "lighttablewindow.h" #include "queuemgrwindow.h" #include "picklabelwidget.h" #include "colorlabelwidget.h" #include "ratingwidget.h" #include "tagmodificationhelper.h" #include "tagspopupmenu.h" #include "fileactionmngr.h" #include "tagscache.h" #include "dimg.h" #include "dxmlguiwindow.h" #ifdef HAVE_AKONADICONTACT # include "akonadiiface.h" #endif namespace Digikam { class Q_DECL_HIDDEN ContextMenuHelper::Private { public: explicit Private(ContextMenuHelper* const q) : gotoAlbumAction(0), gotoDateAction(0), setThumbnailAction(0), imageFilterModel(0), albumModel(0), parent(0), stdActionCollection(0), q(q) { } QAction* gotoAlbumAction; QAction* gotoDateAction; QAction* setThumbnailAction; QList selectedIds; QList selectedItems; QMap queueActions; QMap servicesMap; ImageFilterModel* imageFilterModel; AbstractCheckableAlbumModel* albumModel; QMenu* parent; KActionCollection* stdActionCollection; ContextMenuHelper* q; public: QModelIndex indexForAlbumFromAction(QObject* sender) const { QAction* action = 0; if ((action = qobject_cast(sender))) { Album* const album = action->data().value >(); return albumModel->indexForAlbum(album); } return QModelIndex(); } QAction* copyFromMainCollection(const QString& name) const { QAction* const mainAction = stdActionCollection->action(name); if (!mainAction) { return 0; } QAction* const action = new QAction(mainAction->icon(), mainAction->text(), q); action->setShortcut(mainAction->shortcut()); action->setToolTip(mainAction->toolTip()); return action; } }; ContextMenuHelper::ContextMenuHelper(QMenu* const parent, KActionCollection* const actionCollection) : QObject(parent), d(new Private(this)) { d->parent = parent; if (!actionCollection) { d->stdActionCollection = DigikamApp::instance()->actionCollection(); } else { d->stdActionCollection = actionCollection; } } ContextMenuHelper::~ContextMenuHelper() { delete d; } void ContextMenuHelper::addAction(const QString& name, bool addDisabled) { QAction* const action = d->stdActionCollection->action(name); addAction(action, addDisabled); } void ContextMenuHelper::addAction(QAction* action, bool addDisabled) { if (!action) { return; } if (action->isEnabled() || addDisabled) { d->parent->addAction(action); } } void ContextMenuHelper::addSubMenu(QMenu* subMenu) { d->parent->addMenu(subMenu); } void ContextMenuHelper::addSeparator() { d->parent->addSeparator(); } void ContextMenuHelper::addAction(QAction* action, QObject* recv, const char* slot, bool addDisabled) { if (!action) { return; } connect(action, SIGNAL(triggered()), recv, slot); addAction(action, addDisabled); } void ContextMenuHelper::addStandardActionLightTable() { QAction* action = 0; QStringList ltActionNames; ltActionNames << QLatin1String("image_add_to_lighttable") << QLatin1String("image_lighttable"); if (LightTableWindow::lightTableWindowCreated() && !LightTableWindow::lightTableWindow()->isEmpty()) { action = d->stdActionCollection->action(ltActionNames.at(0)); } else { action = d->stdActionCollection->action(ltActionNames.at(1)); } addAction(action); } void ContextMenuHelper::addStandardActionThumbnail(const imageIds& ids, Album* album) { if (d->setThumbnailAction) { return; } setSelectedIds(ids); if (album && ids.count() == 1) { if (album->type() == Album::PHYSICAL) { d->setThumbnailAction = new QAction(i18n("Set as Album Thumbnail"), this); } else if (album->type() == Album::TAG) { d->setThumbnailAction = new QAction(i18n("Set as Tag Thumbnail"), this); } addAction(d->setThumbnailAction); d->parent->addSeparator(); } } void ContextMenuHelper::addOpenAndNavigateActions(const imageIds &ids) { addAction(QLatin1String("image_edit")); addServicesMenu(ImageInfoList(ids).toImageUrlList()); addAction(QLatin1String("move_selection_to_album")); addGotoMenu(ids); } void ContextMenuHelper::addServicesMenu(const QList& selectedItems) { setSelectedItems(selectedItems); KService::List offers = DFileOperations::servicesForOpenWith(selectedItems); if (!offers.isEmpty()) { QMenu* const servicesMenu = new QMenu(d->parent); qDeleteAll(servicesMenu->actions()); QAction* const serviceAction = servicesMenu->menuAction(); serviceAction->setText(i18n("Open With")); foreach(const KService::Ptr& service, offers) { QString name = service->name().replace(QLatin1Char('&'), QLatin1String("&&")); QAction* const action = servicesMenu->addAction(name); action->setIcon(QIcon::fromTheme(service->icon())); action->setData(service->name()); d->servicesMap[name] = service; } #ifdef HAVE_KIO servicesMenu->addSeparator(); servicesMenu->addAction(i18n("Other...")); addAction(serviceAction); connect(servicesMenu, SIGNAL(triggered(QAction*)), this, SLOT(slotOpenWith(QAction*))); } else { QAction* const serviceAction = new QAction(i18n("Open With..."), this); addAction(serviceAction); connect(serviceAction, SIGNAL(triggered()), this, SLOT(slotOpenWith())); #endif // HAVE_KIO } } void ContextMenuHelper::slotOpenWith() { // call the slot with an "empty" action slotOpenWith(0); } void ContextMenuHelper::slotOpenWith(QAction* action) { KService::Ptr service; QList list = d->selectedItems; QString name = action ? action->data().toString() : QString(); #ifdef HAVE_KIO if (name.isEmpty()) { QPointer dlg = new KOpenWithDialog(list); if (dlg->exec() != KOpenWithDialog::Accepted) { delete dlg; return; } service = dlg->service(); if (!service) { // User entered a custom command if (!dlg->text().isEmpty()) { DFileOperations::runFiles(dlg->text(), list); } delete dlg; return; } delete dlg; } else #endif // HAVE_KIO { service = d->servicesMap[name]; } DFileOperations::runFiles(service.data(), list); } bool ContextMenuHelper::imageIdsHaveSameCategory(const imageIds& ids, DatabaseItem::Category category) { bool sameCategory = true; QVariantList varList; foreach(const qlonglong& id, ids) { varList = CoreDbAccess().db()->getImagesFields(id, DatabaseFields::Category); if (varList.isEmpty() || (DatabaseItem::Category)varList.first().toInt() != category) { sameCategory = false; break; } } return sameCategory; } void ContextMenuHelper::addActionNewTag(TagModificationHelper* helper, TAlbum* tag) { QAction* const newTagAction = new QAction(QIcon::fromTheme(QLatin1String("tag-new")), i18n("New Tag..."), this); addAction(newTagAction); helper->bindTag(newTagAction, tag); connect(newTagAction, SIGNAL(triggered()), helper, SLOT(slotTagNew())); } void ContextMenuHelper::addActionDeleteTag(TagModificationHelper* helper, TAlbum* tag) { QAction* const deleteTagAction = new QAction(QIcon::fromTheme(QLatin1String("user-trash")), i18n("Delete Tag"), this); addAction(deleteTagAction); helper->bindTag(deleteTagAction, tag); connect(deleteTagAction, SIGNAL(triggered()), helper, SLOT(slotTagDelete())); } void ContextMenuHelper::addActionDeleteTags(Digikam::TagModificationHelper* helper, QList< TAlbum* > tags) { QAction* const deleteTagsAction = new QAction(QIcon::fromTheme(QLatin1String("user-trash")), i18n("Delete Tags"), this); addAction(deleteTagsAction); helper->bindMultipleTags(deleteTagsAction, tags); connect(deleteTagsAction, SIGNAL(triggered()), helper, SLOT(slotMultipleTagDel())); } void ContextMenuHelper::addActionTagToFaceTag(TagModificationHelper* helper, TAlbum* tag) { QAction* const tagToFaceTagAction = new QAction(QIcon::fromTheme(QLatin1String("tag-properties")), i18n("Mark As Face Tag"), this); addAction(tagToFaceTagAction); helper->bindTag(tagToFaceTagAction, tag); connect(tagToFaceTagAction, SIGNAL(triggered()), helper, SLOT(slotTagToFaceTag())); } void ContextMenuHelper::addActionTagsToFaceTags(TagModificationHelper* helper, QList< TAlbum* > tags) { QAction* const tagToFaceTagsAction = new QAction(QIcon::fromTheme(QLatin1String("tag-properties")), i18n("Mark As Face Tags"), this); addAction(tagToFaceTagsAction); helper->bindMultipleTags(tagToFaceTagsAction, tags); connect(tagToFaceTagsAction, SIGNAL(triggered()), helper, SLOT(slotMultipleTagsToFaceTags())); } void ContextMenuHelper::addActionEditTag(TagModificationHelper* helper, TAlbum* tag) { QAction* const editTagAction = new QAction(QIcon::fromTheme(QLatin1String("tag-properties")), i18nc("Edit Tag Properties", "Properties..."), this); addAction(editTagAction); helper->bindTag(editTagAction, tag); connect(editTagAction, SIGNAL(triggered()), helper, SLOT(slotTagEdit())); } void ContextMenuHelper::addActionDeleteFaceTag(TagModificationHelper* helper, TAlbum* tag) { QAction* const deleteFaceTagAction = new QAction(QIcon::fromTheme(QLatin1String("user-trash")), i18n("Remove Face Tag"), this); deleteFaceTagAction->setWhatsThis(i18n("Removes the face property from the selected tag and the face region from the contained images. Can also untag the images if wished.")); addAction(deleteFaceTagAction); helper->bindTag(deleteFaceTagAction, tag); connect(deleteFaceTagAction, SIGNAL(triggered()), helper, SLOT(slotFaceTagDelete())); } void ContextMenuHelper::addActionDeleteFaceTags(TagModificationHelper* helper, QList< TAlbum* > tags) { QAction* const deleteFaceTagsAction = new QAction(QIcon::fromTheme(QLatin1String("user-trash")), i18n("Remove Face Tags"), this); deleteFaceTagsAction->setWhatsThis(i18n("Removes the face property from the selected tags and the face region from the contained images. Can also untag the images if wished.")); addAction(deleteFaceTagsAction); helper->bindMultipleTags(deleteFaceTagsAction, tags); connect(deleteFaceTagsAction, SIGNAL(triggered()), helper, SLOT(slotMultipleFaceTagDel())); } void ContextMenuHelper::addActionNewAlbum(AlbumModificationHelper* helper, PAlbum* parentAlbum) { QAction* const action = d->copyFromMainCollection(QLatin1String("album_new")); addAction(action); helper->bindAlbum(action, parentAlbum); connect(action, SIGNAL(triggered()), helper, SLOT(slotAlbumNew())); } void ContextMenuHelper::addActionDeleteAlbum(AlbumModificationHelper* helper, PAlbum* album) { QAction* const action = d->copyFromMainCollection(QLatin1String("album_delete")); addAction(action, !(album->isRoot() || album->isAlbumRoot())); helper->bindAlbum(action, album); connect(action, SIGNAL(triggered()), helper, SLOT(slotAlbumDelete())); } void ContextMenuHelper::addActionEditAlbum(AlbumModificationHelper* helper, PAlbum* album) { QAction* const action = d->copyFromMainCollection(QLatin1String("album_propsEdit")); addAction(action, !album->isRoot()); helper->bindAlbum(action, album); connect(action, SIGNAL(triggered()), helper, SLOT(slotAlbumEdit())); } void ContextMenuHelper::addActionRenameAlbum(AlbumModificationHelper* helper, PAlbum* album) { QAction* const action = d->copyFromMainCollection(QLatin1String("album_rename")); addAction(action, !(album->isRoot() || album->isAlbumRoot())); helper->bindAlbum(action, album); connect(action, SIGNAL(triggered()), helper, SLOT(slotAlbumRename())); } void ContextMenuHelper::addActionResetAlbumIcon(AlbumModificationHelper* helper, PAlbum* album) { QAction* const action = new QAction(QIcon::fromTheme(QLatin1String("view-refresh")), i18n("Reset Album Icon"), this); addAction(action, !album->isRoot()); helper->bindAlbum(action, album); connect(action, SIGNAL(triggered()), helper, SLOT(slotAlbumResetIcon())); } void ContextMenuHelper::addAssignTagsMenu(const imageIds &ids) { setSelectedIds(ids); QMenu* const assignTagsPopup = new TagsPopupMenu(ids, TagsPopupMenu::RECENTLYASSIGNED, d->parent); assignTagsPopup->menuAction()->setText(i18n("A&ssign Tag")); assignTagsPopup->menuAction()->setIcon(QIcon::fromTheme(QLatin1String("tag"))); d->parent->addMenu(assignTagsPopup); connect(assignTagsPopup, SIGNAL(signalTagActivated(int)), this, SIGNAL(signalAssignTag(int))); connect(assignTagsPopup, SIGNAL(signalPopupTagsView()), this, SIGNAL(signalPopupTagsView())); } void ContextMenuHelper::addRemoveTagsMenu(const imageIds &ids) { setSelectedIds(ids); QMenu* const removeTagsPopup = new TagsPopupMenu(ids, TagsPopupMenu::REMOVE, d->parent); removeTagsPopup->menuAction()->setText(i18n("R&emove Tag")); removeTagsPopup->menuAction()->setIcon(QIcon::fromTheme(QLatin1String("tag"))); d->parent->addMenu(removeTagsPopup); // Performance: Only check for tags if there are <250 images selected // Otherwise enable it regardless if there are tags or not if (ids.count() < 250) { QList tagIDs = CoreDbAccess().db()->getItemCommonTagIDs(ids); bool enable = false; foreach (int tag, tagIDs) { if (TagsCache::instance()->colorLabelForTag(tag) == -1 && TagsCache::instance()->pickLabelForTag(tag) == -1 && TagsCache::instance()->isInternalTag(tag) == false) { enable = true; break; } } removeTagsPopup->menuAction()->setEnabled(enable); } connect(removeTagsPopup, SIGNAL(signalTagActivated(int)), this, SIGNAL(signalRemoveTag(int))); } void ContextMenuHelper::addLabelsAction() { QMenu* const menuLabels = new QMenu(i18n("Assign Labe&ls"), d->parent); PickLabelMenuAction* const pmenu = new PickLabelMenuAction(d->parent); ColorLabelMenuAction* const cmenu = new ColorLabelMenuAction(d->parent); RatingMenuAction* const rmenu = new RatingMenuAction(d->parent); menuLabels->addAction(pmenu->menuAction()); menuLabels->addAction(cmenu->menuAction()); menuLabels->addAction(rmenu->menuAction()); addSubMenu(menuLabels); connect(pmenu, SIGNAL(signalPickLabelChanged(int)), this, SIGNAL(signalAssignPickLabel(int))); connect(cmenu, SIGNAL(signalColorLabelChanged(int)), this, SIGNAL(signalAssignColorLabel(int))); connect(rmenu, SIGNAL(signalRatingChanged(int)), this, SIGNAL(signalAssignRating(int))); } void ContextMenuHelper::addCreateTagFromAddressbookMenu() { #ifdef HAVE_AKONADICONTACT AkonadiIface* const abc = new AkonadiIface(d->parent); connect(abc, SIGNAL(signalContactTriggered(QString)), this, SIGNAL(signalAddNewTagFromABCMenu(QString))); // AkonadiIface instance will be deleted with d->parent. #endif } void ContextMenuHelper::slotDeselectAllAlbumItems() { QAction* const selectNoneAction = d->stdActionCollection->action(QLatin1String("selectNone")); QTimer::singleShot(75, selectNoneAction, SIGNAL(triggered())); } void ContextMenuHelper::addImportMenu() { QMenu* const menuImport = new QMenu(i18n("Import"), d->parent); QList importActions = DigikamApp::instance()->importActions(); if (!importActions.isEmpty()) { menuImport->addActions(importActions); } else { QAction* const notools = new QAction(i18n("No import tool available"), this); notools->setEnabled(false); menuImport->addAction(notools); } d->parent->addMenu(menuImport); } void ContextMenuHelper::addExportMenu() { QMenu* const menuExport = new QMenu(i18n("Export"), d->parent); QList exportActions = DigikamApp::instance()->exportActions(); #if 0 QAction* selectAllAction = 0; selectAllAction = d->stdActionCollection->action("selectAll"); #endif if (!exportActions.isEmpty()) { menuExport->addActions(exportActions); } else { QAction* const notools = new QAction(i18n("No export tool available"), this); notools->setEnabled(false); menuExport->addAction(notools); } d->parent->addMenu(menuExport); } void ContextMenuHelper::addAlbumActions() { QList albumActions; if (!albumActions.isEmpty()) { d->parent->addActions(albumActions); } } void ContextMenuHelper::addGotoMenu(const imageIds &ids) { if (d->gotoAlbumAction && d->gotoDateAction) { return; } setSelectedIds(ids); // the currently selected image is always the first item ImageInfo item; if (!d->selectedIds.isEmpty()) { item = ImageInfo(d->selectedIds.first()); } if (item.isNull()) { return; } // when more then one item is selected, don't add the menu if (d->selectedIds.count() > 1) { return; } d->gotoAlbumAction = new QAction(QIcon::fromTheme(QLatin1String("folder-pictures")), i18n("Album"), this); d->gotoDateAction = new QAction(QIcon::fromTheme(QLatin1String("view-calendar")), i18n("Date"), this); QMenu* const gotoMenu = new QMenu(d->parent); gotoMenu->addAction(d->gotoAlbumAction); gotoMenu->addAction(d->gotoDateAction); TagsPopupMenu* const gotoTagsPopup = new TagsPopupMenu(d->selectedIds, TagsPopupMenu::DISPLAY, gotoMenu); QAction* const gotoTag = gotoMenu->addMenu(gotoTagsPopup); gotoTag->setIcon(QIcon::fromTheme(QLatin1String("tag"))); gotoTag->setText(i18n("Tag")); // Disable the goto Tag popup menu, if there are no tags at all. if (!CoreDbAccess().db()->hasTags(d->selectedIds)) { gotoTag->setEnabled(false); } /** * TODO:tags to be ported to multiple selection */ QList albumList = AlbumManager::instance()->currentAlbums(); Album* currentAlbum = 0; if(!albumList.isEmpty()) { currentAlbum = albumList.first(); } else { return; } if (currentAlbum->type() == Album::PHYSICAL) { // If the currently selected album is the same as album to // which the image belongs, then disable the "Go To" Album. // (Note that in recursive album view these can be different). if (item.albumId() == currentAlbum->id()) { d->gotoAlbumAction->setEnabled(false); } } else if (currentAlbum->type() == Album::DATE) { d->gotoDateAction->setEnabled(false); } QAction* const gotoMenuAction = gotoMenu->menuAction(); gotoMenuAction->setIcon(QIcon::fromTheme(QLatin1String("go-jump"))); gotoMenuAction->setText(i18n("Go To")); connect(gotoTagsPopup, SIGNAL(signalTagActivated(int)), this, SIGNAL(signalGotoTag(int))); addAction(gotoMenuAction); } void ContextMenuHelper::addQueueManagerMenu() { QMenu* const bqmMenu = new QMenu(i18n("Batch Queue Manager"), d->parent); bqmMenu->menuAction()->setIcon(QIcon::fromTheme(QLatin1String("run-build"))); bqmMenu->addAction(d->stdActionCollection->action(QLatin1String("image_add_to_current_queue"))); bqmMenu->addAction(d->stdActionCollection->action(QLatin1String("image_add_to_new_queue"))); // if queue list is empty, do not display the queue submenu if (QueueMgrWindow::queueManagerWindowCreated() && !QueueMgrWindow::queueManagerWindow()->queuesMap().isEmpty()) { QueueMgrWindow* const qmw = QueueMgrWindow::queueManagerWindow(); QMenu* const queueMenu = new QMenu(i18n("Add to Existing Queue"), bqmMenu); // queueActions is used by the exec() method to emit an appropriate signal. // Reset the map before filling in the actions. if (!d->queueActions.isEmpty()) { d->queueActions.clear(); } QList queueList; // get queue list from BQM window, do not access it directly, it might crash // when the list is changed QMap qmwMap = qmw->queuesMap(); for (QMap::const_iterator it = qmwMap.constBegin(); it != qmwMap.constEnd(); ++it) { QAction* const action = new QAction(it.value(), this); queueList << action; d->queueActions[it.key()] = action; } queueMenu->addActions(queueList); bqmMenu->addMenu(queueMenu); } d->parent->addMenu(bqmMenu); // NOTE: see bug #252130 : we need to disable new items to add on BQM is this one is running. bqmMenu->setDisabled(QueueMgrWindow::queueManagerWindow()->isBusy()); } void ContextMenuHelper::setAlbumModel(AbstractCheckableAlbumModel* model) { d->albumModel = model; } void ContextMenuHelper::addAlbumCheckUncheckActions(Album* album) { bool enabled = false; QString allString = i18n("All Albums"); QVariant albumData; if (album) { enabled = true; albumData = QVariant::fromValue(AlbumPointer<>(album)); if (album->type() == Album::TAG) allString = i18n("All Tags"); } QMenu* const selectTagsMenu = new QMenu(i18nc("select tags menu", "Select")); addSubMenu(selectTagsMenu); selectTagsMenu->addAction(allString, d->albumModel, SLOT(checkAllAlbums())); selectTagsMenu->addSeparator(); QAction* const selectChildrenAction = selectTagsMenu->addAction(i18n("Children"), this, SLOT(slotSelectChildren())); QAction* const selectParentsAction = selectTagsMenu->addAction(i18n("Parents"), this, SLOT(slotSelectParents())); selectChildrenAction->setData(albumData); selectParentsAction->setData(albumData); QMenu* const deselectTagsMenu = new QMenu(i18nc("deselect tags menu", "Deselect")); addSubMenu(deselectTagsMenu); deselectTagsMenu->addAction(allString, d->albumModel, SLOT(resetAllCheckedAlbums())); deselectTagsMenu->addSeparator(); QAction* const deselectChildrenAction = deselectTagsMenu->addAction(i18n("Children"), this, SLOT(slotDeselectChildren())); QAction* const deselectParentsAction = deselectTagsMenu->addAction(i18n("Parents"), this, SLOT(slotDeselectParents())); deselectChildrenAction->setData(albumData); deselectParentsAction->setData(albumData); d->parent->addAction(i18n("Invert Selection"), d->albumModel, SLOT(invertCheckedAlbums())); selectChildrenAction->setEnabled(enabled); selectParentsAction->setEnabled(enabled); deselectChildrenAction->setEnabled(enabled); deselectParentsAction->setEnabled(enabled); } void ContextMenuHelper::slotSelectChildren() { if (!d->albumModel) { return; } d->albumModel->checkAllAlbums(d->indexForAlbumFromAction(sender())); } void ContextMenuHelper::slotDeselectChildren() { if (!d->albumModel) { return; } d->albumModel->resetCheckedAlbums(d->indexForAlbumFromAction(sender())); } void ContextMenuHelper::slotSelectParents() { if (!d->albumModel) { return; } d->albumModel->checkAllParentAlbums(d->indexForAlbumFromAction(sender())); } void ContextMenuHelper::slotDeselectParents() { if (!d->albumModel) { return; } d->albumModel->resetCheckedParentAlbums(d->indexForAlbumFromAction(sender())); } void ContextMenuHelper::addGroupMenu(const imageIds &ids, const QList& extraMenuItems) { QList actions = groupMenuActions(ids); if (actions.isEmpty() && extraMenuItems.isEmpty()) { return; } if (!extraMenuItems.isEmpty()) { if (!actions.isEmpty()) { QAction* separator = new QAction(this); separator->setSeparator(true); actions << separator; } actions << extraMenuItems; } QMenu* const menu = new QMenu(i18n("Group")); foreach(QAction* const action, actions) { menu->addAction(action); } d->parent->addMenu(menu); } void ContextMenuHelper::addGroupActions(const imageIds &ids) { foreach(QAction* const action, groupMenuActions(ids)) { d->parent->addAction(action); } } void ContextMenuHelper::setImageFilterModel(ImageFilterModel* model) { d->imageFilterModel = model; } QList ContextMenuHelper::groupMenuActions(const imageIds &ids) { setSelectedIds(ids); QList actions; if (ids.isEmpty()) { if (d->imageFilterModel) { if (!d->imageFilterModel->isAllGroupsOpen()) { QAction* const openAction = new QAction(i18nc("@action:inmenu", "Open All Groups"), this); connect(openAction, SIGNAL(triggered()), this, SLOT(slotOpenGroups())); actions << openAction; } else { QAction* const closeAction = new QAction(i18nc("@action:inmenu", "Close All Groups"), this); connect(closeAction, SIGNAL(triggered()), this, SLOT(slotCloseGroups())); actions << closeAction; } } return actions; } ImageInfo info(ids.first()); if (ids.size() == 1) { if (info.hasGroupedImages()) { if (d->imageFilterModel) { if (!d->imageFilterModel->isGroupOpen(info.id())) { QAction* const action = new QAction(i18nc("@action:inmenu", "Show Grouped Images"), this); connect(action, SIGNAL(triggered()), this, SLOT(slotOpenGroups())); actions << action; } else { QAction* const action = new QAction(i18nc("@action:inmenu", "Hide Grouped Images"), this); connect(action, SIGNAL(triggered()), this, SLOT(slotCloseGroups())); actions << action; } } QAction* const separator = new QAction(this); separator->setSeparator(true); actions << separator; QAction* const clearAction = new QAction(i18nc("@action:inmenu", "Ungroup"), this); connect(clearAction, SIGNAL(triggered()), this, SIGNAL(signalUngroup())); actions << clearAction; } else if (info.isGrouped()) { QAction* const action = new QAction(i18nc("@action:inmenu", "Remove From Group"), this); connect(action, SIGNAL(triggered()), this, SIGNAL(signalRemoveFromGroup())); actions << action; // TODO: set as group leader / pick image } } else { QAction* const closeAction = new QAction(i18nc("@action:inmenu", "Group Selected Here"), this); connect(closeAction, SIGNAL(triggered()), this, SIGNAL(signalCreateGroup())); actions << closeAction; QAction* const closeActionDate = new QAction(i18nc("@action:inmenu", "Group Selected By Time"), this); connect(closeActionDate, SIGNAL(triggered()), this, SIGNAL(signalCreateGroupByTime())); actions << closeActionDate; QAction* const closeActionType = new QAction(i18nc("@action:inmenu", "Group Selected By Filename"), this); connect(closeActionType, SIGNAL(triggered()), this, SIGNAL(signalCreateGroupByFilename())); actions << closeActionType; QAction* const separator = new QAction(this); separator->setSeparator(true); actions << separator; if (d->imageFilterModel) { QAction* const openAction = new QAction(i18nc("@action:inmenu", "Show Grouped Images"), this); connect(openAction, SIGNAL(triggered()), this, SLOT(slotOpenGroups())); actions << openAction; QAction* const closeAction = new QAction(i18nc("@action:inmenu", "Hide Grouped Images"), this); connect(closeAction, SIGNAL(triggered()), this, SLOT(slotCloseGroups())); actions << closeAction; QAction* const separator2 = new QAction(this); separator2->setSeparator(true); actions << separator2; } QAction* const removeAction = new QAction(i18nc("@action:inmenu", "Remove Selected From Groups"), this); connect(removeAction, SIGNAL(triggered()), this, SIGNAL(signalRemoveFromGroup())); actions << removeAction; QAction* const clearAction = new QAction(i18nc("@action:inmenu", "Ungroup Selected"), this); connect(clearAction, SIGNAL(triggered()), this, SIGNAL(signalUngroup())); actions << clearAction; } return actions; } void ContextMenuHelper::setGroupsOpen(bool open) { if (!d->imageFilterModel || d->selectedIds.isEmpty()) { return; } GroupImageFilterSettings settings = d->imageFilterModel->groupImageFilterSettings(); foreach(const qlonglong& id, d->selectedIds) { ImageInfo info(id); if (info.hasGroupedImages()) { settings.setOpen(id, open); } } d->imageFilterModel->setGroupImageFilterSettings(settings); } void ContextMenuHelper::slotOpenGroups() { setGroupsOpen(true); } void ContextMenuHelper::slotCloseGroups() { setGroupsOpen(false); } void ContextMenuHelper::slotOpenAllGroups() { if (!d->imageFilterModel) { return; } d->imageFilterModel->setAllGroupsOpen(true); } void ContextMenuHelper::slotCloseAllGroups() { if (!d->imageFilterModel) { return; } d->imageFilterModel->setAllGroupsOpen(false); } void ContextMenuHelper::addStandardActionCut(QObject* recv, const char* slot) { QAction* const cut = DXmlGuiWindow::buildStdAction(StdCutAction, recv, slot, d->parent); addAction(cut); } void ContextMenuHelper::addStandardActionCopy(QObject* recv, const char* slot) { QAction* const copy = DXmlGuiWindow::buildStdAction(StdCopyAction, recv, slot, d->parent); addAction(copy); } void ContextMenuHelper::addStandardActionPaste(QObject* recv, const char* slot) { QAction* const paste = DXmlGuiWindow::buildStdAction(StdPasteAction, recv, slot, d->parent); const QMimeData* const data = qApp->clipboard()->mimeData(QClipboard::Clipboard); if (!data || !data->hasUrls()) { paste->setEnabled(false); } addAction(paste, true); } void ContextMenuHelper::addStandardActionItemDelete(QObject* recv, const char* slot, int quantity) { QAction* const trashAction = new QAction(QIcon::fromTheme(QLatin1String("user-trash")), i18ncp("@action:inmenu Pluralized", "Move to Trash", "Move %1 Files to Trash", quantity), d->parent); connect(trashAction, SIGNAL(triggered()), recv, slot); addAction(trashAction); } QAction* ContextMenuHelper::exec(const QPoint& pos, QAction* at) { QAction* const choice = d->parent->exec(pos, at); if (choice) { if (d->selectedIds.count() == 1) { ImageInfo selectedItem(d->selectedIds.first()); if (choice == d->gotoAlbumAction) { emit signalGotoAlbum(selectedItem); } else if (choice == d->gotoDateAction) { emit signalGotoDate(selectedItem); } else if (choice == d->setThumbnailAction) { emit signalSetThumbnail(selectedItem); } } // check if a BQM action has been triggered for (QMap::const_iterator it = d->queueActions.constBegin(); it != d->queueActions.constEnd(); ++it) { if (choice == it.value()) { emit signalAddToExistingQueue(it.key()); return choice; } } } return choice; } void ContextMenuHelper::setSelectedIds(const imageIds &ids) { if (d->selectedIds.isEmpty()) { d->selectedIds = ids; } } void ContextMenuHelper::setSelectedItems(const QList& urls) { if (d->selectedItems.isEmpty()) { d->selectedItems = urls; } } } // namespace Digikam diff --git a/core/app/views/digikamview.cpp b/core/app/views/digikamview.cpp index 64da7f5a84..3909547066 100644 --- a/core/app/views/digikamview.cpp +++ b/core/app/views/digikamview.cpp @@ -1,2785 +1,2786 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2002-16-10 * Description : implementation of album view interface. * * Copyright (C) 2002-2005 by Renchi Raju * Copyright (C) 2002-2018 by Gilles Caulier * Copyright (C) 2009-2011 by Johannes Wienke * Copyright (C) 2010-2011 by Andi Clemens * Copyright (C) 2011-2013 by Michael G. Hansen * Copyright (C) 2014-2015 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 "digikamview.h" // Qt includes #include #include #include #include // KDE includes #include // Local includes #include "albumhistory.h" #include "albumlabelstreeview.h" +#include "albumpointer.h" #include "coredbsearchxml.h" #include "digikam_config.h" #include "digikam_debug.h" #include "digikam_globals.h" #include "digikamapp.h" #include "digikamimageview.h" #include "dmessagebox.h" #include "dzoombar.h" #include "dtrashitemmodel.h" #include "facescansettings.h" #include "facesdetector.h" #include "fileactionmngr.h" #include "fileactionprogress.h" #include "filtersidebarwidget.h" #include "filterstatusbar.h" #include "imagealbummodel.h" #include "imagedescedittab.h" #include "imagepreviewview.h" #include "imagepropertiessidebardb.h" #include "imagepropertiesversionstab.h" #include "imagethumbnailbar.h" #include "imageviewutilities.h" #include "leftsidebarwidgets.h" #include "loadingcacheinterface.h" #include "metadatahub.h" #include "metadatasettings.h" #include "metadatasynchronizer.h" #include "newitemsfinder.h" #include "presentationmngr.h" #include "queuemgrwindow.h" #include "scancontroller.h" #include "setup.h" #include "sidebar.h" #include "slideshow.h" #include "slideshowbuilder.h" #include "statusprogressbar.h" #include "tableview.h" #include "tagmodificationhelper.h" #include "tagsactionmngr.h" #include "tagscache.h" #include "tagsmanager.h" #include "thumbsgenerator.h" #include "trashview.h" #include "versionmanagersettings.h" #include "contextmenuhelper.h" #ifdef HAVE_MEDIAPLAYER # include "mediaplayerview.h" #endif //HAVE_MEDIAPLAYER #ifdef HAVE_MARBLE # include "mapwidgetview.h" #endif // HAVE_MARBLE namespace Digikam { class Q_DECL_HIDDEN DigikamView::Private { public: explicit Private() : needDispatchSelection(false), useAlbumHistory(false), initialAlbumID(0), thumbSize(ThumbnailSize::Medium), dockArea(0), splitter(0), selectionTimer(0), thumbSizeTimer(0), albumFolderSideBar(0), tagViewSideBar(0), labelsSideBar(0), dateViewSideBar(0), timelineSideBar(0), searchSideBar(0), fuzzySearchSideBar(0), #ifdef HAVE_MARBLE gpsSearchSideBar(0), mapView(0), #endif // HAVE_MARBLE peopleSideBar(0), parent(0), iconView(0), tableView(0), trashView(0), utilities(0), albumManager(0), albumHistory(0), stackedview(0), lastViewMode(StackedView::IconViewMode), albumModificationHelper(0), tagModificationHelper(0), searchModificationHelper(0), leftSideBar(0), rightSideBar(0), filterWidget(0), optionAlbumViewPrefix(QLatin1String("AlbumView")), modelCollection(0), labelsSearchHandler(0) { } QString userPresentableAlbumTitle(const QString& album) const; void addPageUpDownActions(DigikamView* const q, QWidget* const w); public: bool needDispatchSelection; bool useAlbumHistory; int initialAlbumID; int thumbSize; QMainWindow* dockArea; SidebarSplitter* splitter; QTimer* selectionTimer; QTimer* thumbSizeTimer; // left side bar AlbumFolderViewSideBarWidget* albumFolderSideBar; TagViewSideBarWidget* tagViewSideBar; LabelsSideBarWidget* labelsSideBar; DateFolderViewSideBarWidget* dateViewSideBar; TimelineSideBarWidget* timelineSideBar; SearchSideBarWidget* searchSideBar; FuzzySearchSideBarWidget* fuzzySearchSideBar; #ifdef HAVE_MARBLE GPSSearchSideBarWidget* gpsSearchSideBar; MapWidgetView* mapView; #endif // HAVE_MARBLE PeopleSideBarWidget* peopleSideBar; DigikamApp* parent; DigikamImageView* iconView; TableView* tableView; TrashView* trashView; ImageViewUtilities* utilities; AlbumManager* albumManager; AlbumHistory* albumHistory; StackedView* stackedview; StackedView::StackedViewMode lastViewMode; AlbumModificationHelper* albumModificationHelper; TagModificationHelper* tagModificationHelper; SearchModificationHelper* searchModificationHelper; Sidebar* leftSideBar; ImagePropertiesSideBarDB* rightSideBar; FilterSideBarWidget* filterWidget; QString optionAlbumViewPrefix; QList leftSideBarWidgets; DigikamModelCollection* modelCollection; AlbumLabelsSearchHandler* labelsSearchHandler; }; QString DigikamView::Private::userPresentableAlbumTitle(const QString& title) const { if (title == SAlbum::getTemporaryHaarTitle(DatabaseSearch::HaarSketchSearch)) { return i18n("Fuzzy Sketch Search"); } else if (title == SAlbum::getTemporaryHaarTitle(DatabaseSearch::HaarImageSearch)) { return i18n("Fuzzy Image Search"); } else if (title == SAlbum::getTemporaryTitle(DatabaseSearch::MapSearch)) { return i18n("Map Search"); } else if (title == SAlbum::getTemporaryTitle(DatabaseSearch::AdvancedSearch) || title == SAlbum::getTemporaryTitle(DatabaseSearch::KeywordSearch)) { return i18n("Last Search"); } else if (title == SAlbum::getTemporaryTitle(DatabaseSearch::TimeLineSearch)) { return i18n("Timeline"); } return title; } void DigikamView::Private::addPageUpDownActions(DigikamView* const q, QWidget* const w) { defineShortcut(w, Qt::Key_PageDown, q, SLOT(slotNextItem())); defineShortcut(w, Qt::Key_Down, q, SLOT(slotNextItem())); defineShortcut(w, Qt::Key_Right, q, SLOT(slotNextItem())); defineShortcut(w, Qt::Key_PageUp, q, SLOT(slotPrevItem())); defineShortcut(w, Qt::Key_Up, q, SLOT(slotPrevItem())); defineShortcut(w, Qt::Key_Left, q, SLOT(slotPrevItem())); } // ------------------------------------------------------------------------------------------- DigikamView::DigikamView(QWidget* const parent, DigikamModelCollection* const modelCollection) : DHBox(parent), d(new Private) { qRegisterMetaType("SlideShowSettings"); d->parent = static_cast(parent); d->modelCollection = modelCollection; d->albumManager = AlbumManager::instance(); d->albumModificationHelper = new AlbumModificationHelper(this, this); d->tagModificationHelper = new TagModificationHelper(this, this); d->searchModificationHelper = new SearchModificationHelper(this, this); d->splitter = new SidebarSplitter; d->splitter->setFrameStyle( QFrame::NoFrame ); d->splitter->setFrameShadow( QFrame::Plain ); d->splitter->setFrameShape( QFrame::NoFrame ); d->splitter->setOpaqueResize(false); d->leftSideBar = new Sidebar(this, d->splitter, Qt::LeftEdge); d->leftSideBar->setObjectName(QLatin1String("Digikam Left Sidebar")); d->splitter->setParent(this); // The dock area where the thumbnail bar is allowed to go. d->dockArea = new QMainWindow(this, Qt::Widget); d->splitter->addWidget(d->dockArea); d->stackedview = new StackedView(d->dockArea); d->dockArea->setCentralWidget(d->stackedview); d->stackedview->setDockArea(d->dockArea); d->iconView = d->stackedview->imageIconView(); #ifdef HAVE_MARBLE d->mapView = d->stackedview->mapWidgetView(); #endif // HAVE_MARBLE d->tableView = d->stackedview->tableView(); d->trashView = d->stackedview->trashView(); d->utilities = d->iconView->utilities(); d->addPageUpDownActions(this, d->stackedview->imagePreviewView()); d->addPageUpDownActions(this, d->stackedview->thumbBar()); #ifdef HAVE_MEDIAPLAYER d->addPageUpDownActions(this, d->stackedview->mediaPlayerView()); #endif //HAVE_MEDIAPLAYER d->rightSideBar = new ImagePropertiesSideBarDB(this, d->splitter, Qt::RightEdge, true); d->rightSideBar->setObjectName(QLatin1String("Digikam Right Sidebar")); // album folder view d->albumFolderSideBar = new AlbumFolderViewSideBarWidget(d->leftSideBar, d->modelCollection->getAlbumModel(), d->albumModificationHelper); d->leftSideBarWidgets << d->albumFolderSideBar; connect(d->albumFolderSideBar, SIGNAL(signalFindDuplicates(PAlbum*)), this, SLOT(slotNewDuplicatesSearch(PAlbum*))); // Tags sidebar tab contents. d->tagViewSideBar = new TagViewSideBarWidget(d->leftSideBar, d->modelCollection->getTagModel()); d->leftSideBarWidgets << d->tagViewSideBar; connect(d->tagViewSideBar, SIGNAL(signalFindDuplicates(QList)), this, SLOT(slotNewDuplicatesSearch(QList))); // Labels sidebar d->labelsSideBar = new LabelsSideBarWidget(d->leftSideBar); d->leftSideBarWidgets << d->labelsSideBar; d->labelsSearchHandler = new AlbumLabelsSearchHandler(d->labelsSideBar->labelsTree()); // date view d->dateViewSideBar = new DateFolderViewSideBarWidget(d->leftSideBar, d->modelCollection->getDateAlbumModel(), d->iconView->imageAlbumFilterModel()); d->leftSideBarWidgets << d->dateViewSideBar; // timeline side bar d->timelineSideBar = new TimelineSideBarWidget(d->leftSideBar, d->modelCollection->getSearchModel(), d->searchModificationHelper); d->leftSideBarWidgets << d->timelineSideBar; // Search sidebar tab contents. d->searchSideBar = new SearchSideBarWidget(d->leftSideBar, d->modelCollection->getSearchModel(), d->searchModificationHelper); d->leftSideBarWidgets << d->searchSideBar; // Fuzzy search d->fuzzySearchSideBar = new FuzzySearchSideBarWidget(d->leftSideBar, d->modelCollection->getSearchModel(), d->searchModificationHelper); d->leftSideBarWidgets << d->fuzzySearchSideBar; connect(d->fuzzySearchSideBar,SIGNAL(signalActive(bool)), this, SIGNAL(signalFuzzySidebarActive(bool))); #ifdef HAVE_MARBLE d->gpsSearchSideBar = new GPSSearchSideBarWidget(d->leftSideBar, d->modelCollection->getSearchModel(), d->searchModificationHelper, d->iconView->imageFilterModel(), d->iconView->getSelectionModel()); d->leftSideBarWidgets << d->gpsSearchSideBar; #endif // HAVE_MARBLE // People Sidebar d->peopleSideBar = new PeopleSideBarWidget(d->leftSideBar, d->modelCollection->getTagFacesModel(), d->searchModificationHelper); connect(d->peopleSideBar, SIGNAL(requestFaceMode(bool)), d->iconView, SLOT(setFaceMode(bool))); connect(d->peopleSideBar, SIGNAL(signalFindDuplicates(QList)), this, SLOT(slotNewDuplicatesSearch(QList))); d->leftSideBarWidgets << d->peopleSideBar; foreach(SidebarWidget* const leftWidget, d->leftSideBarWidgets) { d->leftSideBar->appendTab(leftWidget, leftWidget->getIcon(), leftWidget->getCaption()); connect(leftWidget, SIGNAL(requestActiveTab(SidebarWidget*)), this, SLOT(slotLeftSideBarActivate(SidebarWidget*))); } // add only page up and down to work correctly with QCompleter defineShortcut(d->rightSideBar->imageDescEditTab(), Qt::Key_PageDown, this, SLOT(slotNextItem())); defineShortcut(d->rightSideBar->imageDescEditTab(), Qt::Key_PageUp, this, SLOT(slotPrevItem())); // Tags Filter sidebar tab contents. d->filterWidget = new FilterSideBarWidget(d->rightSideBar, d->modelCollection->getTagFilterModel()); d->rightSideBar->appendTab(d->filterWidget, QIcon::fromTheme(QLatin1String("view-filter")), i18n("Filters")); // Versions sidebar overlays d->rightSideBar->getFiltersHistoryTab()->addOpenAlbumAction(d->iconView->imageModel()); d->rightSideBar->getFiltersHistoryTab()->addShowHideOverlay(); d->selectionTimer = new QTimer(this); d->selectionTimer->setSingleShot(true); d->selectionTimer->setInterval(75); d->thumbSizeTimer = new QTimer(this); d->thumbSizeTimer->setSingleShot(true); d->thumbSizeTimer->setInterval(300); d->albumHistory = new AlbumHistory(); slotSidebarTabTitleStyleChanged(); setupConnections(); connect(d->rightSideBar->imageDescEditTab()->getNewTagEdit(), SIGNAL(taggingActionFinished()), this, SLOT(slotFocusAndNextImage())); connect(d->rightSideBar, SIGNAL(signalSetupMetadataFilters(int)), this, SLOT(slotSetupMetadataFilters(int))); } DigikamView::~DigikamView() { saveViewState(); delete d->labelsSearchHandler; delete d->albumHistory; delete d; } void DigikamView::applySettings() { foreach(SidebarWidget* const sidebarWidget, d->leftSideBarWidgets) { sidebarWidget->applySettings(); } d->iconView->imageFilterModel()->setVersionImageFilterSettings(VersionImageFilterSettings(ApplicationSettings::instance()->getVersionManagerSettings())); refreshView(); } void DigikamView::refreshView() { d->rightSideBar->refreshTagsView(); } void DigikamView::setupConnections() { // -- DigikamApp connections ---------------------------------- connect(d->parent, SIGNAL(signalEscapePressed()), this, SLOT(slotEscapePreview())); connect(d->parent, SIGNAL(signalEscapePressed()), d->stackedview, SLOT(slotEscapePreview())); connect(d->parent, SIGNAL(signalNextItem()), this, SLOT(slotNextItem())); connect(d->parent, SIGNAL(signalPrevItem()), this, SLOT(slotPrevItem())); connect(d->parent, SIGNAL(signalFirstItem()), this, SLOT(slotFirstItem())); connect(d->parent, SIGNAL(signalLastItem()), this, SLOT(slotLastItem())); connect(d->parent, SIGNAL(signalCutAlbumItemsSelection()), d->iconView, SLOT(cut())); connect(d->parent, SIGNAL(signalCopyAlbumItemsSelection()), d->iconView, SLOT(copy())); connect(d->parent, SIGNAL(signalPasteAlbumItemsSelection()), this, SLOT(slotImagePaste())); // -- AlbumManager connections -------------------------------- connect(d->albumManager, SIGNAL(signalAlbumCurrentChanged(QList)), this, SLOT(slotAlbumSelected(QList))); connect(d->albumManager, SIGNAL(signalAllAlbumsLoaded()), this, SLOT(slotAllAlbumsLoaded())); connect(d->albumManager, SIGNAL(signalAlbumsCleared()), this, SLOT(slotAlbumsCleared())); // -- IconView Connections ------------------------------------- connect(d->iconView->model(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(slotImageSelected())); connect(d->iconView->model(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(slotImageSelected())); connect(d->iconView->model(), SIGNAL(layoutChanged()), this, SLOT(slotImageSelected())); connect(d->iconView, SIGNAL(selectionChanged()), this, SLOT(slotImageSelected())); connect(d->iconView, SIGNAL(previewRequested(ImageInfo)), this, SLOT(slotTogglePreviewMode(ImageInfo))); connect(d->iconView, SIGNAL(fullscreenRequested(ImageInfo)), this, SLOT(slotSlideShowManualFrom(ImageInfo))); connect(d->iconView, SIGNAL(zoomOutStep()), this, SLOT(slotZoomOut())); connect(d->iconView, SIGNAL(zoomInStep()), this, SLOT(slotZoomIn())); connect(d->iconView, SIGNAL(signalShowContextMenu(QContextMenuEvent*,QList)), this, SLOT(slotShowContextMenu(QContextMenuEvent*,QList))); connect(d->iconView, SIGNAL(signalShowContextMenuOnInfo(QContextMenuEvent*,ImageInfo,QList,ImageFilterModel*)), this, SLOT(slotShowContextMenuOnInfo(QContextMenuEvent*,ImageInfo,QList,ImageFilterModel*))); connect(d->iconView, SIGNAL(signalShowGroupContextMenu(QContextMenuEvent*,QList,ImageFilterModel*)), this, SLOT(slotShowGroupContextMenu(QContextMenuEvent*,QList,ImageFilterModel*))); // -- TableView Connections ----------------------------------- connect(d->tableView, SIGNAL(signalPreviewRequested(ImageInfo)), this, SLOT(slotTogglePreviewMode(ImageInfo))); connect(d->tableView, SIGNAL(signalZoomOutStep()), this, SLOT(slotZoomOut())); connect(d->tableView, SIGNAL(signalZoomInStep()), this, SLOT(slotZoomIn())); connect(d->tableView, SIGNAL(signalShowContextMenu(QContextMenuEvent*,QList)), this, SLOT(slotShowContextMenu(QContextMenuEvent*,QList))); connect(d->tableView, SIGNAL(signalShowContextMenuOnInfo(QContextMenuEvent*,ImageInfo,QList,ImageFilterModel*)), this, SLOT(slotShowContextMenuOnInfo(QContextMenuEvent*,ImageInfo,QList,ImageFilterModel*))); // TableView::signalItemsChanged is emitted when something changes in the model that // DigikamView should care about, not only the selection. connect(d->tableView, SIGNAL(signalItemsChanged()), this, SLOT(slotImageSelected())); // -- Trash View Connections ---------------------------------- connect(d->trashView, SIGNAL(selectionChanged()), this, SLOT(slotImageSelected())); // -- Sidebar Connections ------------------------------------- connect(d->leftSideBar, SIGNAL(signalChangedTab(QWidget*)), this, SLOT(slotLeftSidebarChangedTab(QWidget*))); connect(d->rightSideBar, SIGNAL(signalFirstItem()), this, SLOT(slotFirstItem())); connect(d->rightSideBar, SIGNAL(signalNextItem()), this, SLOT(slotNextItem())); connect(d->rightSideBar, SIGNAL(signalPrevItem()), this, SLOT(slotPrevItem())); connect(d->rightSideBar, SIGNAL(signalLastItem()), this, SLOT(slotLastItem())); connect(this, SIGNAL(signalNoCurrentItem()), d->rightSideBar, SLOT(slotNoCurrentItem())); #ifdef HAVE_MARBLE connect(d->gpsSearchSideBar, SIGNAL(signalMapSoloItems(QList,QString)), d->iconView->imageFilterModel(), SLOT(setIdWhitelist(QList,QString))); #endif // HAVE_MARBLE // -- Filter Bars Connections --------------------------------- ImageAlbumFilterModel* const model = d->iconView->imageAlbumFilterModel(); connect(d->filterWidget, SIGNAL(signalTagFilterChanged(QList,QList,ImageFilterSettings::MatchingCondition,bool,QList,QList)), d->iconView->imageFilterModel(), SLOT(setTagFilter(QList,QList,ImageFilterSettings::MatchingCondition,bool,QList,QList))); connect(d->filterWidget, SIGNAL(signalRatingFilterChanged(int,ImageFilterSettings::RatingCondition,bool)), model, SLOT(setRatingFilter(int,ImageFilterSettings::RatingCondition,bool))); connect(d->filterWidget, SIGNAL(signalSearchTextFilterChanged(SearchTextFilterSettings)), model, SLOT(setTextFilter(SearchTextFilterSettings))); connect(model, SIGNAL(filterMatchesForText(bool)), d->filterWidget, SLOT(slotFilterMatchesForText(bool))); connect(d->filterWidget, SIGNAL(signalMimeTypeFilterChanged(int)), model, SLOT(setMimeTypeFilter(int))); connect(d->filterWidget, SIGNAL(signalGeolocationFilterChanged(ImageFilterSettings::GeolocationCondition)), model, SLOT(setGeolocationFilter(ImageFilterSettings::GeolocationCondition))); // -- Preview image widget Connections ------------------------ connect(d->stackedview, SIGNAL(signalNextItem()), this, SLOT(slotNextItem())); connect(d->stackedview, SIGNAL(signalPrevItem()), this, SLOT(slotPrevItem())); connect(d->stackedview, SIGNAL(signalDeleteItem()), this, SLOT(slotImageDelete())); connect(d->stackedview, SIGNAL(signalViewModeChanged()), this, SLOT(slotViewModeChanged())); connect(d->stackedview, SIGNAL(signalEscapePreview()), this, SLOT(slotEscapePreview())); connect(d->stackedview, SIGNAL(signalSlideShowCurrent()), this, SLOT(slotSlideShowManualFromCurrent())); connect(d->stackedview, SIGNAL(signalZoomFactorChanged(double)), this, SLOT(slotZoomFactorChanged(double))); connect(d->stackedview, SIGNAL(signalAddToExistingQueue(int)), this, SLOT(slotImageAddToExistingQueue(int))); connect(d->stackedview, SIGNAL(signalGotoAlbumAndItem(ImageInfo)), this, SLOT(slotGotoAlbumAndItem(ImageInfo))); connect(d->stackedview, SIGNAL(signalGotoDateAndItem(ImageInfo)), this, SLOT(slotGotoDateAndItem(ImageInfo))); connect(d->stackedview, SIGNAL(signalGotoTagAndItem(int)), this, SLOT(slotGotoTagAndItem(int))); connect(d->stackedview, SIGNAL(signalPopupTagsView()), d->rightSideBar, SLOT(slotPopupTagsView())); // -- FileActionMngr progress --------------- connect(FileActionMngr::instance(), SIGNAL(signalImageChangeFailed(QString,QStringList)), this, SLOT(slotImageChangeFailed(QString,QStringList))); // -- timers --------------- connect(d->selectionTimer, SIGNAL(timeout()), this, SLOT(slotDispatchImageSelected())); connect(d->thumbSizeTimer, SIGNAL(timeout()), this, SLOT(slotThumbSizeEffect()) ); // -- Album Settings ---------------- connect(ApplicationSettings::instance(), SIGNAL(setupChanged()), this, SLOT(slotSidebarTabTitleStyleChanged())); // -- Album History ----------------- connect(this, SIGNAL(signalAlbumSelected(Album*)), d->albumHistory, SLOT(slotAlbumSelected())); connect(this, SIGNAL(signalImageSelected(ImageInfoList,ImageInfoList)), d->albumHistory, SLOT(slotImageSelected(ImageInfoList))); connect(d->iconView, SIGNAL(currentChanged(ImageInfo)), d->albumHistory, SLOT(slotCurrentChange(ImageInfo))); connect(d->iconView->imageModel(), SIGNAL(imageInfosAdded(QList)), d->albumHistory, SLOT(slotAlbumCurrentChanged())); connect(d->albumHistory, SIGNAL(signalSetCurrent(qlonglong)), this, SLOT(slotSetCurrentWhenAvailable(qlonglong))); connect(d->albumHistory, SIGNAL(signalSetSelectedInfos(QList)), d->iconView, SLOT(setSelectedImageInfos(QList))); connect(d->albumManager, SIGNAL(signalAlbumDeleted(Album*)), d->albumHistory, SLOT(slotAlbumDeleted(Album*))); connect(d->albumManager, SIGNAL(signalAlbumsCleared()), d->albumHistory, SLOT(slotAlbumsCleared())); // -- Image versions ---------------- connect(d->rightSideBar->getFiltersHistoryTab(), SIGNAL(imageSelected(ImageInfo)), d->iconView, SLOT(hintAt(ImageInfo))); connect(d->rightSideBar->getFiltersHistoryTab(), SIGNAL(actionTriggered(ImageInfo)), this, SLOT(slotGotoAlbumAndItem(ImageInfo))); } void DigikamView::connectIconViewFilter(FilterStatusBar* const filterbar) { ImageAlbumFilterModel* const model = d->iconView->imageAlbumFilterModel(); connect(model, SIGNAL(filterMatches(bool)), filterbar, SLOT(slotFilterMatches(bool))); connect(model, SIGNAL(filterSettingsChanged(ImageFilterSettings)), filterbar, SLOT(slotFilterSettingsChanged(ImageFilterSettings))); connect(filterbar, SIGNAL(signalResetFilters()), d->filterWidget, SLOT(slotResetFilters())); connect(filterbar, SIGNAL(signalPopupFiltersView()), this, SLOT(slotPopupFiltersView())); } void DigikamView::slotPopupFiltersView() { d->rightSideBar->setActiveTab(d->filterWidget); d->filterWidget->setFocusToTextFilter(); } void DigikamView::loadViewState() { foreach(SidebarWidget* const widget, d->leftSideBarWidgets) { widget->loadState(); } d->filterWidget->loadState(); KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(QLatin1String("MainWindow")); // Restore the splitter d->splitter->restoreState(group); // Restore the thumbnail bar dock. QByteArray thumbbarState; thumbbarState = group.readEntry(QLatin1String("ThumbbarState"), thumbbarState); d->dockArea->restoreState(QByteArray::fromBase64(thumbbarState)); d->initialAlbumID = group.readEntry(QLatin1String("InitialAlbumID"), 0); #ifdef HAVE_MARBLE d->mapView->loadState(); #endif // HAVE_MARBLE d->tableView->loadState(); d->rightSideBar->loadState(); } void DigikamView::saveViewState() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group = config->group(QLatin1String("MainWindow")); foreach(SidebarWidget* const widget, d->leftSideBarWidgets) { widget->saveState(); } d->filterWidget->saveState(); // Save the splitter states. d->splitter->saveState(group); // Save the position and size of the thumbnail bar. The thumbnail bar dock // needs to be closed explicitly, because when it is floating and visible // (when the user is in image preview mode) when the layout is saved, it // also reappears when restoring the view, while it should always be hidden. d->stackedview->thumbBarDock()->close(); group.writeEntry(QLatin1String("ThumbbarState"), d->dockArea->saveState().toBase64()); QList albumList = AlbumManager::instance()->currentAlbums(); Album* album = 0; if(!albumList.isEmpty()) { album = albumList.first(); } if (album) { group.writeEntry(QLatin1String("InitialAlbumID"), album->globalID()); } else { group.writeEntry(QLatin1String("InitialAlbumID"), 0); } #ifdef HAVE_MARBLE d->mapView->saveState(); #endif // HAVE_MARBLE d->tableView->saveState(); d->rightSideBar->saveState(); } QList DigikamView::leftSidebarWidgets() const { return d->leftSideBarWidgets; } QList DigikamView::allUrls(bool grouping) const { /// @todo This functions seems not to be used anywhere right now return allInfo(grouping).toImageUrlList(); } QList DigikamView::selectedUrls(bool grouping) const { return selectedInfoList(false, grouping).toImageUrlList(); } QList DigikamView::selectedUrls(const ApplicationSettings::OperationType type) const { return selectedInfoList(type).toImageUrlList(); } void DigikamView::showSideBars() { d->leftSideBar->restore(); d->rightSideBar->restore(); } void DigikamView::hideSideBars() { d->leftSideBar->backup(); d->rightSideBar->backup(); } void DigikamView::toggleLeftSidebar() { d->leftSideBar->isExpanded() ? d->leftSideBar->shrink() : d->leftSideBar->expand(); } void DigikamView::toggleRightSidebar() { d->rightSideBar->isExpanded() ? d->rightSideBar->shrink() : d->rightSideBar->expand(); } void DigikamView::previousLeftSideBarTab() { d->leftSideBar->activePreviousTab(); } void DigikamView::nextLeftSideBarTab() { d->leftSideBar->activeNextTab(); } void DigikamView::previousRightSideBarTab() { d->rightSideBar->activePreviousTab(); } void DigikamView::nextRightSideBarTab() { d->rightSideBar->activeNextTab(); } void DigikamView::slotFirstItem() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->slotGoToRow(0, false); break; default: // all other views are tied to IconView's selection model d->iconView->toFirstIndex(); } } void DigikamView::slotPrevItem() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->slotGoToRow(-1, true); break; default: // all other views are tied to IconView's selection model d->iconView->toPreviousIndex(); } } void DigikamView::slotNextItem() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->slotGoToRow(1, true); break; default: // all other views are tied to IconView's selection model d->iconView->toNextIndex(); } } void DigikamView::slotLastItem() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->slotGoToRow(-1, false); break; default: // all other views are tied to IconView's selection model d->iconView->toLastIndex(); } } void DigikamView::slotSelectItemByUrl(const QUrl& url) { /// @todo This functions seems not to be used anywhere right now /// @todo Adapt to TableView d->iconView->toIndex(url); } void DigikamView::slotAllAlbumsLoaded() { disconnect(d->albumManager, SIGNAL(signalAllAlbumsLoaded()), this, SLOT(slotAllAlbumsLoaded())); loadViewState(); d->leftSideBar->loadState(); d->rightSideBar->loadState(); d->rightSideBar->populateTags(); // now that all albums have been loaded, activate the albumHistory d->useAlbumHistory = true; Album* const album = d->albumManager->findAlbum(d->initialAlbumID); d->albumManager->setCurrentAlbums(QList() << album); } void DigikamView::slotSortAlbums(int role) { ApplicationSettings* const settings = ApplicationSettings::instance(); if (!settings) { return; } settings->setAlbumSortRole((ApplicationSettings::AlbumSortRole) role); settings->saveSettings(); //A dummy way to force the tree view to resort if the album sort role changed PAlbum* const albumBeforeSorting = d->albumFolderSideBar->currentAlbum(); settings->setAlbumSortChanged(true); d->albumFolderSideBar->doSaveState(); d->albumFolderSideBar->doLoadState(); d->albumFolderSideBar->doSaveState(); d->albumFolderSideBar->doLoadState(); settings->setAlbumSortChanged(false); if (d->leftSideBar->getActiveTab() == d->albumFolderSideBar) { d->albumFolderSideBar->setCurrentAlbum(albumBeforeSorting); } } void DigikamView::slotNewAlbum() { // TODO use the selection model of the view instead d->albumModificationHelper->slotAlbumNew(d->albumFolderSideBar->currentAlbum()); } void DigikamView::slotDeleteAlbum() { d->albumModificationHelper->slotAlbumDelete(d->albumFolderSideBar->currentAlbum()); } void DigikamView::slotRenameAlbum() { d->albumModificationHelper->slotAlbumRename(d->albumFolderSideBar->currentAlbum()); } void DigikamView::slotNewTag() { QList talbums = AlbumManager::instance()->currentTAlbums(); if (!talbums.isEmpty()) d->tagModificationHelper->slotTagNew(talbums.first()); } void DigikamView::slotDeleteTag() { QList talbums = AlbumManager::instance()->currentTAlbums(); if (!talbums.isEmpty()) d->tagModificationHelper->slotTagDelete(talbums.first()); } void DigikamView::slotEditTag() { QList talbums = AlbumManager::instance()->currentTAlbums(); if (!talbums.isEmpty()) d->tagModificationHelper->slotTagEdit(talbums.first()); } void DigikamView::slotOpenTagsManager() { TagsManager* const tagMngr = TagsManager::instance(); tagMngr->show(); tagMngr->activateWindow(); tagMngr->raise(); } void DigikamView::slotAssignTag() { d->rightSideBar->setActiveTab(d->rightSideBar->imageDescEditTab()); d->rightSideBar->imageDescEditTab()->setFocusToNewTagEdit(); } void DigikamView::slotNewKeywordSearch() { slotLeftSideBarActivate(d->searchSideBar); d->searchSideBar->newKeywordSearch(); } void DigikamView::slotNewAdvancedSearch() { slotLeftSideBarActivate(d->searchSideBar); d->searchSideBar->newAdvancedSearch(); } void DigikamView::slotNewDuplicatesSearch(PAlbum* album) { slotLeftSideBarActivate(d->fuzzySearchSideBar); d->fuzzySearchSideBar->newDuplicatesSearch(album); } void DigikamView::slotNewDuplicatesSearch(const QList& albums) { slotLeftSideBarActivate(d->fuzzySearchSideBar); d->fuzzySearchSideBar->newDuplicatesSearch(albums); } void DigikamView::slotNewDuplicatesSearch(const QList& albums) { slotLeftSideBarActivate(d->fuzzySearchSideBar); d->fuzzySearchSideBar->newDuplicatesSearch(albums); } void DigikamView::slotAlbumsCleared() { emit signalAlbumSelected(0); } void DigikamView::slotAlbumHistoryBack(int steps) { QList albums; QWidget* widget = 0; d->albumHistory->back(albums, &widget, steps); changeAlbumFromHistory(albums, widget); } void DigikamView::slotAlbumHistoryForward(int steps) { QList albums; QWidget* widget = 0; d->albumHistory->forward(albums, &widget, steps); changeAlbumFromHistory(albums , widget); } // TODO update, use SideBarWidget instead of QWidget void DigikamView::changeAlbumFromHistory(const QList& album, QWidget* const widget) { if (!(album.isEmpty()) && widget) { // TODO update, temporary casting until signature is changed SidebarWidget* const sideBarWidget = dynamic_cast(widget); if (sideBarWidget) { sideBarWidget->changeAlbumFromHistory(album); slotLeftSideBarActivate(sideBarWidget); if (sideBarWidget == d->labelsSideBar) { d->labelsSearchHandler->restoreSelectionFromHistory(d->albumHistory->neededLabels()); } } d->parent->enableAlbumBackwardHistory(d->useAlbumHistory && !d->albumHistory->isBackwardEmpty()); d->parent->enableAlbumForwardHistory(d->useAlbumHistory && !d->albumHistory->isForwardEmpty()); } } void DigikamView::clearHistory() { d->albumHistory->clearHistory(); d->parent->enableAlbumBackwardHistory(false); d->parent->enableAlbumForwardHistory(false); } void DigikamView::getBackwardHistory(QStringList& titles) { d->albumHistory->getBackwardHistory(titles); for (int i = 0 ; i < titles.size() ; ++i) { titles[i] = d->userPresentableAlbumTitle(titles.at(i)); } } void DigikamView::getForwardHistory(QStringList& titles) { d->albumHistory->getForwardHistory(titles); for (int i = 0 ; i < titles.size() ; ++i) { titles[i] = d->userPresentableAlbumTitle(titles.at(i)); } } void DigikamView::slotGotoAlbumAndItem(const ImageInfo& imageInfo) { qCDebug(DIGIKAM_GENERAL_LOG) << "going to " << imageInfo; emit signalNoCurrentItem(); PAlbum* const album = AlbumManager::instance()->findPAlbum(imageInfo.albumId()); d->albumFolderSideBar->setCurrentAlbum(album); slotLeftSideBarActivate(d->albumFolderSideBar); // Set the activate item url to find in the Album View after // all items have be reloaded. slotSetCurrentWhenAvailable(imageInfo.id()); // And finally toggle album manager to handle album history and // reload all items. d->albumManager->setCurrentAlbums(QList() << album); } void DigikamView::slotGotoDateAndItem(const ImageInfo& imageInfo) { QDate date = imageInfo.dateTime().date(); emit signalNoCurrentItem(); // Change to Date Album view. // Note, that this also opens the side bar if it is closed; this is // considered as a feature, because it highlights that the view was changed. slotLeftSideBarActivate(d->dateViewSideBar); // Set the activate item url to find in the Album View after // all items have be reloaded. slotSetCurrentWhenAvailable(imageInfo.id()); // Change the year and month of the iconItem (day is unused). d->dateViewSideBar->gotoDate(date); } void DigikamView::slotGotoTagAndItem(int tagID) { // FIXME: Arnd: don't know yet how to get the iconItem passed through ... // then we would know how to use the following ... // KURL url( iconItem->imageInfo()->kurl() ); // url.cleanPath(); emit signalNoCurrentItem(); // Change to Tag Folder view. // Note, that this also opens the side bar if it is closed; this is // considered as a feature, because it highlights that the view was changed. slotLeftSideBarActivate(d->tagViewSideBar); // Set the current tag in the tag folder view. // TODO this slot should use a TAlbum pointer directly TAlbum* const tag = AlbumManager::instance()->findTAlbum(tagID); if (tag) { d->tagViewSideBar->setCurrentAlbum(tag); } else { qCDebug(DIGIKAM_GENERAL_LOG) << "Could not find a tag album for tag id " << tagID; } // Set the activate item url to find in the Tag View after // all items have be reloaded. // FIXME: see above // d->iconView->setAlbumItemToFind(url); } void DigikamView::slotSelectAlbum(const QUrl& url) { PAlbum* const album = d->albumManager->findPAlbum(url); if (!album) { qCWarning(DIGIKAM_GENERAL_LOG) << "Unable to find album for " << url; return; } slotLeftSideBarActivate(d->albumFolderSideBar); d->albumFolderSideBar->setCurrentAlbum(album); } void DigikamView::slotAlbumSelected(const QList& albums) { emit signalNoCurrentItem(); emit signalAlbumSelected(0); if (albums.isEmpty() || !albums.first()) { d->iconView->openAlbum(QList()); #ifdef HAVE_MARBLE d->mapView->openAlbum(0); #endif // HAVE_MARBLE slotTogglePreviewMode(ImageInfo()); return; } Album* const album = albums.first(); emit signalAlbumSelected(album); if (d->useAlbumHistory && !d->labelsSearchHandler->isRestoringSelectionFromHistory()) { if (!(d->leftSideBar->getActiveTab() == d->labelsSideBar)) { d->albumHistory->addAlbums(albums, d->leftSideBar->getActiveTab()); } else { if (albums.first()->isUsedByLabelsTree()) { d->albumHistory->addAlbums(albums, d->leftSideBar->getActiveTab(), d->labelsSideBar->selectedLabels()); } } } d->parent->enableAlbumBackwardHistory(d->useAlbumHistory && !d->albumHistory->isBackwardEmpty()); d->parent->enableAlbumForwardHistory(d->useAlbumHistory && !d->albumHistory->isForwardEmpty()); d->iconView->openAlbum(albums); if (album->isRoot()) { d->stackedview->setViewMode(StackedView::WelcomePageMode); } else if (album->isTrashAlbum()) { PAlbum* const palbum = d->albumManager->findPAlbum(album->parent()->id()); if (palbum) { QUrl url = palbum->fileUrl(); url = url.adjusted(QUrl::StripTrailingSlash); d->trashView->model()->loadItemsForCollection(url.toLocalFile()); d->filterWidget->setEnabled(false); d->stackedview->setViewMode(StackedView::TrashViewMode); } } else { switch (viewMode()) { case StackedView::PreviewImageMode: case StackedView::MediaPlayerMode: case StackedView::WelcomePageMode: case StackedView::TrashViewMode: slotTogglePreviewMode(ImageInfo()); break; default: break; } d->filterWidget->setEnabled(true); } } void DigikamView::slotAlbumOpenInFileManager() { Album* const album = d->albumManager->currentAlbums().first(); if (!album || album->type() != Album::PHYSICAL) { return; } if (album->isRoot()) { QMessageBox::critical(this, qApp->applicationName(), i18n("Cannot open the root album. It is not a physical location.")); return; } PAlbum* const palbum = dynamic_cast(album); if (palbum) { QDesktopServices::openUrl(QUrl::fromLocalFile(palbum->folderPath())); } } void DigikamView::slotRefresh() { switch (viewMode()) { case StackedView::PreviewImageMode: d->stackedview->imagePreviewView()->reload(); break; #ifdef HAVE_MEDIAPLAYER case StackedView::MediaPlayerMode: d->stackedview->mediaPlayerView()->reload(); break; #endif //HAVE_MEDIAPLAYER default: Album* const album = currentAlbum(); if (!album) return; // force reloading of thumbnails LoadingCacheInterface::cleanThumbnailCache(); ThumbsGenerator* const tool = new ThumbsGenerator(true, album->id()); tool->start(); // if physical album, schedule a collection scan of current album's path if (album->type() == Album::PHYSICAL) { NewItemsFinder* const tool = new NewItemsFinder(NewItemsFinder::ScheduleCollectionScan, QStringList() << static_cast(album)->folderPath()); connect(tool, SIGNAL(signalComplete()), this, SLOT(slotAlbumRefreshComplete())); tool->start(); } break; } } void DigikamView::slotAlbumRefreshComplete() { // force reload. Should normally not be necessary, but we may have bugs qlonglong currentId = currentInfo().id(); d->iconView->imageAlbumModel()->refresh(); if (currentId != -1) { slotSetCurrentWhenAvailable(currentId); } } void DigikamView::slotImageSelected() { // delay to slotDispatchImageSelected d->needDispatchSelection = true; d->selectionTimer->start(); switch (viewMode()) { case StackedView::TableViewMode: emit signalSelectionChanged(d->tableView->numberOfSelectedItems()); break; default: emit signalSelectionChanged(d->iconView->numberOfSelectedIndexes()); } } void DigikamView::slotDispatchImageSelected() { if (viewMode() == StackedView::TrashViewMode) { d->rightSideBar->itemChanged(d->trashView->lastSelectedItemUrl()); return; } if (d->needDispatchSelection) { // the list of ImageInfos of currently selected items, currentItem first const ImageInfoList list = selectedInfoList(true, true); const ImageInfoList allImages = allInfo(true); if (list.isEmpty()) { d->stackedview->setPreviewItem(); emit signalImageSelected(list, allImages); emit signalNoCurrentItem(); } else { d->rightSideBar->itemChanged(list); ImageInfo previousInfo; ImageInfo nextInfo; if (viewMode() == StackedView::TableViewMode) { previousInfo = d->tableView->previousInfo(); nextInfo = d->tableView->nextInfo(); } else { previousInfo = d->iconView->previousInfo(list.first()); nextInfo = d->iconView->nextInfo(list.first()); } if ((viewMode() != StackedView::IconViewMode) && (viewMode() != StackedView::MapWidgetMode) && (viewMode() != StackedView::TableViewMode) ) { d->stackedview->setPreviewItem(list.first(), previousInfo, nextInfo); } emit signalImageSelected(list, allImages); } d->needDispatchSelection = false; } } double DigikamView::zoomMin() const { return d->stackedview->zoomMin(); } double DigikamView::zoomMax() const { return d->stackedview->zoomMax(); } void DigikamView::setZoomFactor(double zoom) { d->stackedview->setZoomFactorSnapped(zoom); } void DigikamView::slotZoomFactorChanged(double zoom) { toggleZoomActions(); emit signalZoomChanged(zoom); } void DigikamView::setThumbSize(int size) { if (viewMode() == StackedView::PreviewImageMode) { double z = DZoomBar::zoomFromSize(size, zoomMin(), zoomMax()); setZoomFactor(z); } else if ( (viewMode() == StackedView::IconViewMode) || (viewMode() == StackedView::TableViewMode) || (viewMode() == StackedView::TrashViewMode)) { if (size > ThumbnailSize::maxThumbsSize()) { d->thumbSize = ThumbnailSize::maxThumbsSize(); } else if (size < ThumbnailSize::Small) { d->thumbSize = ThumbnailSize::Small; } else { d->thumbSize = size; } emit signalThumbSizeChanged(d->thumbSize); d->thumbSizeTimer->start(); } } void DigikamView::slotThumbSizeEffect() { d->iconView->setThumbnailSize(ThumbnailSize(d->thumbSize)); d->tableView->setThumbnailSize(ThumbnailSize(d->thumbSize)); d->trashView->setThumbnailSize(ThumbnailSize(d->thumbSize)); toggleZoomActions(); ApplicationSettings::instance()->setDefaultIconSize(d->thumbSize); } void DigikamView::toggleZoomActions() { if (viewMode() == StackedView::PreviewImageMode) { d->parent->enableZoomMinusAction(true); d->parent->enableZoomPlusAction(true); if (d->stackedview->maxZoom()) { d->parent->enableZoomPlusAction(false); } if (d->stackedview->minZoom()) { d->parent->enableZoomMinusAction(false); } } else if ((viewMode() == StackedView::IconViewMode) || (viewMode() == StackedView::TableViewMode)) { d->parent->enableZoomMinusAction(true); d->parent->enableZoomPlusAction(true); if (d->thumbSize >= ThumbnailSize::maxThumbsSize()) { d->parent->enableZoomPlusAction(false); } if (d->thumbSize <= ThumbnailSize::Small) { d->parent->enableZoomMinusAction(false); } } else { d->parent->enableZoomMinusAction(false); d->parent->enableZoomPlusAction(false); } } void DigikamView::slotZoomIn() { if ( (viewMode() == StackedView::IconViewMode) || (viewMode() == StackedView::TableViewMode) ) { setThumbSize(d->thumbSize + ThumbnailSize::Step); toggleZoomActions(); emit signalThumbSizeChanged(d->thumbSize); } else if (viewMode() == StackedView::PreviewImageMode) { d->stackedview->increaseZoom(); } } void DigikamView::slotZoomOut() { if ( (viewMode() == StackedView::IconViewMode) || (viewMode() == StackedView::TableViewMode) ) { setThumbSize(d->thumbSize - ThumbnailSize::Step); toggleZoomActions(); emit signalThumbSizeChanged(d->thumbSize); } else if (viewMode() == StackedView::PreviewImageMode) { d->stackedview->decreaseZoom(); } } void DigikamView::slotZoomTo100Percents() { if (viewMode() == StackedView::PreviewImageMode) { d->stackedview->toggleFitToWindowOr100(); } } void DigikamView::slotFitToWindow() { if (viewMode() == StackedView::TableViewMode) { /// @todo We should choose an appropriate thumbnail size here } else if (viewMode() == StackedView::IconViewMode) { int nts = d->iconView->fitToWidthIcons(); qCDebug(DIGIKAM_GENERAL_LOG) << "new thumb size = " << nts; setThumbSize(nts); toggleZoomActions(); emit signalThumbSizeChanged(d->thumbSize); } else if (viewMode() == StackedView::PreviewImageMode) { d->stackedview->fitToWindow(); } } void DigikamView::slotAlbumPropsEdit() { d->albumModificationHelper->slotAlbumEdit(d->albumManager->currentPAlbum()); } void DigikamView::slotAlbumWriteMetadata() { Album* const album = d->albumManager->currentAlbums().first(); if (!album) { return; } MetadataSynchronizer* const tool = new MetadataSynchronizer(AlbumList() << album, MetadataSynchronizer::WriteFromDatabaseToFile); tool->start(); } void DigikamView::slotAlbumReadMetadata() { Album* const album = d->albumManager->currentAlbums().first(); if (!album) { return; } MetadataSynchronizer* const tool = new MetadataSynchronizer(AlbumList() << album, MetadataSynchronizer::ReadFromFileToDatabase); tool->start(); } void DigikamView::slotImageWriteMetadata() { const ImageInfoList selected = selectedInfoList(ApplicationSettings::Metadata); MetadataSynchronizer* const tool = new MetadataSynchronizer(selected, MetadataSynchronizer::WriteFromDatabaseToFile); tool->start(); } void DigikamView::slotImageReadMetadata() { const ImageInfoList selected = selectedInfoList(ApplicationSettings::Metadata); MetadataSynchronizer* const tool = new MetadataSynchronizer(selected, MetadataSynchronizer::ReadFromFileToDatabase); tool->start(); } // ---------------------------------------------------------------- void DigikamView::slotEscapePreview() { if (viewMode() == StackedView::IconViewMode || viewMode() == StackedView::MapWidgetMode || viewMode() == StackedView::TableViewMode || viewMode() == StackedView::WelcomePageMode) { return; } // pass a null image info, because we want to fall back to the old // view mode slotTogglePreviewMode(ImageInfo()); } void DigikamView::slotMapWidgetView() { d->stackedview->setViewMode(StackedView::MapWidgetMode); } void DigikamView::slotTableView() { d->stackedview->setViewMode(StackedView::TableViewMode); } void DigikamView::slotIconView() { if (viewMode() == StackedView::PreviewImageMode) { emit signalThumbSizeChanged(d->thumbSize); } // and switch to icon view d->stackedview->setViewMode(StackedView::IconViewMode); // make sure the next/previous buttons are updated slotImageSelected(); } void DigikamView::slotImagePreview() { slotTogglePreviewMode(currentInfo()); } /** * @brief This method toggles between AlbumView/MapWidgetView and ImagePreview modes, depending on the context. */ void DigikamView::slotTogglePreviewMode(const ImageInfo& info) { if ( (viewMode() == StackedView::IconViewMode || viewMode() == StackedView::TableViewMode || viewMode() == StackedView::MapWidgetMode) && !info.isNull() ) { if (info.isLocationAvailable()) { d->lastViewMode = viewMode(); if (viewMode() == StackedView::IconViewMode) { d->stackedview->setPreviewItem(info, d->iconView->previousInfo(info), d->iconView->nextInfo(info)); } else { d->stackedview->setPreviewItem(info, ImageInfo(), ImageInfo()); } } else { QModelIndex index = d->iconView->indexForInfo(info); d->iconView->showIndexNotification(index, i18nc("@info", "The storage location of this image
is currently not available
")); } } else { // go back to either AlbumViewMode or MapWidgetMode d->stackedview->setViewMode( d->lastViewMode ); } // make sure the next/previous buttons are updated slotImageSelected(); } void DigikamView::slotViewModeChanged() { toggleZoomActions(); switch (viewMode()) { case StackedView::IconViewMode: emit signalSwitchedToIconView(); emit signalThumbSizeChanged(d->thumbSize); break; case StackedView::PreviewImageMode: emit signalSwitchedToPreview(); slotZoomFactorChanged(d->stackedview->zoomFactor()); break; case StackedView::WelcomePageMode: emit signalSwitchedToIconView(); break; case StackedView::MediaPlayerMode: emit signalSwitchedToPreview(); break; case StackedView::MapWidgetMode: emit signalSwitchedToMapView(); //TODO: connect map view's zoom buttons to main status bar zoom buttons break; case StackedView::TableViewMode: emit signalSwitchedToTableView(); emit signalThumbSizeChanged(d->thumbSize); break; case StackedView::TrashViewMode: emit signalSwitchedToTrashView(); break; } } void DigikamView::slotImageFindSimilar() { const ImageInfo current = currentInfo(); if (!current.isNull()) { d->fuzzySearchSideBar->newSimilarSearch(current); slotLeftSideBarActivate(d->fuzzySearchSideBar); } } void DigikamView::slotImageScanForFaces() { FaceScanSettings settings; settings.accuracy = ApplicationSettings::instance()->getFaceDetectionAccuracy(); settings.recognizeAlgorithm = RecognitionDatabase::RecognizeAlgorithm::LBP; settings.task = FaceScanSettings::DetectAndRecognize; settings.alreadyScannedHandling = FaceScanSettings::Rescan; settings.infos = selectedInfoList(ApplicationSettings::Tools); FacesDetector* const tool = new FacesDetector(settings); connect(tool, SIGNAL(signalComplete()), this, SLOT(slotRefreshImagePreview())); tool->start(); } void DigikamView::slotRefreshImagePreview() { if (viewMode() == StackedView::PreviewImageMode) { d->stackedview->imagePreviewView()->reload(); } } void DigikamView::slotEditor() { const ImageInfoList imageInfoList = selectedInfoList(ApplicationSettings::Tools); ImageInfo singleInfo = currentInfo(); if (singleInfo.isNull() && !imageInfoList.isEmpty()) { singleInfo = imageInfoList.first(); } Album* const current = currentAlbum(); d->utilities->openInfos(singleInfo, imageInfoList, current); } void DigikamView::slotFileWithDefaultApplication() { d->utilities->openInfosWithDefaultApplication(selectedInfoList(ApplicationSettings::Tools)); } void DigikamView::slotLightTable() { bool grouping = selectedNeedGroupResolving(ApplicationSettings::LightTable); const ImageInfoList selectedList = selectedInfoList(false, grouping); if (selectedList.isEmpty()) { grouping = allNeedGroupResolving(ApplicationSettings::LightTable); } const ImageInfoList allInfoList = allInfo(grouping); const ImageInfo currentImageInfo = currentInfo(); d->utilities->insertToLightTableAuto(allInfoList, selectedList, currentImageInfo); } void DigikamView::slotQueueMgr() { bool grouping = selectedNeedGroupResolving(ApplicationSettings::BQM); ImageInfoList imageInfoList = selectedInfoList(false, grouping); ImageInfo singleInfo = currentInfo(); if (singleInfo.isNull() && !imageInfoList.isEmpty()) { singleInfo = imageInfoList.first(); } if (singleInfo.isNull()) { grouping = allNeedGroupResolving(ApplicationSettings::BQM); const ImageInfoList allItems = allInfo(grouping); if (!allItems.isEmpty()) { singleInfo = allItems.first(); } } d->utilities->insertToQueueManager(imageInfoList, singleInfo, true); } void DigikamView::slotImageEdit() { // Where is the difference to slotEditor? slotEditor(); } void DigikamView::slotImageLightTable() { const ImageInfoList selectedList = selectedInfoList(ApplicationSettings::LightTable); const ImageInfo currentImageInfo = currentInfo(); // replace images in light table d->utilities->insertToLightTable(selectedList, currentImageInfo, false); } void DigikamView::slotImageAddToLightTable() { const ImageInfoList selectedList = selectedInfoList(ApplicationSettings::LightTable); const ImageInfo currentImageInfo = currentInfo(); // add to images in light table d->utilities->insertToLightTable(selectedList, currentImageInfo, true); } void DigikamView::slotImageAddToCurrentQueue() { const ImageInfoList selectedList = selectedInfoList(ApplicationSettings::BQM); const ImageInfo currentImageInfo = currentInfo(); d->utilities->insertToQueueManager(selectedList, currentImageInfo, false); } void DigikamView::slotImageAddToNewQueue() { const bool newQueue = QueueMgrWindow::queueManagerWindowCreated() && !QueueMgrWindow::queueManagerWindow()->queuesMap().isEmpty(); const ImageInfoList selectedList = selectedInfoList(ApplicationSettings::BQM); const ImageInfo currentImageInfo = currentInfo(); d->utilities->insertToQueueManager(selectedList, currentImageInfo, newQueue); } void DigikamView::slotImageAddToExistingQueue(int queueid) { const ImageInfoList selectedList = selectedInfoList(ApplicationSettings::BQM); const ImageInfo currentImageInfo = currentInfo(); if (!selectedList.isEmpty()) { d->utilities->insertSilentToQueueManager(selectedList, currentImageInfo, queueid); } } void DigikamView::slotImageRename() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->rename(); break; default: d->iconView->rename(); } } void DigikamView::slotImageDelete() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->slotDeleteSelected(ImageViewUtilities::DeleteUseTrash); break; default: d->iconView->deleteSelected(ImageViewUtilities::DeleteUseTrash); } } void DigikamView::slotImageDeletePermanently() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->slotDeleteSelected(ImageViewUtilities::DeletePermanently); break; default: d->iconView->deleteSelected(ImageViewUtilities::DeletePermanently); } } void DigikamView::slotImageDeletePermanentlyDirectly() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->slotDeleteSelectedWithoutConfirmation(ImageViewUtilities::DeletePermanently); break; default: d->iconView->deleteSelectedDirectly(ImageViewUtilities::DeletePermanently); } } void DigikamView::slotImageTrashDirectly() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->slotDeleteSelectedWithoutConfirmation(ImageViewUtilities::DeleteUseTrash); break; default: d->iconView->deleteSelectedDirectly(ImageViewUtilities::DeleteUseTrash); } } void DigikamView::slotSelectAll() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->selectAll(); break; default: d->iconView->selectAll(); } } void DigikamView::slotSelectNone() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->clearSelection(); break; default: d->iconView->clearSelection(); } } void DigikamView::slotSelectInvert() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->invertSelection(); break; default: d->iconView->invertSelection(); } } void DigikamView::slotSortImages(int sortRole) { ApplicationSettings* const settings = ApplicationSettings::instance(); if (!settings) { return; } settings->setImageSortOrder(sortRole); d->iconView->imageFilterModel()->setSortRole((ImageSortSettings::SortRole) sortRole); settings->emitSetupChanged(); } void DigikamView::slotSortImagesOrder(int order) { ApplicationSettings* const settings = ApplicationSettings::instance(); if (!settings) { return; } settings->setImageSorting(order); d->iconView->imageFilterModel()->setSortOrder((ImageSortSettings::SortOrder) order); settings->emitSetupChanged(); } void DigikamView::slotSeparateImages(int categoryMode) { ApplicationSettings* const settings = ApplicationSettings::instance(); if (!settings) { return; } settings->setImageSeparationMode(categoryMode); d->iconView->imageFilterModel()->setCategorizationMode((ImageSortSettings::CategorizationMode) categoryMode); } void DigikamView::slotImageSeparationSortOrder(int order) { ApplicationSettings* const settings = ApplicationSettings::instance(); if (!settings) { return; } settings->setImageSeparationSortOrder(order); d->iconView->imageFilterModel()->setCategorizationSortOrder((ImageSortSettings::SortOrder) order); } void DigikamView::slotMoveSelectionToAlbum() { d->utilities->createNewAlbumForInfos(selectedInfoList(false, true), currentAlbum()); } void DigikamView::slotImagePaste() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->slotPaste(); break; default: d->iconView->paste(); } } void DigikamView::slotLeftSidebarChangedTab(QWidget* w) { // TODO update, temporary cast SidebarWidget* const widget = dynamic_cast(w); foreach(SidebarWidget* const sideBarWidget, d->leftSideBarWidgets) { bool active = (widget && (widget == sideBarWidget)); sideBarWidget->setActive(active); } } void DigikamView::toggleTag(int tagID) { ImageInfoList tagToRemove, tagToAssign; const ImageInfoList selectedList = selectedInfoList(ApplicationSettings::Metadata); foreach(const ImageInfo& info, selectedList) { if (info.tagIds().contains(tagID)) tagToRemove.append(info); else tagToAssign.append(info); } FileActionMngr::instance()->assignTag(tagToAssign, tagID); FileActionMngr::instance()->removeTag(tagToRemove, tagID); } void DigikamView::slotAssignPickLabel(int pickId) { FileActionMngr::instance()->assignPickLabel(selectedInfoList(ApplicationSettings::Metadata), pickId); } void DigikamView::slotAssignColorLabel(int colorId) { FileActionMngr::instance()->assignColorLabel(selectedInfoList(ApplicationSettings::Metadata), colorId); } void DigikamView::slotAssignRating(int rating) { FileActionMngr::instance()->assignRating(selectedInfoList(ApplicationSettings::Metadata), rating); } void DigikamView::slotAssignTag(int tagID) { FileActionMngr::instance()->assignTags(selectedInfoList(ApplicationSettings::Metadata), QList() << tagID); } void DigikamView::slotRemoveTag(int tagID) { FileActionMngr::instance()->removeTags(selectedInfoList(ApplicationSettings::Metadata), QList() << tagID); } void DigikamView::slotSlideShowAll() { slideShow(allInfo(ApplicationSettings::Slideshow)); } void DigikamView::slotSlideShowSelection() { slideShow(selectedInfoList(ApplicationSettings::Slideshow)); } void DigikamView::slotSlideShowRecursive() { QList albumList = AlbumManager::instance()->currentAlbums(); Album* album = 0; if (!albumList.isEmpty()) { album = albumList.first(); } if (album) { SlideShowBuilder* const builder = new SlideShowBuilder(album); connect(builder, SIGNAL(signalComplete(SlideShowSettings)), this, SLOT(slotSlideShowBuilderComplete(SlideShowSettings))); builder->run(); } } void DigikamView::slotSlideShowManualFromCurrent() { slotSlideShowManualFrom(currentInfo()); } void DigikamView::slotSlideShowManualFrom(const ImageInfo& info) { SlideShowBuilder* const builder = new SlideShowBuilder(allInfo(ApplicationSettings::Slideshow)); builder->setOverrideStartFrom(info); builder->setAutoPlayEnabled(false); connect(builder, SIGNAL(signalComplete(SlideShowSettings)), this, SLOT(slotSlideShowBuilderComplete(SlideShowSettings))); builder->run(); } void DigikamView::presentation() { QPointer mngr = new PresentationMngr(this); foreach(const ImageInfo& info, selectedInfoList(ApplicationSettings::Slideshow)) { mngr->addFile(info.fileUrl(), info.comment()); qApp->processEvents(); } mngr->showConfigDialog(); } void DigikamView::slideShow(const ImageInfoList& infoList) { SlideShowBuilder* const builder = new SlideShowBuilder(infoList); connect(builder, SIGNAL(signalComplete(SlideShowSettings)), this, SLOT(slotSlideShowBuilderComplete(SlideShowSettings))); builder->run(); } void DigikamView::slotSlideShowBuilderComplete(const SlideShowSettings& settings) { QPointer slide = new SlideShow(settings); TagsActionMngr::defaultManager()->registerActionsToWidget(slide); if (settings.imageUrl.isValid()) { slide->setCurrentItem(settings.imageUrl); } else if (settings.startWithCurrent) { slide->setCurrentItem(currentInfo().fileUrl()); } connect(slide, SIGNAL(signalRatingChanged(QUrl,int)), this, SLOT(slotRatingChanged(QUrl,int))); connect(slide, SIGNAL(signalColorLabelChanged(QUrl,int)), this, SLOT(slotColorLabelChanged(QUrl,int))); connect(slide, SIGNAL(signalPickLabelChanged(QUrl,int)), this, SLOT(slotPickLabelChanged(QUrl,int))); connect(slide, SIGNAL(signalToggleTag(QUrl,int)), this, SLOT(slotToggleTag(QUrl,int))); connect(slide, SIGNAL(signalLastItemUrl(QUrl)), d->iconView, SLOT(setCurrentUrl(QUrl))); slide->show(); } void DigikamView::toggleShowBar(bool b) { d->stackedview->thumbBarDock()->showThumbBar(b); // See bug #319876 : force to reload current view mode to set thumbbar visibility properly. d->stackedview->setViewMode(viewMode()); } void DigikamView::setRecurseAlbums(bool recursive) { d->iconView->imageAlbumModel()->setRecurseAlbums(recursive); } void DigikamView::setRecurseTags(bool recursive) { d->iconView->imageAlbumModel()->setRecurseTags(recursive); } void DigikamView::slotSidebarTabTitleStyleChanged() { d->leftSideBar->setStyle(ApplicationSettings::instance()->getSidebarTitleStyle()); d->rightSideBar->setStyle(ApplicationSettings::instance()->getSidebarTitleStyle()); /// @todo Which settings actually have to be reloaded? // d->rightSideBar->applySettings(); } void DigikamView::slotImageChangeFailed(const QString& message, const QStringList& fileNames) { if (fileNames.isEmpty()) { return; } DMessageBox::showInformationList(QMessageBox::Critical, qApp->activeWindow(), qApp->applicationName(), message, fileNames); } void DigikamView::slotLeftSideBarActivateAlbums() { d->leftSideBar->setActiveTab(d->albumFolderSideBar); } void DigikamView::slotLeftSideBarActivateTags() { d->leftSideBar->setActiveTab(d->tagViewSideBar); } void DigikamView::slotLeftSideBarActivate(SidebarWidget* widget) { d->leftSideBar->setActiveTab(widget); } void DigikamView::slotLeftSideBarActivate(QWidget* widget) { slotLeftSideBarActivate(static_cast(widget)); } void DigikamView::slotRightSideBarActivateTitles() { d->rightSideBar->setActiveTab(d->rightSideBar->imageDescEditTab()); d->rightSideBar->imageDescEditTab()->setFocusToTitlesEdit(); } void DigikamView::slotRightSideBarActivateComments() { d->rightSideBar->setActiveTab(d->rightSideBar->imageDescEditTab()); d->rightSideBar->imageDescEditTab()->setFocusToCommentsEdit(); } void DigikamView::slotRightSideBarActivateAssignedTags() { d->rightSideBar->setActiveTab(d->rightSideBar->imageDescEditTab()); d->rightSideBar->imageDescEditTab()->activateAssignedTagsButton(); } void DigikamView::slotRatingChanged(const QUrl& url, int rating) { rating = qMin(RatingMax, qMax(RatingMin, rating)); ImageInfo info = ImageInfo::fromUrl(url); if (!info.isNull()) { FileActionMngr::instance()->assignRating(info, rating); } } void DigikamView::slotColorLabelChanged(const QUrl& url, int color) { ImageInfo info = ImageInfo::fromUrl(url); if (!info.isNull()) { FileActionMngr::instance()->assignColorLabel(info, color); } } void DigikamView::slotPickLabelChanged(const QUrl& url, int pick) { ImageInfo info = ImageInfo::fromUrl(url); if (!info.isNull()) { FileActionMngr::instance()->assignPickLabel(info, pick); } } void DigikamView::slotToggleTag(const QUrl& url, int tagID) { ImageInfo info = ImageInfo::fromUrl(url); if (!info.isNull()) { if (info.tagIds().contains(tagID)) FileActionMngr::instance()->removeTag(info, tagID); else FileActionMngr::instance()->assignTag(info, tagID); } } bool DigikamView::hasCurrentItem() const { return !currentInfo().isNull(); } void DigikamView::slotFocusAndNextImage() { //slot is called on pressing "return" a second time after assigning a tag d->stackedview->currentWidget()->setFocus(); //select next image, since the user is probably done tagging the current image slotNextItem(); } void DigikamView::slotImageExifOrientation(int orientation) { FileActionMngr::instance()->setExifOrientation( selectedInfoList(ApplicationSettings::Metadata), orientation); } void DigikamView::imageTransform(MetaEngineRotation::TransformationAction transform) { FileActionMngr::instance()->transform( selectedInfoList(ApplicationSettings::Metadata), transform); } ImageInfo DigikamView::currentInfo() const { switch (viewMode()) { case StackedView::TableViewMode: return d->tableView->currentInfo(); #ifdef HAVE_MARBLE case StackedView::MapWidgetMode: return d->mapView->currentImageInfo(); #endif // HAVE_MARBLE case StackedView::MediaPlayerMode: case StackedView::PreviewImageMode: case StackedView::IconViewMode: // all of these modes use the same selection model and data as the IconViewMode return d->iconView->currentInfo(); default: return ImageInfo(); } } Album* DigikamView::currentAlbum() const { switch (viewMode()) { case StackedView::TableViewMode: return d->tableView->currentAlbum(); case StackedView::PreviewImageMode: case StackedView::MediaPlayerMode: case StackedView::MapWidgetMode: case StackedView::IconViewMode: // all of these modes use the same selection model and data as the IconViewMode return d->iconView->currentAlbum(); default: return 0; } } ImageInfoList DigikamView::selectedInfoList(const bool currentFirst, const bool grouping) const { switch (viewMode()) { case StackedView::TableViewMode: if (currentFirst) { return d->tableView->selectedImageInfosCurrentFirst(grouping); } return d->tableView->selectedImageInfos(grouping); case StackedView::PreviewImageMode: case StackedView::MediaPlayerMode: case StackedView::MapWidgetMode: case StackedView::IconViewMode: // all of these modes use the same selection model and data as the IconViewMode if (currentFirst) { return d->iconView->selectedImageInfosCurrentFirst(grouping); } return d->iconView->selectedImageInfos(grouping); default: return ImageInfoList(); } } ImageInfoList DigikamView::selectedInfoList(const ApplicationSettings::OperationType type, const bool currentFirst) const { return selectedInfoList(currentFirst, selectedNeedGroupResolving(type)); } ImageInfoList DigikamView::allInfo(const bool grouping) const { switch (viewMode()) { case StackedView::TableViewMode: return d->tableView->allImageInfos(grouping); case StackedView::MapWidgetMode: case StackedView::PreviewImageMode: case StackedView::MediaPlayerMode: case StackedView::IconViewMode: // all of these modes use the same selection model and data as the IconViewMode return d->iconView->allImageInfos(grouping); default: return ImageInfoList(); } } ImageInfoList DigikamView::allInfo(const ApplicationSettings::OperationType type) const { return allInfo(allNeedGroupResolving(type)); } bool DigikamView::allNeedGroupResolving(const ApplicationSettings::OperationType type) const { switch (viewMode()) { case StackedView::TableViewMode: return d->tableView->allNeedGroupResolving(type); case StackedView::MapWidgetMode: case StackedView::PreviewImageMode: case StackedView::MediaPlayerMode: case StackedView::IconViewMode: // all of these modes use the same selection model and data as the IconViewMode return d->iconView->allNeedGroupResolving(type); default: return false; } } bool DigikamView::selectedNeedGroupResolving(const ApplicationSettings::OperationType type) const { switch (viewMode()) { case StackedView::TableViewMode: return d->tableView->selectedNeedGroupResolving(type); case StackedView::MapWidgetMode: case StackedView::PreviewImageMode: case StackedView::MediaPlayerMode: case StackedView::IconViewMode: // all of these modes use the same selection model and data as the IconViewMode return d->iconView->selectedNeedGroupResolving(type); default: return false; } } QUrl DigikamView::currentUrl() const { const ImageInfo cInfo = currentInfo(); return cInfo.fileUrl(); } void DigikamView::slotSetCurrentWhenAvailable(const qlonglong id) { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->slotSetCurrentWhenAvailable(id); break; default: d->iconView->setCurrentWhenAvailable(id); } } void DigikamView::slotAwayFromSelection() { switch (viewMode()) { case StackedView::TableViewMode: d->tableView->slotAwayFromSelection(); break; default: d->iconView->awayFromSelection(); } } StackedView::StackedViewMode DigikamView::viewMode() const { return d->stackedview->viewMode(); } void DigikamView::slotSetupMetadataFilters(int tab) { Setup::execMetadataFilters(this, tab); } void DigikamView::toggleFullScreen(bool set) { d->stackedview->imagePreviewView()->toggleFullScreen(set); } void DigikamView::setToolsIconView(DCategorizedView* const view) { d->rightSideBar->appendTab(view, QIcon::fromTheme(QLatin1String("document-edit")), i18n("Tools")); } void DigikamView::slotShowContextMenu(QContextMenuEvent* event, const QList& extraGroupingActions) { Album* const album = currentAlbum(); if (!album || album->isRoot() || (album->type() != Album::PHYSICAL && album->type() != Album::TAG) ) { return; } QMenu menu(this); ContextMenuHelper cmHelper(&menu); cmHelper.addAction(QLatin1String("full_screen")); cmHelper.addAction(QLatin1String("options_show_menubar")); cmHelper.addSeparator(); // -------------------------------------------------------- cmHelper.addStandardActionPaste(this, SLOT(slotImagePaste())); // -------------------------------------------------------- if (!extraGroupingActions.isEmpty()) { cmHelper.addSeparator(); cmHelper.addGroupMenu(QList(), extraGroupingActions); } cmHelper.exec(event->globalPos()); } void DigikamView::slotShowContextMenuOnInfo(QContextMenuEvent* event, const ImageInfo& info, const QList& extraGroupingActions, ImageFilterModel* imageFilterModel) { QList selectedImageIds = selectedInfoList(true, true).toImageIdList(); // -------------------------------------------------------- QMenu menu(this); ContextMenuHelper cmHelper(&menu); cmHelper.setImageFilterModel(imageFilterModel); cmHelper.addAction(QLatin1String("full_screen")); cmHelper.addAction(QLatin1String("options_show_menubar")); cmHelper.addSeparator(); // -------------------------------------------------------- QAction* const viewAction = new QAction(i18nc("View the selected image", "Preview"), this); viewAction->setIcon(QIcon::fromTheme(QLatin1String("view-preview"))); viewAction->setEnabled(selectedImageIds.count() == 1); cmHelper.addAction(viewAction); cmHelper.addOpenAndNavigateActions(selectedImageIds); cmHelper.addSeparator(); // -------------------------------------------------------- cmHelper.addAction(QLatin1String("image_scan_for_faces")); cmHelper.addAction(QLatin1String("image_find_similar")); cmHelper.addStandardActionLightTable(); cmHelper.addQueueManagerMenu(); cmHelper.addSeparator(); // -------------------------------------------------------- cmHelper.addAction(QLatin1String("image_rotate")); cmHelper.addAction(QLatin1String("cut_album_selection")); cmHelper.addAction(QLatin1String("copy_album_selection")); cmHelper.addAction(QLatin1String("paste_album_selection")); cmHelper.addAction(QLatin1String("image_rename")); cmHelper.addStandardActionItemDelete(this, SLOT(slotImageDelete()), selectedImageIds.count()); cmHelper.addSeparator(); // -------------------------------------------------------- cmHelper.addStandardActionThumbnail(selectedImageIds, currentAlbum()); cmHelper.addAssignTagsMenu(selectedImageIds); cmHelper.addRemoveTagsMenu(selectedImageIds); cmHelper.addLabelsAction(); if (d->leftSideBar->getActiveTab() != d->peopleSideBar) { cmHelper.addSeparator(); cmHelper.addGroupMenu(selectedImageIds, extraGroupingActions); } // special action handling -------------------------------- connect(&cmHelper, SIGNAL(signalAssignColorLabel(int)), this, SLOT(slotAssignColorLabel(int))); connect(&cmHelper, SIGNAL(signalAssignPickLabel(int)), this, SLOT(slotAssignPickLabel(int))); connect(&cmHelper, SIGNAL(signalAssignRating(int)), this, SLOT(slotAssignRating(int))); connect(&cmHelper, SIGNAL(signalAssignTag(int)), this, SLOT(slotAssignTag(int))); connect(&cmHelper, SIGNAL(signalRemoveTag(int)), this, SLOT(slotRemoveTag(int))); connect(&cmHelper, SIGNAL(signalPopupTagsView()), d->rightSideBar, SLOT(slotPopupTagsView())); connect(&cmHelper, SIGNAL(signalGotoTag(int)), this, SLOT(slotGotoTagAndItem(int))); connect(&cmHelper, SIGNAL(signalGotoTag(int)), d->albumHistory, SLOT(slotClearSelectTAlbum(int))); connect(&cmHelper, SIGNAL(signalGotoAlbum(ImageInfo)), this, SLOT(slotGotoAlbumAndItem(ImageInfo))); connect(&cmHelper, SIGNAL(signalGotoAlbum(ImageInfo)), d->albumHistory, SLOT(slotClearSelectPAlbum(ImageInfo))); connect(&cmHelper, SIGNAL(signalGotoDate(ImageInfo)), this, SLOT(slotGotoDateAndItem(ImageInfo))); connect(&cmHelper, SIGNAL(signalSetThumbnail(ImageInfo)), this, SLOT(slotSetAsAlbumThumbnail(ImageInfo))); connect(&cmHelper, SIGNAL(signalAddToExistingQueue(int)), this, SLOT(slotImageAddToExistingQueue(int))); connect(&cmHelper, SIGNAL(signalCreateGroup()), this, SLOT(slotCreateGroupFromSelection())); connect(&cmHelper, SIGNAL(signalCreateGroupByTime()), this, SLOT(slotCreateGroupByTimeFromSelection())); connect(&cmHelper, SIGNAL(signalCreateGroupByFilename()), this, SLOT(slotCreateGroupByFilenameFromSelection())); connect(&cmHelper, SIGNAL(signalRemoveFromGroup()), this, SLOT(slotRemoveSelectedFromGroup())); connect(&cmHelper, SIGNAL(signalUngroup()), this, SLOT(slotUngroupSelected())); // -------------------------------------------------------- QAction* const choice = cmHelper.exec(event->globalPos()); if (choice && (choice == viewAction)) { slotTogglePreviewMode(info); } } void DigikamView::slotShowGroupContextMenu(QContextMenuEvent* event, const QList& selectedInfos, ImageFilterModel* imageFilterModel) { QList selectedImageIDs; foreach(const ImageInfo& info, selectedInfos) { selectedImageIDs << info.id(); } QMenu popmenu(this); ContextMenuHelper cmhelper(&popmenu); cmhelper.setImageFilterModel(imageFilterModel); cmhelper.addGroupActions(selectedImageIDs); // special action handling -------------------------------- connect(&cmhelper, SIGNAL(signalCreateGroup()), this, SLOT(slotCreateGroupFromSelection())); connect(&cmhelper, SIGNAL(signalCreateGroupByTime()), this, SLOT(slotCreateGroupByTimeFromSelection())); connect(&cmhelper, SIGNAL(signalCreateGroupByFilename()), this, SLOT(slotCreateGroupByFilenameFromSelection())); connect(&cmhelper, SIGNAL(signalUngroup()), this, SLOT(slotUngroupSelected())); connect(&cmhelper, SIGNAL(signalRemoveFromGroup()), this, SLOT(slotRemoveSelectedFromGroup())); cmhelper.exec(event->globalPos()); } void DigikamView::slotSetAsAlbumThumbnail(const ImageInfo& info) { d->utilities->setAsAlbumThumbnail(currentAlbum(), info); } void DigikamView::slotCreateGroupFromSelection() { FileActionMngr::instance()->addToGroup(currentInfo(), selectedInfoList(false, true)); } void DigikamView::slotCreateGroupByTimeFromSelection() { d->utilities->createGroupByTimeFromInfoList(selectedInfoList(false, true)); } void DigikamView::slotCreateGroupByFilenameFromSelection() { d->utilities->createGroupByFilenameFromInfoList(selectedInfoList(false, true)); } void DigikamView::slotRemoveSelectedFromGroup() { FileActionMngr::instance()->removeFromGroup(selectedInfoList(false, true)); } void DigikamView::slotUngroupSelected() { FileActionMngr::instance()->ungroup(selectedInfoList(false, true)); } } // namespace Digikam diff --git a/core/libs/album/albummanager.cpp b/core/libs/album/albummanager.cpp index defcc0804a..c56e3e95cf 100644 --- a/core/libs/album/albummanager.cpp +++ b/core/libs/album/albummanager.cpp @@ -1,3755 +1,3756 @@ /* ============================================================ * * 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 "albumpointer.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 Q_DECL_HIDDEN PAlbumPath { public: PAlbumPath() : albumRootId(-1) { } PAlbumPath(int albumRootId, const QString& albumPath) : albumRootId(albumRootId), albumPath(albumPath) { } explicit 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 Q_DECL_HIDDEN 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 Q_DECL_HIDDEN ChangingDB { public: explicit ChangingDB(AlbumManager::Private* const d) : d(d) { d->changingDB = true; } ~ChangingDB() { d->changingDB = false; } AlbumManager::Private* const d; }; // ----------------------------------------------------------------------------------- class Q_DECL_HIDDEN 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(const QList& albums) { if (albums.isEmpty()) return; d->currentAlbums.clear(); /** * Filter out the null pointers */ foreach (Album* const album, albums) { if (album) { d->currentAlbums << album; } } /** * Sort is needed to identify selection correctly, ex AlbumHistory */ std::sort(d->currentAlbums.begin(), d->currentAlbums.end()); 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(QLatin1Char('/'))) { 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->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->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(QLatin1Char('/'))) { 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[PAlbumPath(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(QLatin1Char('/'))) { 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->firstChild(); while (sibling) { if (sibling->title() == title) { return true; } sibling = sibling->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(QLatin1Char('/'))) { 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->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[PAlbumPath(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->firstChild(); PAlbum* toBeRemoved = 0; while (child) { Album* next = child->next(); toBeRemoved = static_cast(child); if (toBeRemoved) { removePAlbum(toBeRemoved); toBeRemoved = 0; } child = next; } emit signalAlbumAboutToBeDeleted(album); d->albumPathHash.remove(PAlbumPath(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->firstChild(); TAlbum* toBeRemoved = 0; while (child) { Album* next = child->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/album/albummanager.h b/core/libs/album/albummanager.h index a87f2d7c86..32caed7a45 100644 --- a/core/libs/album/albummanager.h +++ b/core/libs/album/albummanager.h @@ -1,915 +1,790 @@ /* ============================================================ * * 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 * * 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. * * ============================================================ */ /** @file albummanager.h */ #ifndef DIGIKAM_ALBUM_MANAGER_H #define DIGIKAM_ALBUM_MANAGER_H // Qt includes #include #include #include #include #include +#include // Local includes #include "album.h" #include "coredbalbuminfo.h" #include "dbengineparameters.h" #include "digikam_export.h" #include "imagelisterrecord.h" -class QDate; - namespace Digikam { class FAlbum; class CollectionLocation; class AlbumChangeset; class TagChangeset; class SearchChangeset; class CollectionImageChangeset; class ImageTagChangeset; /** * \class AlbumManager * * AlbumManager manages albums: does listing of albums and controls the lifetime of it. * For PAlbums and TAlbums, the listing is done by reading the db directly and * building the hierarchy of the albums. For DAlbums, since the listing takes * time, the work is delegated to a dbjob. Interested frontend entities can * connect to the albummanager to receive notifications of new Albums, when * Albums are deleted and when the current album is changed. * * Additional operations are provided for: creating/deleting/rename Albums, * updating icons and moving Albums. * */ class DIGIKAM_EXPORT AlbumManager : public QObject { Q_OBJECT public: /** * A convenience function to get the instance of the AlbumManager */ static AlbumManager* instance(); /** @name Library path And Scanning */ //@{ /** * Initialize. Informs the user about failures. * Returns true on success, false on failure. * A return value of false during startup indicates termination of the program * (user is informed) */ bool setDatabase(const DbEngineParameters& params, bool priority, const QString& suggestedAlbumRoot = QString()); /** * Some checks for settings done in first run wizard in case of QSlite Database. */ static void checkDatabaseDirsAfterFirstRun(const QString& dbPath, const QString& albumPath); /** * Sets new database when chosen by the user in setup. * Handles user notification about problems. * Call this instead of setDatabase when digiKam is up and running. */ void changeDatabase(const DbEngineParameters& params); /** * Stop ongoing operations, prepare for application shutdown */ void cleanUp(); /** * Checks if the given database path is equal to the current one */ bool databaseEqual(const DbEngineParameters& parameters) const; /** * starts scanning the libraryPath and listing the albums. If the * libraryPath has not changed since the last scan, then nothing happens * @see setLibraryPath * @see refresh */ void startScan(); /** * This is similar to startScan, except that it assumes you have run * startScan at least once. It checks the database to see if any new albums * have been added and updates them accordingly. Use this when a change in the * filesystem is detected (but the album library path hasn't changed) * @see startScan */ void refresh(); /** * Ensures that valid item counts for physical and tag albums are available */ void prepareItemCounts(); //@} /** @name List of Albums and current Album */ //@{ /** * @return a list of all PAlbums */ AlbumList allPAlbums() const; /** * @return a list of all TAlbums */ AlbumList allTAlbums() const; /** * @return a list of all SAlbums */ AlbumList allSAlbums() const; /** * @return a list of all DAlbums */ AlbumList allDAlbums() const; /** * @return a list of all FAlbums */ AlbumList allFAlbums() const; /** * set current album to @p albums. It's similar to setCurrentAlbum, * but supports multiple selected albums */ void setCurrentAlbums(const QList& albums); /** * @returns current albums, previously set up by setCurrentAlbums */ AlbumList currentAlbums() const; /** * @returns the current PAlbum or null if no one is selected */ PAlbum* currentPAlbum() const; /** * @returns the current TAlbum or null if no one is selected */ QList currentTAlbums() const; /** * @returns the current FAlbum or null if no one is selected */ FAlbum* currentFAlbum() const; //@} /** @name Finding Albums */ //@{ /** * Given a complete file url (kde url with file protocol), it will try to find * a PAlbum corresponding to it. * \warning This should not be used, unless really necessary * @return PAlbum corresponding to supplied @p url * @param url the url we need to check */ PAlbum* findPAlbum(const QUrl& url) const; /** * @return a PAlbum with given ID * @param id the id for the PAlbum */ PAlbum* findPAlbum(int id) const; /** * @return a TAlbum with given ID * @param id the id for the TAlbum */ TAlbum* findTAlbum(int id) const; /** * @return a SAlbum with given ID * @param id the id for the SAlbum */ SAlbum* findSAlbum(int id) const; /** * @return a DAlbum with given ID * @param id the id for the DAlbum */ DAlbum* findDAlbum(int id) const; /** * @return a FAlbum with given name * @param name the name for the FAlbum (name of the person which the FAlbum corresponds to */ FAlbum* findFAlbum(const QString& name) const; /** * @return a Album with the given globalID * @param gid the global id for the album */ Album* findAlbum(int gid) const; /** * @return a Album with the given type and id * @param id the id for the album (not the global id) */ Album* findAlbum(Album::Type type, int id) const; /** * @return a TAlbum with given tag path, or 0 if not found * @param tagPath the tag path ("People/Friend/John") */ TAlbum* findTAlbum(const QString& tagPath) const; /** * @return a SAlbum with given name, or 0 if not found * @param name the name of the search */ SAlbum* findSAlbum(const QString& name) const; /** * @return SAlbums with given type, empty list if not found * @param searchType the type of the search */ QList< SAlbum* > findSAlbumsBySearchType(int searchType) const; //@} /** @name Operations on PAlbum */ //@{ /** * Create a new PAlbum with supplied properties as a child of the parent * This is equivalent to creating a new folder on the disk with supplied * name in the parent's folder path. Also the supplied attributes are written * out to the database * \note the signalAlbumAdded will be fired before this function returns. Its * recommended to connect to that signal to get notification of new album added * @return the newly created PAlbum or 0 if it fails * @param parent The parent album under which to create the new Album. * Parent must not be root. Otherwise, use the other variants of this method. * If parent is root, the albumRootPath must be supplied. * @param name the name of the new album * @param caption the caption for the new album * @param date the date for the new album * @param errMsg this will contain the error message describing why the * operation failed */ PAlbum* createPAlbum(PAlbum* parent, const QString& name, const QString& caption, const QDate& date, const QString& category, QString& errMsg); /** * Overloaded method. Here you can supply an albumRootPath which must * correspond to an available collection location. */ PAlbum* createPAlbum(const QString& albumRootPath, const QString& name, const QString& caption, const QDate& date, const QString& category, QString& errMsg); /** * Overloaded method. Here you can supply a collection location (which * must be available). * * @param location the collection for the new album */ PAlbum* createPAlbum(const CollectionLocation& location, const QString& name, const QString& caption, const QDate& date, const QString& category, QString& errMsg); /** * Renames a PAlbum. This is equivalent to actually renaming the corresponding * folder on the disk. * @return true if the operation succeeds, false otherwise * @param album the Album which should be renamed * @param newName the new name for the album * @param errMsg this will contain the error message describing why the * operation failed */ bool renamePAlbum(PAlbum* album, const QString& newName, QString& errMsg); /** * Update the icon for an album. The @p icon is the name (and not full path) * of the file in the album * @return true if the operation succeeds, false otherwise * @param album the album for which icon should be changed * @param iconID the filename of the new icon * @param errMsg if the operation fails, this will contain the error message * describing why the operation failed */ bool updatePAlbumIcon(PAlbum* album, qlonglong iconID, QString& errMsg); /** * Returns the id of the item with the given filename in * the given PAlbum. * @param album The albumId in which we search the item. * @param fileName The name of the item file. * @return The item id or -1 if not existent. */ qlonglong getItemFromAlbum(PAlbum* album, const QString& fileName); //@} /** * @return A hash with the titles for all album IDs. */ QHash albumTitles() const; /** @name Operations on TAlbum */ //@{ /** * Create a new TAlbum with supplied properties as a child of the parent * The tag is added to the database * \note the signalAlbumAdded will be fired before this function returns. Its * recommended to connect to that signal to get notification of new album added * @return the newly created TAlbum or 0 if it fails * @param parent the parent album under which to create the new Album * @param name the name of the new album * @param iconkde the iconkde for the new album (this is a filename which * kde iconloader can load up * @param errMsg this will contain the error message describing why the * operation failed */ TAlbum* createTAlbum(TAlbum* parent, const QString& name, const QString& iconkde, QString& errMsg); /** * A list of tag paths is supplied. * If no corresponding TAlbum exists, a new one will be created. * @param tagPaths A list of tag paths * @returns A list of all TAlbums for the list (already existing or newly created) */ AlbumList findOrCreateTAlbums(const QStringList& tagPaths); /** * Delete a TAlbum. * The tag is removed from the database * \note the signalAlbumDeleted will be fired before this function returns. Its * recommended to connect to that signal to get notification of album deletes * @return true if the operation succeeds or false otherwise * @param album the TAlbum to delete * @param errMsg this will contain the error message describing why the * @param askUser ask user to write metadata to images * operation failed */ bool deleteTAlbum(TAlbum* album, QString& errMsg, bool askUser = true); /** * Renames a TAlbum. * This updates the tag name in the database * @return true if the operation succeeds, false otherwise * @param album the Album which should be renamed * @param name the new name for the album * @param errMsg this will contain the error message describing why the * operation failed */ bool renameTAlbum(TAlbum* album, const QString& name, QString& errMsg); /** * Move a TAlbum to a new parent. * This updates the tag parent ID in the database * @return true if the operation succeeds, false otherwise * @param album the Album which should be moved * @param newParent the Parent Album to which album should be moved * @param errMsg this will contain the error message describing why the * operation failed */ bool moveTAlbum(TAlbum* album, TAlbum* newParent, QString& errMsg); /** * Merge a TAlbum to a TAlbum. * This updates the image tags in the database * @return true if the operation succeeds, false otherwise * @param album the Album which should be merged * @param destAlbum the Album to which album should be merged * @param dialog show dialog to ask the user if he wants to merge * @param errMsg this will contain the error message describing why the * operation failed */ bool mergeTAlbum(TAlbum* album, TAlbum* destAlbum, bool dialog, QString& errMsg); /** * Update the icon for a TAlbum. * @return true if the operation succeeds, false otherwise * @param album the album for which icon should be changed * @param iconKDE a simple filename which can be loaded by KIconLoader * @param iconID id of the icon image file * @param errMsg this will contain the error message describing why the * operation failed * \note if iconKDE is not empty then iconID is used. So if you want to set * the icon to a file which can be loaded by KIconLoader, pass it in as * iconKDE. otherwise pass a null QString to iconKDE and set iconID */ bool updateTAlbumIcon(TAlbum* album, const QString& iconKDE, qlonglong iconID, QString& errMsg); /** * Get a list of recently assigned tags (only last 6 tags are listed) * @return the list of recently assigned TAlbums * @param includeInternal include internal tags in the returned list, or skip them */ AlbumList getRecentlyAssignedTags(bool includeInternal = false) const; /** * @return A list with the tag paths for a list of tag IDs. * @param tagIDs list of tag album IDs * @param leadingSlash if true return tags with a leading slash * @param includeInternal include internal tags in the returned list, or skip them */ QStringList tagPaths(const QList& tagIDs, bool leadingSlash=true, bool includeInternal = false) const; /** * @return A list with the tag names for a list of tag IDs. * @param tagIDs list of tag album IDs */ QStringList tagNames(const QList& tagIDs, bool includeInternal = false) const; /** * @return A hash with the tag paths for all tag IDs. */ QHash tagPaths(bool leadingSlash=true, bool includeInternal = false) const; /** * @return A hash with the tag names for all tag IDs. */ QHash tagNames(bool includeInternal = false) const; /** * Returns a list of TAlbums which have the given property, * or the given property/value combination. */ AlbumList findTagsWithProperty(const QString& property); AlbumList findTagsWithProperty(const QString& property, const QString& value); /** * TODO */ QList subTags(int tagId, bool recursive = false); //@} /** @name Operations on SAlbum */ //@{ /** * Create a new SAlbum with supplied url. If an existing SAlbum with same name * exists this function will return a pointer to that album, instead of creating * a new one. A newly created search album is added to the database. For an * existing SAlbum, the url is updated and written out to the database * \note the signalAlbumAdded will be fired before this function returns. Its * recommended to connect to that signal to get notification of new album added * @return the newly created SAlbum or an existing SAlbum with same name * @param name name for the new search * @param type the type of the search * @param query search query to use */ SAlbum* createSAlbum(const QString& name, DatabaseSearch::Type type, const QString& query); /** * Update the url for a SAlbum * @return true if the operation succeeds, false otherwise * @param album the album to update * @param changedQuery the new query data of the album * @param changedName a new name, or null to keep the current name * @param type a new type, or UndefinedType to keep the current type */ bool updateSAlbum(SAlbum* album, const QString& changedQuery, const QString& changedName = QString(), DatabaseSearch::Type type = DatabaseSearch::UndefinedType); /** * Delete a SAlbum from the database * \note the signalAlbumDeleted will be fired before this function returns. Its * recommended to connect to that signal to get notification of album deletes * @return true if the operation succeeds, false otherwise * @param album the album to delete */ bool deleteSAlbum(SAlbum* album); //@} /** @name Operations on TAlbum */ //@{ /** * Create a new FAlbum with supplied properties as a child of the parent * The person is added to the database * \note the signalAlbumAdded will be fired before this function returns. Its * recommended to connect to that signal to get notification of new album added * @return the newly created FAlbum or 0 if it fails * @param parent the parent album under which to create the new FAlbum * @param name the name of the new album * @param iconkde the iconkde for the new album (this is a filename which * kde iconloader can load up * @param errMsg this will contain the error message describing why the * operation failed */ FAlbum* createFAlbum(FAlbum* parent, const QString& name, const QString& iconkde, QString& errMsg); /** * Delete a FAlbum. * The person is removed from the database * \note the signalAlbumDeleted will be fired before this function returns. Its * recommended to connect to that signal to get notification of album deletes * @return true if the operation succeeds or false otherwise * @param album the FAlbum to delete * @param errMsg this will contain the error message describing why the * operation failed */ bool deleteFAlbum(FAlbum* album, QString& errMsg); /** * Renames a FAlbum. * This updates the person name in the database * @return true if the operation succeeds, false otherwise * @param album the Album which should be renamed * @param name the new name for the album * @param errMsg this will contain the error message describing why the * operation failed */ bool renameFAlbum(FAlbum* album, const QString& name, QString& errMsg); /** * Update the icon for a FAlbum. * @return true if the operation succeeds, false otherwise * @param album the album for which icon should be changed * @param iconKDE a simple filename which can be loaded by KIconLoader * @param iconID id of the icon image file * @param errMsg this will contain the error message describing why the * operation failed * \note if iconKDE is not empty then iconID is used. So if you want to set * the icon to a file which can be loaded by KIconLoader, pass it in as * iconKDE. otherwise pass a null QString to iconKDE and set iconID */ bool updateFAlbumIcon(FAlbum* album, const QString& iconKDE, qlonglong iconID, QString& errMsg); /** * @return A list with the name paths for a list of tag names. * @param names list of tag album names * @param leadingSlash if true return name paths with a leading slash */ QStringList namePaths(const QList& tagNames, bool leadingSlash=true) const; //@} /** @name Accessors to counting maps */ //@{ /** * Returns the latest count for PAlbums as also emitted via * signalPAlbumsDirty. * * @return count map for PAlbums */ QMap getPAlbumsCount() const; /** * Returns the latest count for TAlbums as also emitted via * signalTAlbumsDirty. * * @return count map for TAlbums */ QMap getTAlbumsCount() const; /** * Returns the latest count for DAlbums as also emitted via * signalDAlbumsDirty. * * @return count map for DAlbums */ QMap getDAlbumsCount() const; /** * Returns the latest count for faces as also emitted via * signalFaceCountsDirty. * * @return count map for faces (confirmed and unconfirmed combined) */ QMap getFaceCount() const; /** * Returns if the given album is currently being moved, that is, * if this album is in between signalAlbumAboutToBeMoved and * signalAlbumMoved. In this case, you can preserve state of such an album * because the object is guaranteed not to be deleted, even if * signalAlbumAboutToBeDeleted is emitted. */ bool isMovingAlbum(Album* album) const; bool isShowingOnlyAvailableAlbums() const; void setShowOnlyAvailableAlbums(bool onlyAvailable); void removeWatchedPAlbums(const PAlbum* const album); void addFakeConnection(); void removeFakeConnection(); //@} Q_SIGNALS: /// An album is about to be added to the given parent (0 if album is root) /// after the item given by prev (prev is 0 if parent has no children yet) void signalAlbumAboutToBeAdded(Album* album, Album* parent, Album* prev); /// The album has been added. void signalAlbumAdded(Album* album); /// The album is about to be deleted, but is still fully valid. void signalAlbumAboutToBeDeleted(Album* album); /// The album is deleted, but the object can still be accessed. void signalAlbumDeleted(Album* album); /// The album is deleted, the object can no longer be accessed. /// For identification purposes, the former album pointer is passed. void signalAlbumHasBeenDeleted(quintptr); void signalAlbumsCleared(); void signalAlbumCurrentChanged(const QList& albums); void signalAllAlbumsLoaded(); void signalAllDAlbumsLoaded(); void signalAlbumIconChanged(Album* album); void signalAlbumRenamed(Album* album); void signalAlbumNewPath(Album* album); void signalSearchUpdated(SAlbum* album); /// Indicates that an album is about to be moved. Signals for deleting and adding will be /// sent afterwards, but the album object is guaranteed not to be deleted until after signalAlbumMoved. void signalAlbumAboutToBeMoved(Album* album); /// Emitted when the album is moved to its new parent. After signalAlbumAboutToBeMoved, /// all four signals for first deleting and then adding will have been sent. void signalAlbumMoved(Album* album); void signalPAlbumsDirty(const QMap&); void signalTAlbumsDirty(const QMap&); void signalDAlbumsDirty(const QMap&); void signalFaceCountsDirty(const QMap&); void signalDatesMapDirty(const QMap&); void signalTagPropertiesChanged(TAlbum* album); void signalAlbumsUpdated(int type); void signalUpdateDuplicatesAlbums(const QList& modifiedAlbums, const QList& deletedImages); // Signals a change in this property. Please note that affected albums may appear or disappear after this signal has been emitted. void signalShowOnlyAvailableAlbumsChanged(bool showsOnlyAvailableAlbums); private Q_SLOTS: void slotDatesJobResult(); void slotDatesJobData(const QMap& datesStatMap); void slotAlbumsJobResult(); void slotAlbumsJobData(const QMap& albumsStatMap); void slotTagsJobResult(); void slotTagsJobData(const QMap& tagsStatMap); void slotPeopleJobResult(); void slotPeopleJobData(const QMap >& facesStatMap); void slotCollectionLocationStatusChanged(const CollectionLocation&, int); void slotCollectionLocationPropertiesChanged(const CollectionLocation& location); void slotAlbumChange(const AlbumChangeset& changeset); void slotTagChange(const TagChangeset& changeset); void slotSearchChange(const SearchChangeset& changeset); void slotCollectionImageChange(const CollectionImageChangeset& changeset); void slotImageTagChange(const ImageTagChangeset& changeset); void slotImagesDeleted(const QList& imageIds); /** * Scan albums directly from database and creates new PAlbums * It only creates those PAlbums which haven't already been * created. */ void scanPAlbums(); void updateChangedPAlbums(); /** * Scan tags directly from database and creates new TAlbums * It only creates those TAlbums which haven't already been * created. */ void scanTAlbums(); /** * Scan searches directly from database and creates new SAlbums * It only creates those SAlbums which haven't already been * created. */ void scanSAlbums(); /** * Scan dates from the database (via IOSlave) and * updates the DAlbums. */ void scanDAlbumsScheduled(); void scanDAlbums(); void getAlbumItemsCount(); void getTagItemsCount(); void tagItemsCount(); void personItemsCount(); private: friend class AlbumManagerCreator; AlbumManager(); ~AlbumManager(); /** * Checks whether an Album has a direct child with the given name. * * @param parent album to check children for * @param title title to search for * @return true if there is a child with name, else * false */ void askUserForWriteChangedTAlbumToFiles(TAlbum* const album); void askUserForWriteChangedTAlbumToFiles(const QList& imageIds); bool hasDirectChildAlbumWithTitle(Album* parent, const QString& title); bool handleCollectionStatusChange(const CollectionLocation& location, int oldStatus); void insertPAlbum(PAlbum* album, PAlbum* parent); void removePAlbum(PAlbum* album); void insertTAlbum(TAlbum* album, TAlbum* parent); void removeTAlbum(TAlbum* album); void updateAlbumPathHash(); void notifyAlbumDeletion(Album* album); void addAlbumRoot(const CollectionLocation& location); void removeAlbumRoot(const CollectionLocation& location); void addGuardedPointer(Album* a, Album** pointer); void removeGuardedPointer(Album* a, Album** pointer); void changeGuardedPointer(Album* oldAlbum, Album* a, Album** pointer); void invalidateGuardedPointers(Album* album); static AlbumManager* internalInstance; public: // Declared public because it used in ChangingDB class. class Private; private: Private* const d; template friend class AlbumPointer; friend class Album; }; -// ------------------------------------------------------------------------------------ - -/** - * You can use AlbumPointer to store a guarded pointer to Album - * or one of the subclasses (use template parameter). - * The pointer will be set to 0 when the album object is deleted. - */ -template - -class AlbumPointer -{ -public: - - AlbumPointer() - : album(0) - { - } - - // cppcheck-suppress noExplicitConstructor - AlbumPointer(T* const a) // krazy:exclude=explicit - : album(a) - { - AlbumManager::instance()->addGuardedPointer(album, &album); - } - - // cppcheck-suppress noExplicitConstructor - AlbumPointer(const AlbumPointer& p) // krazy:exclude=explicit - : album(p.album) - { - AlbumManager::instance()->addGuardedPointer(album, &album); - } - - ~AlbumPointer() - { - AlbumManager::instance()->removeGuardedPointer(album, &album); - } - - AlbumPointer& operator=(T* const a) - { - Album* const oldAlbum = album; - album = a; - AlbumManager::instance()->changeGuardedPointer(oldAlbum, album, &album); - return *this; - } - - AlbumPointer& operator=(const AlbumPointer& p) - { - Album* const oldAlbum = album; - album = p.album; - AlbumManager::instance()->changeGuardedPointer(oldAlbum, album, &album); - return *this; - } - - T* operator->() const - { - return static_cast(const_cast(album)); - } - - T& operator*() const - { - return *static_cast(const_cast(album)); - } - - operator T* () const - { - return static_cast(const_cast(album)); - } - - bool operator!() const - { - return !album; - } - -private: - - friend class AlbumManager; - Album* album; -}; - -// ------------------------------------------------------------------------------------ - -template - -class AlbumPointerList : public QList > -{ -public: - - AlbumPointerList() - { - } - - explicit AlbumPointerList(const AlbumPointerList& list) - : QList >(list) - { - } - - explicit AlbumPointerList(const QList& list) - { - operator=(list); - } - - AlbumPointerList& operator=(const AlbumPointerList& list) - { - return QList >::operator=(list); - } - - AlbumPointerList& operator=(const QList& list) - { - foreach(T* const t, list) - { - this->append(AlbumPointer(t)); - } - - return *this; - } -}; - } // namespace Digikam -Q_DECLARE_METATYPE(Digikam::AlbumPointer<>) -Q_DECLARE_METATYPE(Digikam::AlbumPointer) -Q_DECLARE_METATYPE(Digikam::AlbumPointer) -Q_DECLARE_METATYPE(Digikam::AlbumPointer) -Q_DECLARE_METATYPE(Digikam::AlbumPointer) -Q_DECLARE_METATYPE(QList) - #endif // DIGIKAM_ALBUM_MANAGER_H diff --git a/core/libs/album/albummodificationhelper.cpp b/core/libs/album/albummodificationhelper.cpp index e02ca2c590..155aaebef1 100644 --- a/core/libs/album/albummodificationhelper.cpp +++ b/core/libs/album/albummodificationhelper.cpp @@ -1,345 +1,346 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2000-12-05 * Description : helper class used to modify physical albums in views * * Copyright (C) 2009-2011 by Johannes Wienke * Copyright (C) 2014-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 "albummodificationhelper.h" // Qt includes #include #include #include #include #include // KDE includes #include // Local includes #include "digikam_debug.h" #include "albummanager.h" #include "albumpropsedit.h" +#include "albumpointer.h" #include "applicationsettings.h" #include "collectionmanager.h" #include "deletedialog.h" #include "dio.h" #include "digikamview.h" #include "digikamapp.h" #include "coredb.h" #include "coredbaccess.h" namespace Digikam { class Q_DECL_HIDDEN AlbumModificationHelper::Private { public: explicit Private() : dialogParent(0) { } QWidget* dialogParent; }; AlbumModificationHelper::AlbumModificationHelper(QObject* const parent, QWidget* const dialogParent) : QObject(parent), d(new Private) { d->dialogParent = dialogParent; } AlbumModificationHelper::~AlbumModificationHelper() { delete d; } void AlbumModificationHelper::bindAlbum(QAction* const action, PAlbum* const album) const { action->setData(QVariant::fromValue(AlbumPointer(album))); } PAlbum* AlbumModificationHelper::boundAlbum(QObject* const sender) const { QAction* action = 0; if ( (action = qobject_cast(sender)) ) { return action->data().value >(); } return 0; } PAlbum* AlbumModificationHelper::slotAlbumNew() { return slotAlbumNew(boundAlbum(sender())); } PAlbum* AlbumModificationHelper::slotAlbumNew(PAlbum* parent) { if (!parent) { qCWarning(DIGIKAM_GENERAL_LOG) << "No parent album given"; return 0; } ApplicationSettings* settings = ApplicationSettings::instance(); if (!settings) { qCWarning(DIGIKAM_GENERAL_LOG) << "could not get Album Settings"; return 0; } /* QDir libraryDir(settings->getAlbumLibraryPath()); if(!libraryDir.exists()) { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), i18n("The album library has not been set correctly.\n" "Select \"Configure Digikam\" from the Settings " "menu and choose a folder to use for the album " "library.")); return; } */ // if we create an album under root, need to supply the album root path. QString albumRootPath; albumRootPath = CollectionManager::instance()->oneAlbumRootPath(); QString title; QString comments; QString category; QDate date; QStringList albumCategories; int parentSelector; if (!AlbumPropsEdit::createNew(parent, title, comments, date, category, albumCategories, parentSelector)) { return 0; } QStringList oldAlbumCategories(ApplicationSettings::instance()->getAlbumCategoryNames()); if (albumCategories != oldAlbumCategories) { ApplicationSettings::instance()->setAlbumCategoryNames(albumCategories); } QString errMsg; PAlbum* album = 0; if (parent->isRoot() || parentSelector == 1) { album = AlbumManager::instance()->createPAlbum(albumRootPath, title, comments, date, category, errMsg); } else { album = AlbumManager::instance()->createPAlbum(parent, title, comments, date, category, errMsg); } if (!album) { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), errMsg); return 0; } return album; } void AlbumModificationHelper::slotAlbumDelete() { slotAlbumDelete(boundAlbum(sender())); } void AlbumModificationHelper::slotAlbumDelete(PAlbum* album) { if (!album || album->isRoot() || album->isAlbumRoot()) { return; } // find subalbums QList childrenList; addAlbumChildrenToList(childrenList, album); DeleteDialog dialog(d->dialogParent); // All subalbums will be presented in the list as well if (!dialog.confirmDeleteList(childrenList, childrenList.size() == 1 ? DeleteDialogMode::Albums : DeleteDialogMode::Subalbums, DeleteDialogMode::UserPreference)) { return; } bool useTrash = !dialog.shouldDelete(); DIO::del(album, useTrash); } void AlbumModificationHelper::slotAlbumRename() { slotAlbumRename(boundAlbum(sender())); } void AlbumModificationHelper::slotAlbumRename(PAlbum* album) { if (!album) { return; } QString oldTitle(album->title()); bool ok; QString title = QInputDialog::getText(d->dialogParent, i18n("Rename Album (%1)", oldTitle), i18n("Enter new album name:"), QLineEdit::Normal, oldTitle, &ok); if (!ok) { return; } if (title != oldTitle) { QString errMsg; if (!AlbumManager::instance()->renamePAlbum(album, title, errMsg)) { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), errMsg); } } } void AlbumModificationHelper::addAlbumChildrenToList(QList& list, Album* const album) { // simple recursive helper function if (album) { if (!list.contains(album->databaseUrl())) { list.append(album->databaseUrl()); } AlbumIterator it(album); while (it.current()) { addAlbumChildrenToList(list, *it); ++it; } } } void AlbumModificationHelper::slotAlbumEdit() { slotAlbumEdit(boundAlbum(sender())); } void AlbumModificationHelper::slotAlbumEdit(PAlbum* album) { if (!album || album->isRoot() || album->isAlbumRoot()) { return; } QString oldTitle(album->title()); QString oldComments(album->caption()); QString oldCategory(album->category()); QDate oldDate(album->date()); QStringList oldAlbumCategories(ApplicationSettings::instance()->getAlbumCategoryNames()); QString title, comments, category; QDate date; QStringList albumCategories; if (AlbumPropsEdit::editProps(album, title, comments, date, category, albumCategories)) { if (comments != oldComments) { album->setCaption(comments); } if (date != oldDate && date.isValid()) { album->setDate(date); } if (category != oldCategory) { album->setCategory(category); } ApplicationSettings::instance()->setAlbumCategoryNames(albumCategories); // Do this last : so that if anything else changed we can // successfully save to the db with the old name if (title != oldTitle) { QString errMsg; if (!AlbumManager::instance()->renamePAlbum(album, title, errMsg)) { QMessageBox::critical(d->dialogParent, qApp->applicationName(), errMsg); } } // Resorting the tree View after changing metadata DigikamApp::instance()->view()->slotSortAlbums(ApplicationSettings::instance()->getAlbumSortRole()); } } void AlbumModificationHelper::slotAlbumResetIcon(PAlbum* album) { if (!album) { return; } QString err; AlbumManager::instance()->updatePAlbumIcon(album, 0, err); } void AlbumModificationHelper::slotAlbumResetIcon() { slotAlbumResetIcon(boundAlbum(sender())); } } // namespace Digikam diff --git a/core/libs/album/albumpointer.h b/core/libs/album/albumpointer.h new file mode 100644 index 0000000000..6e2892ef4d --- /dev/null +++ b/core/libs/album/albumpointer.h @@ -0,0 +1,165 @@ +/* ============================================================ + * + * This file is a part of digiKam project + * http://www.digikam.org + * + * Date : 2004-06-15 + * Description : Albums manager interface. + * + * Copyright (C) 2006-2018 by Gilles Caulier + * Copyright (C) 2006-2011 by Marcel Wiesweg + * + * This program is free software; you can redistribute it + * and/or modify it under the terms of the GNU General + * Public License as published by the Free Software Foundation; + * either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * ============================================================ */ + +#ifndef DIGIKAM_ALBUM_POINTER_H +#define DIGIKAM_ALBUM_POINTER_H + +// Qt includes + +#include +#include + +// Local includes + +#include "album.h" +#include "albummanager.h" + +namespace Digikam +{ + +/** + * You can use AlbumPointer to store a guarded pointer to Album + * or one of the subclasses (use template parameter). + * The pointer will be set to 0 when the album object is deleted. + */ +template + +class AlbumPointer +{ +public: + + AlbumPointer() + : album(0) + { + } + + // cppcheck-suppress noExplicitConstructor + AlbumPointer(T* const a) // krazy:exclude=explicit + : album(a) + { + AlbumManager::instance()->addGuardedPointer(album, &album); + } + + // cppcheck-suppress noExplicitConstructor + AlbumPointer(const AlbumPointer& p) // krazy:exclude=explicit + : album(p.album) + { + AlbumManager::instance()->addGuardedPointer(album, &album); + } + + ~AlbumPointer() + { + AlbumManager::instance()->removeGuardedPointer(album, &album); + } + + AlbumPointer& operator=(T* const a) + { + Album* const oldAlbum = album; + album = a; + AlbumManager::instance()->changeGuardedPointer(oldAlbum, album, &album); + return *this; + } + + AlbumPointer& operator=(const AlbumPointer& p) + { + Album* const oldAlbum = album; + album = p.album; + AlbumManager::instance()->changeGuardedPointer(oldAlbum, album, &album); + return *this; + } + + T* operator->() const + { + return static_cast(const_cast(album)); + } + + T& operator*() const + { + return *static_cast(const_cast(album)); + } + + operator T* () const + { + return static_cast(const_cast(album)); + } + + bool operator!() const + { + return !album; + } + +private: + + friend class AlbumManager; + Album* album; +}; + +// ------------------------------------------------------------------------------------ + +template + +class AlbumPointerList : public QList > +{ +public: + + AlbumPointerList() + { + } + + explicit AlbumPointerList(const AlbumPointerList& list) + : QList >(list) + { + } + + explicit AlbumPointerList(const QList& list) + { + operator=(list); + } + + AlbumPointerList& operator=(const AlbumPointerList& list) + { + return QList >::operator=(list); + } + + AlbumPointerList& operator=(const QList& list) + { + foreach(T* const t, list) + { + this->append(AlbumPointer(t)); + } + + return *this; + } +}; + +} // namespace Digikam + +Q_DECLARE_METATYPE(Digikam::AlbumPointer<>) +Q_DECLARE_METATYPE(Digikam::AlbumPointer) +Q_DECLARE_METATYPE(Digikam::AlbumPointer) +Q_DECLARE_METATYPE(Digikam::AlbumPointer) +Q_DECLARE_METATYPE(Digikam::AlbumPointer) +Q_DECLARE_METATYPE(QList) + +#endif // DIGIKAM_ALBUM_POINTER_H diff --git a/core/libs/album/albumtreeview.h b/core/libs/album/albumtreeview.h index b51daf23ad..16feab99f4 100644 --- a/core/libs/album/albumtreeview.h +++ b/core/libs/album/albumtreeview.h @@ -1,638 +1,639 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-03-25 * Description : Tree View for album models * * Copyright (C) 2009-2010 by Marcel Wiesweg * Copyright (C) 2010-2011 by Andi Clemens * Copyright (C) 2009-2018 by Gilles Caulier * * This program is free software; you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; * either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #ifndef DIGIKAM_ALBUM_TREE_VIEW_H #define DIGIKAM_ALBUM_TREE_VIEW_H // Qt includes #include // Local includes #include "albummanager.h" #include "albummodel.h" #include "albumfiltermodel.h" +#include "albumpointer.h" #include "statesavingobject.h" namespace Digikam { class ContextMenuHelper; class TagModificationHelper; // NOTE: This structure name can be in conflict with QAbstractItemView::State. struct State; class AbstractAlbumTreeView; /** * Base class for all tree views that display Album-based content provided by an * AbstractSpecificAlbumModel. This class enables various utility functions like * selecting albums on mouse actions or providing an infrastructure for * displaying a context menu for albums. * * Context menu handling is implemented as template methods with hook methods * that can be implemented by subclasses to provide a custom behavior. In * default mode no context menu is shown at all. It must be enabled via a call * to setEnableContextMenu. */ class AbstractAlbumTreeView : public QTreeView, public StateSavingObject { Q_OBJECT public: enum Flag { /** Create a default model. Not supported by abstract classes. Not part of default flags! */ CreateDefaultModel, /** Create a default filter model. */ CreateDefaultFilterModel, /** Create a delegate which paints according to settings. * If not set, the Qt default delegate of the view is used. */ CreateDefaultDelegate, /** Show the count according to the settings. If not set, call * setShowCount() on the model yourself. */ ShowCountAccordingToSettings, DefaultFlags = CreateDefaultFilterModel | CreateDefaultDelegate | ShowCountAccordingToSettings }; Q_DECLARE_FLAGS(Flags, Flag) public: /** * Constructs an album tree view. * If you give 0 for model, call setAlbumModel afterwards. * If you supply 0 for filterModel, call setAlbumFilterModel afterwards. */ AbstractAlbumTreeView(QWidget* const parent, Flags flags); ~AbstractAlbumTreeView(); AbstractSpecificAlbumModel* albumModel() const; AlbumFilterModel* albumFilterModel() const; /// Enable expanding of tree items on single click on the item (default: off) void setExpandOnSingleClick(const bool doThat); /// Expand an item when making it the new current item void setExpandNewCurrentItem(const bool doThat); /** * Sets whether to select an album on click via the album manager or not. * * @param selectOnClick if true, a click on an item automatically sets this * item as the current album in the album manager */ void setSelectAlbumOnClick(const bool selectOnClick); /** * Determines the global decision to show a popup menu or not. More detailed * decision at which position a menu can be shown and where not can be made * by implementing showContextMenuAt. * * @param enable if true, a context menu can be shown */ void setEnableContextMenu(const bool enable); /** * Sets whether to select the album under the mouse cursor on a context menu * request (so that the album is shown using the album manager) or not * * Defaults to true. * * @param select true if a context menu request shall select the album */ void setSelectOnContextMenu(const bool select); /** * Set the context menu title and icon. * This is used by the default implementation of contextMenuIcon() * and contextMenuTitle(). You can alternatively reimplement these methods. */ void setContextMenuIcon(const QPixmap& pixmap); void setContextMenuTitle(const QString& title); /** * This is a combination of indexAt() checked with visualRect(). * p must be in the viewport currently. Decoration will not be included. * Suitable for mouse click positions. */ QModelIndex indexVisuallyAt(const QPoint& p); /** * Ensures that every current match is visible by expanding all parent * entries. * * @param index index to start ensuring expansion state * @return true if there was a match under index. * This return value can normally be ignored by the caller because * it is only used for an internal recursion. */ bool expandMatches(const QModelIndex& index); /** * Implements state loading for the album tree view in a somewhat clumsy * procedure because the model may not be fully loaded when this method is * called. Therefore the config is first parsed into d->statesByAlbumId * which holds the state of all tree view entries indexed by album id. * Afterwards an initial sync run is done restoring the state of all model * entries that are already present at this time. Every processed entry * is removed from d->statesByAlbumId. If there are still entries left in * this map we assume that the model is not fully loaded at the moment. * Therefore the rowsInserted signal is connected to a slot that restores * the state of new rows based on the remaining entries in * d->statesByAlbumId. */ virtual void doLoadState(); virtual void doSaveState(); /** * Some treeviews shall control the global current album kept by AlbumManager. * Other treeview are self-contained and shall not change the current album. * Default: false */ void setAlbumManagerCurrentAlbum(const bool setCurrentAlbum); /** * Add a context menu element which can add actions to the context * menu when the menu is generated. * First, addCustomContextMenuActions is called, then * all elements' addActions method is called in order of addition. */ class ContextMenuElement { public: virtual ~ContextMenuElement() { } /** * Add actions to the context menu being generated * * @param view The AbstractAlbumTreeView which generates the menu * @param cmh helper object to create the context menu * @param album album on which the context menu will be created. May be null if * it is requested on no tag entry */ virtual void addActions(AbstractAlbumTreeView* view, ContextMenuHelper& cmh, Album* album) = 0; }; void addContextMenuElement(ContextMenuElement* const element); void removeContextMenuElement(ContextMenuElement* const element); QList contextMenuElements() const; template QList currentAlbums(); // for internal use: public viewportEvent virtual bool viewportEvent(QEvent* event); /** * @brief selectedItems() - */ QList selectedItems(); public Q_SLOTS: void setSearchTextSettings(const SearchTextSettings& settings); /** * Selects the given album. * * @param album album to select * @param selectInAlbumManager the album will be set as current album, if both * this parameter is true and setAlbumManagerCurrentAlbum() was set to true. */ void setCurrentAlbums(const QList& albums, bool selectInAlbumManager = true); /** * Adapt the column sizes to the contents of the tree view. */ void adaptColumnsToContent(); /** * Scrolls to the first selected album if there is one. */ void scrollToSelectedAlbum(); /** * Expands the complete tree under the given index. * * @param index index to start expanding everything */ void expandEverything(const QModelIndex& index); Q_SIGNALS: /// Emitted when the currently selected album changes void currentAlbumChanged(Album* currentAlbum); /// Emitted when the current selection changes. Use currentChanged unless in multi-selection mode. void selectedAlbumsChanged(const QList& selectedAlbums); protected Q_SLOTS: // override if implemented behavior is not as intended virtual void slotRootAlbumAvailable(); void slotSearchTextSettingsChanged(bool wasSearching, bool searching); void slotSearchTextSettingsAboutToChange(bool searched, bool willSearch); void slotCurrentChanged(); void slotSelectionChanged(); void albumSettingsChanged(); protected: // context menu handling /** * Hook method to implement that determines if a context menu shall be * displayed for the given event at the position coded in the event. * * @param event context menu event to react on * @param albumForEvent the album at the mouse position or null if there is * no album at that position * @return true if a context menu shall be displayed at the event * coordinates, else false */ virtual bool showContextMenuAt(QContextMenuEvent* event, Album* albumForEvent); /** * Hook method that can be implemented to return a special icon used for the * context menu. * * @return the icon for the context menu */ virtual QPixmap contextMenuIcon() const; /** * Hook method to implement that returns the title for the context menu. * * @return title for the context menu */ virtual QString contextMenuTitle() const; /** * Hook method to add custom actions to the generated context menu. * * @param cmh helper object to create the context menu * @param album tag on which the context menu will be created. May be null if * it is requested on no tag entry */ virtual void addCustomContextMenuActions(ContextMenuHelper& cmh, Album* album); /** * Hook method to handle the custom context menu actions that were added * with addCustomContextMenuActions. * * @param action the action that was chosen by the user, may be null if none * of the custom actions were selected * @param album the tag on which the context menu was requested. May be null * if there was no */ virtual void handleCustomContextMenuAction(QAction* action, AlbumPointer album); // other stuff void mousePressEvent(QMouseEvent* e); void rowsInserted(const QModelIndex& index, int start, int end); void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); void startDrag(Qt::DropActions supportedActions); void dragEnterEvent(QDragEnterEvent* e); void dragMoveEvent(QDragMoveEvent* e); void dragLeaveEvent(QDragLeaveEvent* e); void dropEvent(QDropEvent* e); virtual void middleButtonPressed(Album* a); virtual QPixmap pixmapForDrag(const QStyleOptionViewItem& option, QList indexes); void setAlbumFilterModel(AlbumFilterModel* const filterModel); void setAlbumModel(AbstractSpecificAlbumModel* const model); protected: AbstractSpecificAlbumModel* m_albumModel; AlbumFilterModel* m_albumFilterModel; AlbumModelDragDropHandler* m_dragDropHandler; bool m_checkOnMiddleClick; bool m_restoreCheckState; Flags m_flags; private: void saveStateRecursive(const QModelIndex& index, QList& selection, QList& expansion); /** * Restores the state of the index and all sub-indexes if there is an entry * for this index in stateStore. Every album that is restored is removed * from the stateStore. * * @param index index to start restoring * @param stateStore states indexed by album id */ void restoreStateForHierarchy(const QModelIndex& index, const QMap& stateStore); /** * Restore the state for this index. */ void restoreState(const QModelIndex& index, const QMap& stateStore); /** * Creates the context menu. * * @param event event that requested the menu */ void contextMenuEvent(QContextMenuEvent* event); private Q_SLOTS: /** * Adapts the columns in between the given model indices to the content * size. This can be connected to dataChanged. * * @param topLeft top left index of changed data * @param bottomRight index of changed data */ void adaptColumnsOnDataChange(const QModelIndex& topLeft, const QModelIndex& bottomRight); /** * Adapt the column sizes to new contents. This can be connected to all * signals indicating row changes. * * @param parent parent index of changed rows * @param start start row changed under the parent * @param end end row changed under the parent */ void adaptColumnsOnRowChange(const QModelIndex& parent, int start, int end); /** * Adapts the column sizes if the layout changes. */ void adaptColumnsOnLayoutChange(); /** * This slot is used to ensure that after searching for entries the correct * album is selected again. Therefore it tracks new selections. */ void currentAlbumChangedForBackupSelection(Album* currentAlbum); private: class Private; Private* d; }; // ------------------------------------------------------------------------------------- class AbstractCountingAlbumTreeView : public AbstractAlbumTreeView { Q_OBJECT public: explicit AbstractCountingAlbumTreeView(QWidget* const parent, Flags flags); protected: void setAlbumModel(AbstractCountingAlbumModel* const model); void setAlbumFilterModel(AlbumFilterModel* const filterModel); virtual void rowsInserted(const QModelIndex& parent, int start, int end); private Q_SLOTS: void slotCollapsed(const QModelIndex& index); void slotExpanded(const QModelIndex& index); void setShowCountFromSettings(); void updateShowCountState(const QModelIndex& index, bool recurse); private: void init(); }; // ------------------------------------------------------------------------------------- class AbstractCheckableAlbumTreeView : public AbstractCountingAlbumTreeView { Q_OBJECT public: /// Models of these view _can_ be checkable, they need _not_. You need to enable it on the model. explicit AbstractCheckableAlbumTreeView(QWidget* const parent, Flags flags); virtual ~AbstractCheckableAlbumTreeView(); /// Manage check state through the model directly AbstractCheckableAlbumModel* albumModel() const; CheckableAlbumFilterModel* albumFilterModel() const; AbstractCheckableAlbumModel* checkableModel() const { return albumModel(); } CheckableAlbumFilterModel* checkableAlbumFilterModel() const { return albumFilterModel(); } /// Enable checking on middle mouse button click (default: on) void setCheckOnMiddleClick(bool doThat); /** * Tells if the check state is restored while loading / saving state. * * @return true if restoring check state is active */ bool isRestoreCheckState() const; /** * Set whether to restore check state or not. * * @param restore if true, restore check state */ void setRestoreCheckState(bool restore); virtual void doLoadState(); virtual void doSaveState(); protected: virtual void middleButtonPressed(Album* a); virtual void rowsInserted(const QModelIndex& parent, int start, int end); private: void restoreCheckStateForHierarchy(const QModelIndex& index); void restoreCheckState(const QModelIndex& index); private: class Private; Private* d; }; // ------------------------------------------------------------------------------------- class AlbumTreeView : public AbstractCheckableAlbumTreeView { Q_OBJECT public: explicit AlbumTreeView(QWidget* const parent = 0, Flags flags = DefaultFlags); virtual ~AlbumTreeView(); AlbumModel* albumModel() const; PAlbum* currentAlbum() const; PAlbum* albumForIndex(const QModelIndex& index) const; void setAlbumFilterModel(CheckableAlbumFilterModel* const filterModel); void setAlbumModel(AlbumModel* const model); public Q_SLOTS: void setCurrentAlbums(const QList& albums, bool selectInAlbumManager = true); void setCurrentAlbum(int albumId, bool selectInAlbumManager = true); }; // ------------------------------------------------------------------------------------- class TagTreeView : public AbstractCheckableAlbumTreeView { Q_OBJECT public: explicit TagTreeView(QWidget* const parent = 0, Flags flags = DefaultFlags); ~TagTreeView(); TagModel* albumModel() const; /// Contains only the tags filtered by properties - prefer to albumModel() TagPropertiesFilterModel* filteredModel() const; /** * @brief currentAlbum - even if multiple selection is enabled current * Album can be only one, the last clicked item * if you need selected items, see selectedAlbums() * It's NOT the same as AlbumManager::currentAlbums() */ TAlbum* currentAlbum() const; /** * @brief selectedTags - return a list of all selected items in tag model */ QList selectedTags(); QList selectedTagAlbums(); TAlbum* albumForIndex(const QModelIndex& index) const; TagModificationHelper* tagModificationHelper() const; void setAlbumFilterModel(TagPropertiesFilterModel* const filteredModel, CheckableAlbumFilterModel* const filterModel); void setAlbumModel(TagModel* const model); public Q_SLOTS: void setCurrentAlbums(const QList& tags, bool selectInAlbumManager = true); void setCurrentAlbum(int tagId, bool selectInAlbumManager = true); Q_SIGNALS: void assignTags(int tagId, const QList& imageIDs); protected: TagPropertiesFilterModel* m_filteredModel; TagModificationHelper* m_modificationHelper; }; // ------------------------------------------------------------------------------------- class SearchTreeView : public AbstractCheckableAlbumTreeView { Q_OBJECT public: explicit SearchTreeView(QWidget* const parent = 0, Flags flags = DefaultFlags); ~SearchTreeView(); /// Note: not filtered by search type SearchModel* albumModel() const; /// Contains only the searches with appropriate type - prefer to albumModel() SearchFilterModel* filteredModel() const; SAlbum* currentAlbum() const; void setAlbumModel(SearchModel* const model); void setAlbumFilterModel(SearchFilterModel* const filteredModel, CheckableAlbumFilterModel* const model); public Q_SLOTS: void setCurrentAlbums(const QList& albums, bool selectInAlbumManager = true); void setCurrentAlbum(int searchId, bool selectInAlbumManager = true); protected: SearchFilterModel* m_filteredModel; }; // ------------------------------------------------------------------------------------- class DateAlbumTreeView : public AbstractCountingAlbumTreeView { Q_OBJECT public: explicit DateAlbumTreeView(QWidget* const parent = 0, Flags flags = DefaultFlags); DateAlbumModel* albumModel() const; DAlbum* currentAlbum() const; DAlbum* albumForIndex(const QModelIndex& index) const; void setAlbumModel(DateAlbumModel* const model); void setAlbumFilterModel(AlbumFilterModel* const filterModel); public Q_SLOTS: void setCurrentAlbums(const QList& albums, bool selectInAlbumManager = true); void setCurrentAlbum(int dateId, bool selectInAlbumManager = true); }; } // namespace Digikam Q_DECLARE_OPERATORS_FOR_FLAGS(Digikam::AbstractAlbumTreeView::Flags) #endif // DIGIKAM_ALBUM_TREE_VIEW_H diff --git a/core/libs/tags/tagmodificationhelper.cpp b/core/libs/tags/tagmodificationhelper.cpp index bdc82a57ab..ab600d6b97 100644 --- a/core/libs/tags/tagmodificationhelper.cpp +++ b/core/libs/tags/tagmodificationhelper.cpp @@ -1,710 +1,711 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2000-12-05 * Description : helper class used to modify tag albums in views * * Copyright (C) 2009-2010 by Johannes Wienke * Copyright (C) 2010-2018 by Gilles Caulier * * This program is free software; you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; * either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #include "tagmodificationhelper.h" // Qt includes #include #include #include // KDE includes #include // Local includes #include "digikam_debug.h" #include "album.h" +#include "albumpointer.h" #include "coredb.h" #include "coredbtransaction.h" #include "imageinfo.h" #include "imagetagpair.h" #include "metadatahub.h" #include "scancontroller.h" #include "statusprogressbar.h" #include "tagsactionmngr.h" #include "tagproperties.h" #include "tageditdlg.h" #include "facetags.h" #include "facedbaccess.h" #include "facedb.h" namespace Digikam { class Q_DECL_HIDDEN TagModificationHelper::Private { public: explicit Private() { parentTag = 0; dialogParent = 0; } AlbumPointer parentTag; QWidget* dialogParent; }; TagModificationHelper::TagModificationHelper(QObject* const parent, QWidget* const dialogParent) : QObject(parent), d(new Private) { d->dialogParent = dialogParent; } TagModificationHelper::~TagModificationHelper() { delete d; } void TagModificationHelper::bindTag(QAction* action, TAlbum* album) const { action->setData(QVariant::fromValue(AlbumPointer(album))); } TAlbum* TagModificationHelper::boundTag(QObject* sender) const { QAction* action = 0; if ( (action = qobject_cast(sender)) ) { return action->data().value >(); } return 0; } void TagModificationHelper::bindMultipleTags(QAction* action, QList tags) { action->setData(QVariant::fromValue(tags)); } QList TagModificationHelper::boundMultipleTags(QObject* sender) { QAction* action = 0; if ((action = qobject_cast(sender))) { return (action->data().value >()); } return QList(); } TAlbum* TagModificationHelper::slotTagNew(TAlbum* parent, const QString& title, const QString& iconName) { // ensure that there is a parent AlbumPointer p(parent); if (!p) { p = AlbumManager::instance()->findTAlbum(0); if (!p) { qCDebug(DIGIKAM_GENERAL_LOG) << "Could not find root tag album"; return 0; } } QString editTitle = title; QString editIconName = iconName; QKeySequence ks; if (title.isEmpty()) { bool doCreate = TagEditDlg::tagCreate(d->dialogParent, p, editTitle, editIconName, ks); if (!doCreate || !p) { return 0; } } QMap errMap; AlbumList tList = TagEditDlg::createTAlbum(p, editTitle, editIconName, ks, errMap); TagEditDlg::showtagsListCreationError(d->dialogParent, errMap); if (errMap.isEmpty() && !tList.isEmpty()) { TAlbum* const tag = static_cast(tList.last()); emit tagCreated(tag); return tag; } else { return 0; } } TAlbum* TagModificationHelper::slotTagNew() { return slotTagNew(boundTag(sender())); } void TagModificationHelper::slotTagEdit(TAlbum* t) { if (!t) { return; } AlbumPointer tag(t); QString title, icon; QKeySequence ks; bool doEdit = TagEditDlg::tagEdit(d->dialogParent, tag, title, icon, ks); if (!doEdit || !tag) { return; } if (tag && tag->title() != title) { QString errMsg; if (AlbumManager::instance()->renameTAlbum(tag, title, errMsg)) { // TODO: make an option to edit the full name of a face tag if (FaceTags::isPerson(tag->id())) { TagProperties props(tag->id()); props.setProperty(TagPropertyName::person(), title); props.setProperty(TagPropertyName::faceEngineName(), title); } } else { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), errMsg); } } if (tag && tag->icon() != icon) { QString errMsg; if (!AlbumManager::instance()->updateTAlbumIcon(tag, icon, 0, errMsg)) { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), errMsg); } } if (tag && tag->property(TagPropertyName::tagKeyboardShortcut()) != ks.toString()) { TagsActionMngr::defaultManager()->updateTagShortcut(tag->id(), ks); } emit tagEdited(tag); } void TagModificationHelper::slotTagEdit() { slotTagEdit(boundTag(sender())); } void TagModificationHelper::slotTagDelete(TAlbum* t) { if (!t || t->isRoot()) { return; } AlbumPointer tag(t); // find number of subtags int children = 0; AlbumIterator iter(tag); while (iter.current()) { ++children; ++iter; } // ask for deletion of children if (children) { int result = QMessageBox::warning(d->dialogParent, qApp->applicationName(), i18np("Tag '%2' has one subtag. " "Deleting this will also delete " "the subtag.\n" "Do you want to continue?", "Tag '%2' has %1 subtags. " "Deleting this will also delete " "the subtags.\n" "Do you want to continue?", children, tag->title()), QMessageBox::Yes | QMessageBox::Cancel); if (result != QMessageBox::Yes || !tag) { return; } } QString message; QList assignedItems = CoreDbAccess().db()->getItemIDsInTag(tag->id()); if (!assignedItems.isEmpty()) { message = i18np("Tag '%2' is assigned to one item. " "Do you want to continue?", "Tag '%2' is assigned to %1 items. " "Do you want to continue?", assignedItems.count(), tag->title()); } else { message = i18n("Delete '%1' tag?", tag->title()); } int result = QMessageBox::warning(qApp->activeWindow(), i18n("Delete Tag"), message, QMessageBox::Yes | QMessageBox::Cancel); if (result == QMessageBox::Yes && tag) { emit aboutToDeleteTag(tag); QString errMsg; if (!AlbumManager::instance()->deleteTAlbum(tag, errMsg)) { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), errMsg); } } } void TagModificationHelper::slotTagDelete() { slotTagDelete(boundTag(sender())); } void TagModificationHelper::slotMultipleTagDel(QList& tags) { QString tagWithChildrens; QString tagWithoutImages; QString tagWithImages; QMultiMap sortedTags; foreach(TAlbum* const t, tags) { if (!t || t->isRoot()) { continue; } AlbumPointer tag(t); // find number of subtags int children = 0; AlbumIterator iter(tag); while (iter.current()) { ++children; ++iter; } if (children) tagWithChildrens.append(tag->title() + QLatin1Char(' ')); QList assignedItems = CoreDbAccess().db()->getItemIDsInTag(tag->id()); if (!assignedItems.isEmpty()) { tagWithImages.append(tag->title() + QLatin1Char(' ')); } else { tagWithoutImages.append(tag->title() + QLatin1Char(' ')); } /** * Tags must be deleted from children to parents, if we don't want * to step on invalid index. Use QMultiMap to order them by distance * to root tag */ Album* parent = t; int depth = 0; while (!parent->isRoot()) { parent = parent->parent(); depth++; } sortedTags.insert(depth, tag); } // ask for deletion of children if (!tagWithChildrens.isEmpty()) { int result = QMessageBox::warning(qApp->activeWindow(), qApp->applicationName(), i18n("Tags '%1' have one or more subtags. " "Deleting them will also delete " "the subtags.\n" "Do you want to continue?", tagWithChildrens), QMessageBox::Yes | QMessageBox::Cancel); if (result != QMessageBox::Yes) { return; } } QString message; if (!tagWithImages.isEmpty()) { message = i18n("Tags '%1' are assigned to one or more items. " "Do you want to continue?", tagWithImages); } else { message = i18n("Delete '%1' tag(s)?", tagWithoutImages); } int result = QMessageBox::warning(qApp->activeWindow(), i18n("Delete Tag"), message, QMessageBox::Yes | QMessageBox::Cancel); if (result == QMessageBox::Yes) { QMultiMap::iterator it; /** * QMultimap doesn't provide reverse iterator, -1 is required * because end() points after the last element */ for (it = sortedTags.end()-1 ; it != sortedTags.begin()-1 ; --it) { emit aboutToDeleteTag(it.value()); QString errMsg; if (!AlbumManager::instance()->deleteTAlbum(it.value(), errMsg)) { QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), errMsg); } } } } void TagModificationHelper::slotMultipleTagDel() { QList lst = boundMultipleTags(sender()); qCDebug(DIGIKAM_GENERAL_LOG) << lst.size(); slotMultipleTagDel(lst); } void TagModificationHelper::slotFaceTagDelete(TAlbum* t) { QList tag; tag.append(t); slotMultipleFaceTagDel(tag); } void TagModificationHelper::slotFaceTagDelete() { slotFaceTagDelete(boundTag(sender())); } void TagModificationHelper::slotMultipleFaceTagDel(QList& tags) { QString tagsWithChildren; QString tagsWithImages; // We use a set here since else one tag could occur more than once // which could lead to undefined behaviour. QSet allPersonTagsToDelete; int tagsWithChildrenCount = 0; QSet allAssignedItems; int tagsWithImagesCount = 0; foreach(TAlbum* const selectedTag, tags) { if (!selectedTag || selectedTag->isRoot()) { continue; } // find tags and subtags with person property QSet personTagsToDelete = getFaceTags(selectedTag).toSet(); // If there is more than one person tag in the list, // the tag to remove has at least one sub tag that is a face tag. // Thus, we have to warn. // // If there is only one face tag, it is either the original tag, // or it is the only sub tag of the original tag. // Behave, like the face tag itself was selected. if (personTagsToDelete.size() > 1) { if (tagsWithChildrenCount > 0) { tagsWithChildren.append(QLatin1String(",")); } tagsWithChildren.append(selectedTag->title()); ++tagsWithChildrenCount; } // Get the assigned faces for all person tags to delete foreach(TAlbum* const tAlbum, personTagsToDelete) { // If the global set does not yet contain the tag if (!allPersonTagsToDelete.contains(tAlbum)) { QSet assignedItems = CoreDbAccess().db()->getImagesWithImageTagProperty( tAlbum->id(), Digikam::ImageTagPropertyName::tagRegion()).toSet(); assignedItems.unite(CoreDbAccess().db()->getImagesWithImageTagProperty( tAlbum->id(), Digikam::ImageTagPropertyName::autodetectedFace()).toSet()); if (!assignedItems.isEmpty()) { // Add the items to the global set for potential untagging allAssignedItems.unite(assignedItems); if (tagsWithImagesCount > 0) { tagsWithImages.append(QLatin1String(",")); } tagsWithImages.append(tAlbum->title()); ++tagsWithImagesCount; } } } // Add the found tags to the global set. allPersonTagsToDelete.unite(personTagsToDelete); } // ask for deletion of children if (tagsWithChildrenCount) { QString message = i18np("Face tag '%2' has at least one face tag child. " "Deleting it will also delete the children.\n" "Do you want to continue?", "Face tags '%2' have at least one face tag child. " "Deleting it will also delete the children.\n" "Do you want to continue?", tagsWithChildrenCount, tagsWithChildren); bool removeChildren = QMessageBox::Yes == (QMessageBox::warning(qApp->activeWindow(), qApp->applicationName(), message, QMessageBox::Yes | QMessageBox::Cancel)); if (!removeChildren) { return; } } QString message; if (!allAssignedItems.isEmpty()) { message = i18np("Face tag '%2' is assigned to at least one item. " "Do you want to continue?", "Face tags '%2' are assigned to at least one item. " "Do you want to continue?", tagsWithImagesCount, tagsWithImages); } else { message = i18np("Remove face tag?", "Remove face tags?", tags.size()); } bool removeFaceTag = QMessageBox::Yes == (QMessageBox::warning(qApp->activeWindow(), qApp->applicationName(), message, QMessageBox::Yes | QMessageBox::Cancel)); if (removeFaceTag) { // Now we ask the user if we should also remove the tags from the images. QString msg = i18np("Remove the tag corresponding to this face tag from the images?", "Remove the %1 tags corresponding to this face tags from the images?", allPersonTagsToDelete.size()); bool removeTagFromImages = QMessageBox::Yes == (QMessageBox::warning(qApp->activeWindow(), qApp->applicationName(), msg, QMessageBox::Yes | QMessageBox::No)); MetadataHub metadataHub; // remove the face region from images and unassign the tag if wished foreach(const qlonglong& imageId, allAssignedItems) { foreach (TAlbum* const tagToRemove, allPersonTagsToDelete) { ImageTagPair imageTagAssociation(imageId,tagToRemove->id()); if (imageTagAssociation.isAssigned()) { imageTagAssociation.removeProperties(ImageTagPropertyName::autodetectedFace()); imageTagAssociation.removeProperties(ImageTagPropertyName::tagRegion()); if (removeTagFromImages) { imageTagAssociation.unAssignTag(); // Load the current metadata and sync the tags ImageInfo info(imageId); if (!info.isNull()) { metadataHub.load(info); if (!metadataHub.writeToMetadata(info)) { qCWarning(DIGIKAM_GENERAL_LOG) << "Failed writing tags to image " << info.filePath(); } } } } } } foreach(TAlbum* const tAlbum, allPersonTagsToDelete) { TagProperties props(tAlbum->id()); // Delete TagPropertyName::person() and TagPropertyName::faceEngineName() // fetch the UUID to delete the identity from facesdb props.removeProperties(TagPropertyName::person()); props.removeProperties(TagPropertyName::faceEngineName()); QString uuid = props.value(TagPropertyName::faceEngineUuid()); qCDebug(DIGIKAM_GENERAL_LOG) << "Deleting person tag properties for tag " << tAlbum->title() << " with uuid " << uuid; if (!uuid.isEmpty()) { // Delete the UUID props.removeProperties(TagPropertyName::faceEngineUuid()); // delete the faces db identity with this uuid. FaceDbAccess access; access.db()->deleteIdentity(uuid); } } } } void TagModificationHelper::slotMultipleFaceTagDel() { QList lst = boundMultipleTags(sender()); qCDebug(DIGIKAM_GENERAL_LOG) << lst.size(); slotMultipleFaceTagDel(lst); } void TagModificationHelper::slotTagToFaceTag(TAlbum* tAlbum) { if (!tAlbum) { return; } if (!FaceTags::isPerson(tAlbum->id())) { FaceTags::ensureIsPerson(tAlbum->id()); } } void TagModificationHelper::slotTagToFaceTag() { slotTagToFaceTag(boundTag(sender())); } void TagModificationHelper::slotMultipleTagsToFaceTags(QList& tags) { foreach(TAlbum* const selectedTag, tags) { slotTagToFaceTag(selectedTag); } } void TagModificationHelper::slotMultipleTagsToFaceTags() { QList lst = boundMultipleTags(sender()); qCDebug(DIGIKAM_GENERAL_LOG) << lst.size(); slotMultipleTagsToFaceTags(lst); } QList TagModificationHelper::getFaceTags(TAlbum* rootTag) { if (!rootTag) { return QList(); } QList tags; tags.append(rootTag); return getFaceTags(tags).toList(); } QSet TagModificationHelper::getFaceTags(QList tags) { QSet faceTags; foreach(TAlbum* const tAlbum, tags) { if (FaceTags::isPerson(tAlbum->id())) { faceTags.insert(tAlbum); } AlbumPointer tag(tAlbum); AlbumIterator iter(tag); // Get all shild tags which have the person property. while (iter.current()) { Album* const album = iter.current(); // Make sure that no nullp pointer dereference is done. // though while(iter.current()) already tests for the current // album being true, i.e. > 0 , i.e. non-null if (album) { TAlbum* const tAlbum = dynamic_cast(album); if (tAlbum && FaceTags::isPerson(tAlbum->id())) { faceTags.insert(tAlbum); } ++iter; } } } return faceTags; } } // namespace Digikam diff --git a/core/utilities/maintenance/facesdetector.cpp b/core/utilities/maintenance/facesdetector.cpp index 5224fc3e5c..b5bd686a8a 100644 --- a/core/utilities/maintenance/facesdetector.cpp +++ b/core/utilities/maintenance/facesdetector.cpp @@ -1,399 +1,400 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2010-07-18 * Description : batch face detection * * Copyright (C) 2010 by Aditya Bhatt * Copyright (C) 2010-2018 by Gilles Caulier * 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 "facesdetector.h" // Qt includes #include #include #include #include #include #include #include // KDE includes #include #include // Local includes #include "recognitiondatabase.h" #include "digikam_debug.h" #include "coredb.h" #include "album.h" #include "albummanager.h" +#include "albumpointer.h" #include "facepipeline.h" #include "facescansettings.h" #include "imageinfo.h" #include "imageinfojob.h" namespace Digikam { class Q_DECL_HIDDEN BenchmarkMessageDisplay : public QWidget { public: explicit BenchmarkMessageDisplay(const QString& richText) : QWidget(0) { setAttribute(Qt::WA_DeleteOnClose); QVBoxLayout* const vbox = new QVBoxLayout; QTextEdit* const edit = new QTextEdit; vbox->addWidget(edit, 1); QPushButton* const okButton = new QPushButton(i18n("OK")); vbox->addWidget(okButton, 0, Qt::AlignRight); setLayout(vbox); connect(okButton, SIGNAL(clicked()), this, SLOT(close())); edit->setHtml(richText); QApplication::clipboard()->setText(edit->toPlainText()); resize(500, 400); show(); raise(); } }; // -------------------------------------------------------------------------- class Q_DECL_HIDDEN FacesDetector::Private { public: explicit Private() : benchmark(false), useImageInfos(false) { } bool benchmark; bool useImageInfos; AlbumPointerList<> albumTodoList; ImageInfoList infoTodoList; ImageInfoJob albumListing; FacePipeline pipeline; }; FacesDetector::FacesDetector(const FaceScanSettings& settings, ProgressItem* const parent) : MaintenanceTool(QLatin1String("FacesDetector"), parent), d(new Private) { setLabel(i18n("Updating faces database.")); ProgressManager::addProgressItem(this); if (settings.task == FaceScanSettings::RetrainAll) { // clear all training data in the database RecognitionDatabase().clearAllTraining(QLatin1String("digikam")); d->pipeline.plugRetrainingDatabaseFilter(); d->pipeline.plugTrainer(); d->pipeline.construct(); } else if (settings.task == FaceScanSettings::BenchmarkDetection) { d->benchmark = true; d->pipeline.plugDatabaseFilter(FacePipeline::ScanAll); d->pipeline.plugPreviewLoader(); if (settings.useFullCpu) { d->pipeline.plugParallelFaceDetectors(); } else { d->pipeline.plugFaceDetector(); } d->pipeline.plugDetectionBenchmarker(); d->pipeline.construct(); } else if (settings.task == FaceScanSettings::BenchmarkRecognition) { d->benchmark = true; d->pipeline.plugRetrainingDatabaseFilter(); d->pipeline.plugFaceRecognizer(); d->pipeline.plugRecognitionBenchmarker(); d->pipeline.construct(); } else if ((settings.task == FaceScanSettings::DetectAndRecognize) || (settings.task == FaceScanSettings::Detect)) { FacePipeline::FilterMode filterMode; FacePipeline::WriteMode writeMode; if (settings.alreadyScannedHandling == FaceScanSettings::Skip) { filterMode = FacePipeline::SkipAlreadyScanned; writeMode = FacePipeline::NormalWrite; } else if (settings.alreadyScannedHandling == FaceScanSettings::Rescan) { filterMode = FacePipeline::ScanAll; writeMode = FacePipeline::OverwriteUnconfirmed; } else // FaceScanSettings::Merge { filterMode = FacePipeline::ScanAll; writeMode = FacePipeline::NormalWrite; } d->pipeline.plugDatabaseFilter(filterMode); d->pipeline.plugPreviewLoader(); if (settings.useFullCpu) { d->pipeline.plugParallelFaceDetectors(); } else { d->pipeline.plugFaceDetector(); } if (settings.task == FaceScanSettings::DetectAndRecognize) { //d->pipeline.plugRerecognizingDatabaseFilter(); qCDebug(DIGIKAM_GENERAL_LOG) << "recognize algorithm: " << (int)settings.recognizeAlgorithm; d->pipeline.plugFaceRecognizer(); d->pipeline.activeFaceRecognizer(settings.recognizeAlgorithm); } d->pipeline.plugDatabaseWriter(writeMode); d->pipeline.setDetectionAccuracy(settings.accuracy); d->pipeline.construct(); } else // FaceScanSettings::RecognizeMarkedFaces { d->pipeline.plugRerecognizingDatabaseFilter(); d->pipeline.plugFaceRecognizer(); d->pipeline.activeFaceRecognizer(settings.recognizeAlgorithm); d->pipeline.plugDatabaseWriter(FacePipeline::NormalWrite); d->pipeline.setDetectionAccuracy(settings.accuracy); d->pipeline.construct(); } connect(&d->albumListing, SIGNAL(signalItemsInfo(ImageInfoList)), this, SLOT(slotItemsInfo(ImageInfoList))); connect(&d->albumListing, SIGNAL(signalCompleted()), this, SLOT(slotContinueAlbumListing())); connect(&d->pipeline, SIGNAL(finished()), this, SLOT(slotContinueAlbumListing())); connect(&d->pipeline, SIGNAL(processed(FacePipelinePackage)), this, SLOT(slotShowOneDetected(FacePipelinePackage))); connect(&d->pipeline, SIGNAL(skipped(QList)), this, SLOT(slotImagesSkipped(QList))); connect(this, SIGNAL(progressItemCanceled(ProgressItem*)), this, SLOT(slotCancel())); if ((settings.albums.isEmpty() && settings.infos.isEmpty()) || settings.task == FaceScanSettings::RetrainAll) { d->albumTodoList = AlbumManager::instance()->allPAlbums(); } else if (!settings.albums.isEmpty()) { d->albumTodoList = settings.albums; } else { d->infoTodoList = settings.infos; d->useImageInfos = true; } } FacesDetector::~FacesDetector() { delete d; } void FacesDetector::slotStart() { MaintenanceTool::slotStart(); setThumbnail(QIcon::fromTheme(QLatin1String("edit-image-face-show")).pixmap(22)); if (d->useImageInfos) { int total = d->infoTodoList.count(); qCDebug(DIGIKAM_GENERAL_LOG) << "Total is" << total; setTotalItems(total); return slotItemsInfo(d->infoTodoList); } setUsesBusyIndicator(true); // get total count, cached by AlbumManager QMap palbumCounts; QMap talbumCounts; bool hasPAlbums = false; bool hasTAlbums = false; foreach(Album* const album, d->albumTodoList) { if (album->type() == Album::PHYSICAL) { hasPAlbums = true; } else { hasTAlbums = true; } } palbumCounts = AlbumManager::instance()->getPAlbumsCount(); talbumCounts = AlbumManager::instance()->getTAlbumsCount(); if (palbumCounts.isEmpty() && hasPAlbums) { QApplication::setOverrideCursor(Qt::WaitCursor); palbumCounts = CoreDbAccess().db()->getNumberOfImagesInAlbums(); QApplication::restoreOverrideCursor(); } if (talbumCounts.isEmpty() && hasTAlbums) { QApplication::setOverrideCursor(Qt::WaitCursor); talbumCounts = CoreDbAccess().db()->getNumberOfImagesInTags(); QApplication::restoreOverrideCursor(); } // first, we use the progressValueMap map to store absolute counts QMap progressValueMap; foreach(Album* const album, d->albumTodoList) { if (album->type() == Album::PHYSICAL) { progressValueMap[album] = palbumCounts.value(album->id()); } else { // this is possibly broken of course because we do not know if images have multiple tags, // but there's no better solution without expensive operation progressValueMap[album] = talbumCounts.value(album->id()); } } // second, calculate (approximate) overall sum int total = 0; foreach(int count, progressValueMap) { total += count; } total = qMax(1, total); qCDebug(DIGIKAM_GENERAL_LOG) << "Total is" << total; setUsesBusyIndicator(false); setTotalItems(total); slotContinueAlbumListing(); } void FacesDetector::slotContinueAlbumListing() { if (d->useImageInfos) { return slotDone(); } qCDebug(DIGIKAM_GENERAL_LOG) << d->albumListing.isRunning() << !d->pipeline.hasFinished(); // we get here by the finished signal from both, and want both to have finished to continue if (d->albumListing.isRunning() || !d->pipeline.hasFinished()) { return; } // list can have null pointer if album was deleted recently Album* album = 0; do { if (d->albumTodoList.isEmpty()) { return slotDone(); } album = d->albumTodoList.takeFirst(); } while (!album); d->albumListing.allItemsFromAlbum(album); } void FacesDetector::slotItemsInfo(const ImageInfoList& items) { d->pipeline.process(items); } void FacesDetector::slotDone() { if (d->benchmark) { new BenchmarkMessageDisplay(d->pipeline.benchmarkResult()); } // Switch on scanned for faces flag on digiKam config file. KSharedConfig::openConfig()->group("General Settings").writeEntry("Face Scanner First Run", true); MaintenanceTool::slotDone(); } void FacesDetector::slotCancel() { d->pipeline.shutDown(); MaintenanceTool::slotCancel(); } void FacesDetector::slotImagesSkipped(const QList& infos) { advance(infos.size()); } void FacesDetector::slotShowOneDetected(const FacePipelinePackage& /*package*/) { advance(1); } } // namespace Digikam