diff --git a/core/libs/database/models/imagemodel.cpp b/core/libs/database/models/imagemodel.cpp index 8610f914d6..8aeaba6139 100644 --- a/core/libs/database/models/imagemodel.cpp +++ b/core/libs/database/models/imagemodel.cpp @@ -1,1368 +1,1370 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2009-03-05 * Description : Qt item model for database entries * * Copyright (C) 2009-2011 by Marcel Wiesweg * * This program is free software; you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; * either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #include "imagemodel.h" // Qt includes #include #include // Local includes #include "digikam_debug.h" #include "coredbchangesets.h" #include "coredbfields.h" #include "coredbwatch.h" #include "imageinfo.h" #include "imageinfolist.h" #include "abstractitemdragdrophandler.h" namespace Digikam { class Q_DECL_HIDDEN ImageModel::Private { public: explicit Private() { preprocessor = 0; keepFilePathCache = false; sendRemovalSignals = false; incrementalUpdater = 0; refreshing = false; reAdding = false; incrementalRefreshRequested = false; } ImageInfoList infos; QList extraValues; QHash idHash; bool keepFilePathCache; QHash filePathHash; bool sendRemovalSignals; QObject* preprocessor; bool refreshing; bool reAdding; bool incrementalRefreshRequested; DatabaseFields::Set watchFlags; class ImageModelIncrementalUpdater* incrementalUpdater; ImageInfoList pendingInfos; QList pendingExtraValues; inline bool isValid(const QModelIndex& index) { if (!index.isValid()) { return false; } if (index.row() < 0 || index.row() >= infos.size()) { qCDebug(DIGIKAM_GENERAL_LOG) << "Invalid index" << index; return false; } return true; } inline bool extraValueValid(const QModelIndex& index) { // we assume isValid() being called before, no duplicate checks if (index.row() >= extraValues.size()) { qCDebug(DIGIKAM_GENERAL_LOG) << "Invalid index for extraData" << index; return false; } return true; } }; typedef QPair IntPair; // to make foreach macro happy typedef QList IntPairList; class Q_DECL_HIDDEN ImageModelIncrementalUpdater { public: explicit ImageModelIncrementalUpdater(ImageModel::Private* d); void appendInfos(const QList& infos, const QList& extraValues); void aboutToBeRemovedInModel(const IntPairList& aboutToBeRemoved); QList oldIndexes(); static QList toContiguousPairs(const QList& ids); public: QHash oldIds; QList oldExtraValues; QList newInfos; QList newExtraValues; QList modelRemovals; }; ImageModel::ImageModel(QObject* parent) : QAbstractListModel(parent), d(new Private) { connect(CoreDbAccess::databaseWatch(), SIGNAL(imageChange(ImageChangeset)), this, SLOT(slotImageChange(ImageChangeset))); connect(CoreDbAccess::databaseWatch(), SIGNAL(imageTagChange(ImageTagChangeset)), this, SLOT(slotImageTagChange(ImageTagChangeset))); } ImageModel::~ImageModel() { delete d->incrementalUpdater; delete d; } // ------------ Access methods ------------- void ImageModel::setKeepsFilePathCache(bool keepCache) { d->keepFilePathCache = keepCache; } bool ImageModel::keepsFilePathCache() const { return d->keepFilePathCache; } bool ImageModel::isEmpty() const { return d->infos.isEmpty(); } void ImageModel::setWatchFlags(const DatabaseFields::Set& set) { d->watchFlags = set; } ImageInfo ImageModel::imageInfo(const QModelIndex& index) const { if (!d->isValid(index)) { return ImageInfo(); } return d->infos.at(index.row()); } ImageInfo& ImageModel::imageInfoRef(const QModelIndex& index) const { return d->infos[index.row()]; } qlonglong ImageModel::imageId(const QModelIndex& index) const { if (!d->isValid(index)) { return 0; } return d->infos.at(index.row()).id(); } QList ImageModel::imageInfos(const QList& indexes) const { QList infos; foreach(const QModelIndex& index, indexes) { infos << imageInfo(index); } return infos; } QList ImageModel::imageIds(const QList& indexes) const { QList ids; foreach(const QModelIndex& index, indexes) { ids << imageId(index); } return ids; } ImageInfo ImageModel::imageInfo(int row) const { if (row >= d->infos.size()) { return ImageInfo(); } return d->infos.at(row); } ImageInfo& ImageModel::imageInfoRef(int row) const { return d->infos[row]; } qlonglong ImageModel::imageId(int row) const { if (row < 0 || row >= d->infos.size()) { return -1; } return d->infos.at(row).id(); } QModelIndex ImageModel::indexForImageInfo(const ImageInfo& info) const { return indexForImageId(info.id()); } QModelIndex ImageModel::indexForImageInfo(const ImageInfo& info, const QVariant& extraValue) const { return indexForImageId(info.id(), extraValue); } QList ImageModel::indexesForImageInfo(const ImageInfo& info) const { return indexesForImageId(info.id()); } QModelIndex ImageModel::indexForImageId(qlonglong id) const { int index = d->idHash.value(id, -1); if (index != -1) { return createIndex(index, 0); } return QModelIndex(); } QModelIndex ImageModel::indexForImageId(qlonglong id, const QVariant& extraValue) const { if (d->extraValues.isEmpty()) return indexForImageId(id); QHash::const_iterator it; for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it) { if (d->extraValues.at(it.value()) == extraValue) return createIndex(it.value(), 0); } return QModelIndex(); } QList ImageModel::indexesForImageId(qlonglong id) const { QList indexes; QHash::const_iterator it; for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it) { indexes << createIndex(it.value(), 0); } return indexes; } int ImageModel::numberOfIndexesForImageInfo(const ImageInfo& info) const { return numberOfIndexesForImageId(info.id()); } int ImageModel::numberOfIndexesForImageId(qlonglong id) const { if (d->extraValues.isEmpty()) { return 0; } int count = 0; QHash::const_iterator it; for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it) { ++count; } return count; } // static method ImageInfo ImageModel::retrieveImageInfo(const QModelIndex& index) { if (!index.isValid()) { return ImageInfo(); } ImageModel* const model = index.data(ImageModelPointerRole).value(); int row = index.data(ImageModelInternalId).toInt(); if (!model) { return ImageInfo(); } return model->imageInfo(row); } // static method qlonglong ImageModel::retrieveImageId(const QModelIndex& index) { if (!index.isValid()) { return 0; } ImageModel* const model = index.data(ImageModelPointerRole).value(); int row = index.data(ImageModelInternalId).toInt(); if (!model) { return 0; } return model->imageId(row); } QModelIndex ImageModel::indexForPath(const QString& filePath) const { if (d->keepFilePathCache) { return indexForImageId(d->filePathHash.value(filePath)); } else { const int size = d->infos.size(); for (int i = 0 ; i < size ; ++i) { if (d->infos.at(i).filePath() == filePath) { return createIndex(i, 0); } } } return QModelIndex(); } QList ImageModel::indexesForPath(const QString& filePath) const { if (d->keepFilePathCache) { return indexesForImageId(d->filePathHash.value(filePath)); } else { QList indexes; const int size = d->infos.size(); for (int i = 0 ; i < size ; ++i) { if (d->infos.at(i).filePath() == filePath) { indexes << createIndex(i, 0); } } return indexes; } } ImageInfo ImageModel::imageInfo(const QString& filePath) const { if (d->keepFilePathCache) { qlonglong id = d->filePathHash.value(filePath); if (id) { int index = d->idHash.value(id, -1); if (index != -1) { return d->infos.at(index); } } } else { foreach(const ImageInfo& info, d->infos) { if (info.filePath() == filePath) { return info; } } } return ImageInfo(); } QList ImageModel::imageInfos(const QString& filePath) const { QList infos; if (d->keepFilePathCache) { qlonglong id = d->filePathHash.value(filePath); if (id) { foreach(int index, d->idHash.values(id)) { infos << d->infos.at(index); } } } else { foreach(const ImageInfo& info, d->infos) { if (info.filePath() == filePath) { infos << info; } } } return infos; } void ImageModel::addImageInfo(const ImageInfo& info) { addImageInfos(QList() << info, QList()); } void ImageModel::addImageInfos(const QList& infos) { addImageInfos(infos, QList()); } void ImageModel::addImageInfos(const QList& infos, const QList& extraValues) { if (infos.isEmpty()) { return; } if (d->incrementalUpdater) { d->incrementalUpdater->appendInfos(infos, extraValues); } else { appendInfos(infos, extraValues); } } void ImageModel::addImageInfoSynchronously(const ImageInfo& info) { addImageInfosSynchronously(QList() << info, QList()); } void ImageModel::addImageInfosSynchronously(const QList& infos) { addImageInfos(infos, QList()); } void ImageModel::addImageInfosSynchronously(const QList& infos, const QList& extraValues) { if (infos.isEmpty()) { return; } publiciseInfos(infos, extraValues); emit processAdded(infos, extraValues); } void ImageModel::ensureHasImageInfo(const ImageInfo& info) { ensureHasImageInfos(QList() << info, QList()); } void ImageModel::ensureHasImageInfos(const QList& infos) { ensureHasImageInfos(infos, QList()); } void ImageModel::ensureHasImageInfos(const QList& infos, const QList& extraValues) { if (extraValues.isEmpty()) { if (!d->pendingExtraValues.isEmpty()) { qCDebug(DIGIKAM_GENERAL_LOG) << "ExtraValue / No Extra Value mismatch. Ignoring added infos."; return; } } else { if (d->pendingInfos.size() != d->pendingExtraValues.size()) { qCDebug(DIGIKAM_GENERAL_LOG) << "ExtraValue / No Extra Value mismatch. Ignoring added infos."; return; } } d->pendingInfos << infos; d->pendingExtraValues << extraValues; cleanSituationChecks(); } void ImageModel::clearImageInfos() { + beginResetModel(); + d->infos.clear(); d->extraValues.clear(); d->idHash.clear(); d->filePathHash.clear(); + delete d->incrementalUpdater; + d->incrementalUpdater = 0; d->pendingInfos.clear(); d->pendingExtraValues.clear(); d->refreshing = false; d->reAdding = false; d->incrementalRefreshRequested = false; - beginResetModel(); - endResetModel(); - imageInfosCleared(); + endResetModel(); } void ImageModel::setImageInfos(const QList& infos) { clearImageInfos(); addImageInfos(infos); } QList ImageModel::imageInfos() const { return d->infos; } QList ImageModel::imageIds() const { return d->idHash.keys(); } bool ImageModel::hasImage(qlonglong id) const { return d->idHash.contains(id); } bool ImageModel::hasImage(const ImageInfo& info) const { return d->idHash.contains(info.id()); } bool ImageModel::hasImage(const ImageInfo& info, const QVariant& extraValue) const { return hasImage(info.id(), extraValue); } bool ImageModel::hasImage(qlonglong id, const QVariant& extraValue) const { if (d->extraValues.isEmpty()) return hasImage(id); QHash::const_iterator it; for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it) { if (d->extraValues.at(it.value()) == extraValue) return true; } return false;; } QList ImageModel::uniqueImageInfos() const { if (d->extraValues.isEmpty()) { return d->infos; } QList uniqueInfos; const int size = d->infos.size(); for (int i = 0 ; i < size ; ++i) { const ImageInfo& info = d->infos.at(i); if (d->idHash.value(info.id()) == i) { uniqueInfos << info; } } return uniqueInfos; } void ImageModel::emitDataChangedForAll() { if (d->infos.isEmpty()) { return; } QModelIndex first = createIndex(0, 0); QModelIndex last = createIndex(d->infos.size() - 1, 0); emit dataChanged(first, last); } void ImageModel::emitDataChangedForSelection(const QItemSelection& selection) { if (!selection.isEmpty()) { foreach(const QItemSelectionRange& range, selection) { emit dataChanged(range.topLeft(), range.bottomRight()); } } } void ImageModel::ensureHasGroupedImages(const ImageInfo& groupLeader) { ensureHasImageInfos(groupLeader.groupedImages()); } // ------------ Preprocessing ------------- void ImageModel::setPreprocessor(QObject* preprocessor) { unsetPreprocessor(d->preprocessor); d->preprocessor = preprocessor; } void ImageModel::unsetPreprocessor(QObject* preprocessor) { if (preprocessor && d->preprocessor == preprocessor) { disconnect(this, SIGNAL(preprocess(QList,QList)), 0, 0); disconnect(d->preprocessor, 0, this, SLOT(reAddImageInfos(QList,QList))); disconnect(d->preprocessor, 0, this, SLOT(reAddingFinished())); } } void ImageModel::appendInfos(const QList& infos, const QList& extraValues) { if (infos.isEmpty()) { return; } if (d->preprocessor) { d->reAdding = true; emit preprocess(infos, extraValues); } else { publiciseInfos(infos, extraValues); } } void ImageModel::appendInfosChecked(const QList& infos, const QList& extraValues) { // This method does deduplication. It is private because in context of readding or refreshing it is of no use. if (extraValues.isEmpty()) { QList checkedInfos; foreach(const ImageInfo& info, infos) { if (!hasImage(info)) { checkedInfos << info; } } appendInfos(checkedInfos, QList()); } else { QList checkedInfos; QList checkedExtraValues; const int size = infos.size(); for (int i = 0 ; i < size ; i++) { if (!hasImage(infos[i], extraValues[i])) { checkedInfos << infos[i]; checkedExtraValues << extraValues[i]; } } appendInfos(checkedInfos, checkedExtraValues); } } void ImageModel::reAddImageInfos(const QList& infos, const QList& extraValues) { // addImageInfos -> appendInfos -> preprocessor -> reAddImageInfos publiciseInfos(infos, extraValues); } void ImageModel::reAddingFinished() { d->reAdding = false; cleanSituationChecks(); } void ImageModel::startRefresh() { d->refreshing = true; } void ImageModel::finishRefresh() { d->refreshing = false; cleanSituationChecks(); } bool ImageModel::isRefreshing() const { return d->refreshing; } void ImageModel::cleanSituationChecks() { // For starting an incremental refresh we want a clear situation: // Any remaining batches from non-incremental refreshing subclasses have been received in appendInfos(), // any batches sent to preprocessor for re-adding have been re-added. if (d->refreshing || d->reAdding) { return; } if (!d->pendingInfos.isEmpty()) { appendInfosChecked(d->pendingInfos, d->pendingExtraValues); d->pendingInfos.clear(); d->pendingExtraValues.clear(); cleanSituationChecks(); return; } if (d->incrementalRefreshRequested) { d->incrementalRefreshRequested = false; emit readyForIncrementalRefresh(); } else { emit allRefreshingFinished(); } } void ImageModel::publiciseInfos(const QList& infos, const QList& extraValues) { if (infos.isEmpty()) { return; } Q_ASSERT(infos.size() == extraValues.size() || (extraValues.isEmpty() && d->extraValues.isEmpty())); emit imageInfosAboutToBeAdded(infos); const int firstNewIndex = d->infos.size(); const int lastNewIndex = d->infos.size() + infos.size() - 1; beginInsertRows(QModelIndex(), firstNewIndex, lastNewIndex); d->infos << infos; d->extraValues << extraValues; for (int i = firstNewIndex ; i <= lastNewIndex ; ++i) { const ImageInfo& info = d->infos.at(i); qlonglong id = info.id(); d->idHash.insertMulti(id, i); if (d->keepFilePathCache) { d->filePathHash[info.filePath()] = id; } } endInsertRows(); emit imageInfosAdded(infos); } void ImageModel::requestIncrementalRefresh() { if (d->reAdding) { d->incrementalRefreshRequested = true; } else { emit readyForIncrementalRefresh(); } } bool ImageModel::hasIncrementalRefreshPending() const { return d->incrementalRefreshRequested; } void ImageModel::startIncrementalRefresh() { delete d->incrementalUpdater; d->incrementalUpdater = new ImageModelIncrementalUpdater(d); } void ImageModel::finishIncrementalRefresh() { if (!d->incrementalUpdater) { return; } // remove old entries QList > pairs = d->incrementalUpdater->oldIndexes(); removeRowPairs(pairs); // add new indexes appendInfos(d->incrementalUpdater->newInfos, d->incrementalUpdater->newExtraValues); delete d->incrementalUpdater; d->incrementalUpdater = 0; } void ImageModel::removeIndex(const QModelIndex& index) { removeIndexes(QList() << index); } void ImageModel::removeIndexes(const QList& indexes) { QList listIndexes; foreach(const QModelIndex& index, indexes) { if (d->isValid(index)) { listIndexes << index.row(); } } if (listIndexes.isEmpty()) { return; } removeRowPairsWithCheck(ImageModelIncrementalUpdater::toContiguousPairs(listIndexes)); } void ImageModel::removeImageInfo(const ImageInfo& info) { removeImageInfos(QList() << info); } void ImageModel::removeImageInfos(const QList& infos) { QList listIndexes; foreach(const ImageInfo& info, infos) { QModelIndex index = indexForImageId(info.id()); if (index.isValid()) { listIndexes << index.row(); } } removeRowPairsWithCheck(ImageModelIncrementalUpdater::toContiguousPairs(listIndexes)); } void ImageModel::removeImageInfos(const QList& infos, const QList& extraValues) { if (extraValues.isEmpty()) { removeImageInfos(infos); return; } QList listIndexes; for (int i = 0 ; i < infos.size() ; ++i) { QModelIndex index = indexForImageId(infos.at(i).id(), extraValues.at(i)); if (index.isValid()) { listIndexes << index.row(); } } removeRowPairsWithCheck(ImageModelIncrementalUpdater::toContiguousPairs(listIndexes)); } void ImageModel::setSendRemovalSignals(bool send) { d->sendRemovalSignals = send; } template static bool pairsContain(const List& list, T value) { typename List::const_iterator middle; typename List::const_iterator begin = list.begin(); typename List::const_iterator end = list.end(); int n = int(end - begin); int half; while (n > 0) { half = n >> 1; middle = begin + half; if (middle->first <= value && middle->second >= value) { return true; } else if (middle->second < value) { begin = middle + 1; n -= half + 1; } else { n = half; } } return false; } void ImageModel::removeRowPairsWithCheck(const QList >& toRemove) { if (d->incrementalUpdater) { d->incrementalUpdater->aboutToBeRemovedInModel(toRemove); } removeRowPairs(toRemove); } void ImageModel::removeRowPairs(const QList >& toRemove) { if (toRemove.isEmpty()) { return; } // Remove old indexes // Keep in mind that when calling beginRemoveRows all structures announced to be removed // must still be valid, and this includes our hashes as well, which limits what we can optimize int removedRows = 0, offset = 0; typedef QPair IntPair; // to make foreach macro happy foreach(const IntPair& pair, toRemove) { const int begin = pair.first - offset; const int end = pair.second - offset; // inclusive removedRows = end - begin + 1; // when removing from the list, all subsequent indexes are affected offset += removedRows; QList removedInfos; if (d->sendRemovalSignals) { std::copy(d->infos.begin() + begin, d->infos.begin() + end, removedInfos.begin()); emit imageInfosAboutToBeRemoved(removedInfos); } imageInfosAboutToBeRemoved(begin, end); beginRemoveRows(QModelIndex(), begin, end); // update idHash - which points to indexes of d->infos, and these change now! QHash::iterator it; for (it = d->idHash.begin() ; it != d->idHash.end() ; ) { if (it.value() >= begin) { if (it.value() > end) { // after the removed interval: adjust index it.value() -= removedRows; } else { // in the removed interval it = d->idHash.erase(it); continue; } } ++it; } // remove from list d->infos.erase(d->infos.begin() + begin, d->infos.begin() + (end + 1)); if (!d->extraValues.isEmpty()) { d->extraValues.erase(d->extraValues.begin() + begin, d->extraValues.begin() + (end + 1)); } endRemoveRows(); if (d->sendRemovalSignals) { emit imageInfosRemoved(removedInfos); } } // tidy up: remove old indexes from file path hash now if (d->keepFilePathCache) { QHash::iterator it; for (it = d->filePathHash.begin() ; it != d->filePathHash.end() ; ) { if (pairsContain(toRemove, it.value())) { it = d->filePathHash.erase(it); } else { ++it; } } } } ImageModelIncrementalUpdater::ImageModelIncrementalUpdater(ImageModel::Private* d) { oldIds = d->idHash; oldExtraValues = d->extraValues; } void ImageModelIncrementalUpdater::appendInfos(const QList& infos, const QList& extraValues) { if (extraValues.isEmpty()) { foreach(const ImageInfo& info, infos) { QHash::iterator it = oldIds.find(info.id()); if (it != oldIds.end()) { oldIds.erase(it); } else { newInfos << info; } } } else { for (int i = 0 ; i < infos.size() ; ++i) { const ImageInfo& info = infos.at(i); bool found = false; QHash::iterator it; for (it = oldIds.find(info.id()) ; it != oldIds.end() && it.key() == info.id() ; ++it) { // first check is for bug #262596. Not sure if needed. if (it.value() < oldExtraValues.size() && extraValues.at(i) == oldExtraValues.at(it.value())) { found = true; break; } } if (found) { oldIds.erase(it); // do not erase from oldExtraValues - oldIds is a hash id -> index. } else { newInfos << info; newExtraValues << extraValues.at(i); } } } } void ImageModelIncrementalUpdater::aboutToBeRemovedInModel(const IntPairList& toRemove) { modelRemovals << toRemove; } QList > ImageModelIncrementalUpdater::oldIndexes() { // first, apply all changes to indexes by direct removal in model // while the updater was active foreach(const IntPairList& list, modelRemovals) { int removedRows = 0, offset = 0; foreach(const IntPair& pair, list) { const int begin = pair.first - offset; const int end = pair.second - offset; // inclusive removedRows = end - begin + 1; // when removing from the list, all subsequent indexes are affected offset += removedRows; // update idHash - which points to indexes of d->infos, and these change now! QHash::iterator it; for (it = oldIds.begin() ; it != oldIds.end() ; ) { if (it.value() >= begin) { if (it.value() > end) { // after the removed interval: adjust index it.value() -= removedRows; } else { // in the removed interval it = oldIds.erase(it); continue; } } ++it; } } } modelRemovals.clear(); return toContiguousPairs(oldIds.values()); } QList > ImageModelIncrementalUpdater::toContiguousPairs(const QList& unsorted) { // Take the given indices and return them as contiguous pairs [begin, end] QList > pairs; if (unsorted.isEmpty()) { return pairs; } QList indices(unsorted); std::sort(indices.begin(), indices.end()); QPair pair(indices.first(), indices.first()); for (int i = 1 ; i < indices.size() ; ++i) { const int &index = indices.at(i); if (index == pair.second + 1) { pair.second = index; continue; } pairs << pair; // insert last pair pair.first = index; pair.second = index; } pairs << pair; return pairs; } // ------------ QAbstractItemModel implementation ------------- QVariant ImageModel::data(const QModelIndex& index, int role) const { if (!d->isValid(index)) { return QVariant(); } switch (role) { case Qt::DisplayRole: case Qt::ToolTipRole: return d->infos.at(index.row()).name(); case ImageModelPointerRole: return QVariant::fromValue(const_cast(this)); case ImageModelInternalId: return index.row(); case CreationDateRole: return d->infos.at(index.row()).dateTime(); case ExtraDataRole: if (d->extraValueValid(index)) { return d->extraValues.at(index.row()); } else { return QVariant(); } case ExtraDataDuplicateCount: { qlonglong id = d->infos.at(index.row()).id(); return numberOfIndexesForImageId(id); } } return QVariant(); } QVariant ImageModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(section) Q_UNUSED(orientation) Q_UNUSED(role) return QVariant(); } int ImageModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } return d->infos.size(); } Qt::ItemFlags ImageModel::flags(const QModelIndex& index) const { if (!d->isValid(index)) { return 0; } Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled; f |= dragDropFlags(index); return f; } QModelIndex ImageModel::index(int row, int column, const QModelIndex& parent) const { if (column != 0 || row < 0 || parent.isValid() || row >= d->infos.size()) { return QModelIndex(); } return createIndex(row, 0); } // ------------ Database watch ------------- void ImageModel::slotImageChange(const ImageChangeset& changeset) { if (d->infos.isEmpty()) { return; } if (d->watchFlags & changeset.changes()) { QItemSelection items; foreach(const qlonglong& id, changeset.ids()) { QModelIndex index = indexForImageId(id); if (index.isValid()) { items.select(index, index); } } if (!items.isEmpty()) { emitDataChangedForSelection(items); emit imageChange(changeset, items); } } } void ImageModel::slotImageTagChange(const ImageTagChangeset& changeset) { if (d->infos.isEmpty()) { return; } QItemSelection items; foreach(const qlonglong& id, changeset.ids()) { QModelIndex index = indexForImageId(id); if (index.isValid()) { items.select(index, index); } } if (!items.isEmpty()) { emitDataChangedForSelection(items); emit imageTagChange(changeset, items); } } } // namespace Digikam diff --git a/core/showfoto/thumbbar/showfotoimagemodel.cpp b/core/showfoto/thumbbar/showfotoimagemodel.cpp index 329e39d1d7..79a62d7924 100644 --- a/core/showfoto/thumbbar/showfotoimagemodel.cpp +++ b/core/showfoto/thumbbar/showfotoimagemodel.cpp @@ -1,640 +1,641 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2013-07-05 * Description : Qt model for Showfoto entries * * Copyright (C) 2013 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 "showfotoimagemodel.h" // Qt includes #include // Local includes #include "showfoto.h" #include "showfotoiteminfo.h" namespace ShowFoto { class Q_DECL_HIDDEN ShowfotoImageModel::Private { public: explicit Private() { keepFileUrlCache = false; refreshing = false; reAdding = false; incrementalRefreshRequested = false; sendRemovalSignals = false; } inline bool isValid(const QModelIndex& index) { return (index.isValid() && (index.row() >= 0) && (index.row() < infos.size()) ); } public: ShowfotoItemInfoList infos; QHash idHash; QHash fileUrlHash; bool keepFileUrlCache; bool refreshing; bool reAdding; bool incrementalRefreshRequested; bool sendRemovalSignals; }; // ---------------------------------------------------------------------------------------------------- ShowfotoImageModel::ShowfotoImageModel(QObject* const parent) : QAbstractListModel(parent), d(new Private) { } ShowfotoImageModel::~ShowfotoImageModel() { delete d; } bool ShowfotoImageModel::isEmpty() const { return d->infos.isEmpty(); } ShowfotoItemInfo ShowfotoImageModel::showfotoItemInfo(const QModelIndex& index) const { if (!d->isValid(index)) { return ShowfotoItemInfo(); } return d->infos.at(index.row()); } ShowfotoItemInfo& ShowfotoImageModel::showfotoItemInfoRef(const QModelIndex& index) const { return d->infos[index.row()]; } QList ShowfotoImageModel::showfotoItemInfos(const QList& indexes) const { QList infos; foreach(const QModelIndex& index, indexes) { infos << showfotoItemInfo(index); } return infos; } ShowfotoItemInfo ShowfotoImageModel::showfotoItemInfo(int row) const { if (row >= d->infos.size()) { return ShowfotoItemInfo(); } return d->infos.at(row); } ShowfotoItemInfo& ShowfotoImageModel::showfotoItemInfoRef(int row) const { return d->infos[row]; } QModelIndex ShowfotoImageModel::indexForShowfotoItemInfo(const ShowfotoItemInfo& info) const { return indexForUrl(info.url); } QList ShowfotoImageModel::indexesForShowfotoItemInfo(const ShowfotoItemInfo& info) const { return indexesForUrl(info.url); } QModelIndex ShowfotoImageModel::indexForShowfotoItemId(qlonglong id) const { int index = d->idHash.value(id, 0); if (index != -1) { return createIndex(index, 0); } return QModelIndex(); } // static method ShowfotoItemInfo ShowfotoImageModel::retrieveShowfotoItemInfo(const QModelIndex& index) { if (!index.isValid()) { return ShowfotoItemInfo(); } ShowfotoImageModel* const model = index.data(ShowfotoImageModelPointerRole).value(); int row = index.data(ShowfotoImageModelInternalId).toInt(); if (!model) { return ShowfotoItemInfo(); } return model->showfotoItemInfo(row); } QModelIndex ShowfotoImageModel::indexForUrl(const QUrl& fileUrl) const { const int size = d->infos.size(); for (int i = 0; i < size; i++) { if (d->infos.at(i).url == fileUrl) { return createIndex(i, 0); } } return QModelIndex(); } QList ShowfotoImageModel::indexesForUrl(const QUrl& fileUrl) const { QList indexes; const int size = d->infos.size(); for (int i = 0; i < size; i++) { if (d->infos.at(i).url == fileUrl) { indexes << createIndex(i, 0); } } return indexes; } ShowfotoItemInfo ShowfotoImageModel::showfotoItemInfo(const QUrl& fileUrl) const { foreach(const ShowfotoItemInfo& info, d->infos) { if (info.url == fileUrl) { return info; } } return ShowfotoItemInfo(); } QList ShowfotoImageModel::showfotoItemInfos(const QUrl& fileUrl) const { QList infos; foreach(const ShowfotoItemInfo& info, d->infos) { if (info.url == fileUrl) { infos << info; } } return infos; } void ShowfotoImageModel::addShowfotoItemInfo(const ShowfotoItemInfo& info) { addShowfotoItemInfos(QList() << info); } void ShowfotoImageModel::addShowfotoItemInfos(const QList& infos) { if (infos.isEmpty()) { return; } appendInfos(infos); } void ShowfotoImageModel::addShowfotoItemInfoSynchronously(const ShowfotoItemInfo& info) { addShowfotoItemInfosSynchronously(QList() << info); } void ShowfotoImageModel::addShowfotoItemInfosSynchronously(const QList& infos) { if (infos.isEmpty()) { return; } publiciseInfos(infos); emit processAdded(infos); } void ShowfotoImageModel::clearShowfotoItemInfos() { + beginResetModel(); + d->infos.clear(); d->fileUrlHash.clear(); d->reAdding = false; d->refreshing = false; d->incrementalRefreshRequested = false; - beginResetModel(); showfotoItemInfosCleared(); endResetModel(); } void ShowfotoImageModel::setShowfotoItemInfos(const QList& infos) { clearShowfotoItemInfos(); addShowfotoItemInfos(infos); } QList ShowfotoImageModel::showfotoItemInfos() const { return d->infos; } bool ShowfotoImageModel::hasImage(const ShowfotoItemInfo& info) const { return d->fileUrlHash.contains(info.url.toDisplayString()); } void ShowfotoImageModel::emitDataChangedForAll() { if (d->infos.isEmpty()) { return; } QModelIndex first = createIndex(0, 0); QModelIndex last = createIndex(d->infos.size() - 1, 0); emit dataChanged(first, last); } void ShowfotoImageModel::emitDataChangedForSelections(const QItemSelection& selection) { if (!selection.isEmpty()) { foreach(const QItemSelectionRange& range, selection) { emit dataChanged(range.topLeft(), range.bottomRight()); } } } void ShowfotoImageModel::appendInfos(const QList& infos) { if (infos.isEmpty()) { return; } publiciseInfos(infos); } void ShowfotoImageModel::reAddShowfotoItemInfos(ShowfotoItemInfoList& infos) { publiciseInfos(infos); } void ShowfotoImageModel::reAddingFinished() { d->reAdding = false; //cleanSituationChecks(); } void ShowfotoImageModel::slotFileDeleted(const QString& folder, const QString& file, bool status) { Q_UNUSED(status) ShowfotoItemInfo info = showfotoItemInfo(QUrl::fromLocalFile(folder + file)); //removeShowfotoItemInfo(info); } void ShowfotoImageModel::slotFileUploaded(const ShowfotoItemInfo& info) { addShowfotoItemInfo(info); } void ShowfotoImageModel::publiciseInfos(const QList& infos) { if (infos.isEmpty()) { return; } emit itemInfosAboutToBeAdded(infos); const int firstNewIndex = d->infos.size(); const int lastNewIndex = d->infos.size() + infos.size() -1; beginInsertRows(QModelIndex(), firstNewIndex, lastNewIndex); d->infos << infos; for (int i = firstNewIndex; i <= lastNewIndex; ++i) { const ShowfotoItemInfo& info = d->infos.at(i); qlonglong id = info.id; d->idHash.insertMulti(id, i); if (d->keepFileUrlCache) { d->fileUrlHash[info.url.toDisplayString()] = id; } } endInsertRows(); emit itemInfosAdded(infos); } template static bool pairsContain(const List& list, T value) { typename List::const_iterator middle; typename List::const_iterator begin = list.begin(); typename List::const_iterator end = list.end(); int n = int(end - begin); int half; while(n > 0) { half = n >> 1; middle = begin + half; if ((middle->first <= value) && (middle->second >= value)) { return true; } else if (middle->second < value) { begin = middle + 1; n -= half + 1; } else { n = half; } } return false; } void ShowfotoImageModel::removeIndex(const QModelIndex& index) { removeIndexs(QList() << index); } void ShowfotoImageModel::removeIndexs(const QList& indexes) { QList indexesList; foreach(const QModelIndex& index, indexes) { if (d->isValid(index)) { indexesList << index.row(); } } if (indexesList.isEmpty()) { return; } removeRowPairs(toContiguousPairs(indexesList)); } void ShowfotoImageModel::setSendRemovalSignals(bool send) { d->sendRemovalSignals = send; } void ShowfotoImageModel::removeRowPairs(const QList >& toRemove) { if (toRemove.isEmpty()) { return; } // Remove old indexes // Keep in mind that when calling beginRemoveRows all structures announced to be removed // must still be valid, and this includes our hashes as well, which limits what we can optimize int removedRows = 0; int offset = 0; typedef QPair IntPair; foreach(const IntPair& pair, toRemove) { const int begin = pair.first - offset; const int end = pair.second - offset; removedRows = end - begin + 1; // when removing from the list, all subsequent indexes are affected offset += removedRows; QList removedInfos; if (d->sendRemovalSignals) { std::copy(d->infos.begin() + begin, d->infos.begin() + end, removedInfos.begin()); emit itemInfosAboutToBeRemoved(removedInfos); } showfotoItemInfosAboutToBeRemoved(begin, end); beginRemoveRows(QModelIndex(), begin, end); // update idHash - which points to indexes of d->infos QHash::iterator it; for (it = d->idHash.begin(); it != d->idHash.end(); ) { if (it.value() >= begin) { if (it.value() > end) { // after the removed interval, adjust index it.value() -= removedRows; } else { // in the removed interval it = d->idHash.erase(it); continue; } } ++it; } // remove from list d->infos.erase(d->infos.begin() + begin, d->infos.begin() + (end + 1)); endRemoveRows(); if (d->sendRemovalSignals) { emit itemInfosRemoved(removedInfos); } } // tidy up: remove old indexes from file path hash now if (d->keepFileUrlCache) { QHash::iterator it; for (it = d->fileUrlHash.begin(); it!= d->fileUrlHash.end(); ) { if (pairsContain(toRemove, it.value())) { it = d->fileUrlHash.erase(it); } else { ++it; } } } } QList > ShowfotoImageModel::toContiguousPairs(const QList& unsorted) { // Take the given indices and return them as contiguous pairs [begin, end] QList > pairs; if (unsorted.isEmpty()) { return pairs; } QList indices(unsorted); std::sort(indices.begin(), indices.end()); QPair pair(indices.first(), indices.first()); for (int i=1; i < indices.size(); i++) { const int &index = indices.at(i); if (index == pair.second + 1) { pair.second = index; continue; } pairs << pair; // insert last pair pair.first = index; pair.second = index; } pairs << pair; return pairs; } // ------------ QAbstractItemModel implementation ------------- int ShowfotoImageModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } return d->infos.size(); } Qt::ItemFlags ShowfotoImageModel::flags(const QModelIndex& index) const { if (!d->isValid(index)) { return 0; } Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled; f |= dragDropFlags(index); return f; } QModelIndex ShowfotoImageModel::index(int row, int column, const QModelIndex& parent) const { if (column != 0 || row < 0 || parent.isValid() || row >= d->infos.size()) { return QModelIndex(); } return createIndex(row, 0); } QVariant ShowfotoImageModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(section) Q_UNUSED(orientation) Q_UNUSED(role) return QVariant(); } QVariant ShowfotoImageModel::data(const QModelIndex& index, int role) const { if (!d->isValid(index)) { return QVariant(); } switch(role) { case Qt::DisplayRole: case Qt::ToolTipRole: return d->infos.at(index.row()).name; break; case ShowfotoImageModelPointerRole: return QVariant::fromValue(const_cast(this)); break; case ShowfotoImageModelInternalId: return index.row(); break; } return QVariant(); } } // namespace Digikam diff --git a/core/utilities/import/models/importimagemodel.cpp b/core/utilities/import/models/importimagemodel.cpp index aca62df645..0f01746707 100644 --- a/core/utilities/import/models/importimagemodel.cpp +++ b/core/utilities/import/models/importimagemodel.cpp @@ -1,1070 +1,1071 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2012-05-22 * Description : Qt item model for camera entries * * Copyright (C) 2012 by Islam Wazery * * 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 "importimagemodel.h" // Qt includes #include // Local includes #include "digikam_debug.h" #include "coredbdownloadhistory.h" #include "cameracontroller.h" namespace Digikam { class Q_DECL_HIDDEN ImportImageModel::Private { public: explicit Private() { keepFileUrlCache = false; refreshing = false; reAdding = false; incrementalRefreshRequested = false; sendRemovalSignals = false; incrementalUpdater = 0; controller = 0; } inline bool isValid(const QModelIndex& index) { return (index.isValid() && (index.row() >= 0) && (index.row() < infos.size()) ); } public: CameraController* controller; CamItemInfoList infos; QHash idHash; QHash fileUrlHash; bool keepFileUrlCache; bool refreshing; bool reAdding; bool incrementalRefreshRequested; bool sendRemovalSignals; class ImportImageModelIncrementalUpdater* incrementalUpdater; }; // ---------------------------------------------------------------------------------------------------- typedef QPair IntPair; typedef QList IntPairList; class Q_DECL_HIDDEN ImportImageModelIncrementalUpdater { public: explicit ImportImageModelIncrementalUpdater(ImportImageModel::Private* const d); void appendInfos(const QList& infos); void aboutToBeRemovedInModel(const IntPairList& aboutToBeRemoved); QList oldIndexes(); static QList toContiguousPairs(const QList& ids); public: QHash oldIds; QList newInfos; QList modelRemovals; }; // ---------------------------------------------------------------------------------------------------- ImportImageModel::ImportImageModel(QObject* const parent) : QAbstractListModel(parent), d(new Private) { } ImportImageModel::~ImportImageModel() { delete d; } void ImportImageModel::setCameraThumbsController(CameraThumbsCtrl* const thumbsCtrl) { d->controller = thumbsCtrl->cameraController(); connect(d->controller, SIGNAL(signalFileList(CamItemInfoList)), SLOT(addCamItemInfos(CamItemInfoList))); connect(d->controller, SIGNAL(signalDeleted(QString,QString,bool)), SLOT(slotFileDeleted(QString,QString,bool))); connect(d->controller, SIGNAL(signalUploaded(CamItemInfo)), SLOT(slotFileUploaded(CamItemInfo))); } void ImportImageModel::setKeepsFileUrlCache(bool keepCache) { d->keepFileUrlCache = keepCache; } bool ImportImageModel::keepsFileUrlCache() const { return d->keepFileUrlCache; } bool ImportImageModel::isEmpty() const { return d->infos.isEmpty(); } CamItemInfo ImportImageModel::camItemInfo(const QModelIndex& index) const { if (!d->isValid(index)) { return CamItemInfo(); } return d->infos.at(index.row()); } CamItemInfo& ImportImageModel::camItemInfoRef(const QModelIndex& index) const { return d->infos[index.row()]; } qlonglong ImportImageModel::camItemId(const QModelIndex& index) const { if (!d->isValid(index)) { return -1; } return d->infos.at(index.row()).id; } QList ImportImageModel::camItemInfos(const QList& indexes) const { QList infos; foreach(const QModelIndex& index, indexes) { infos << camItemInfo(index); } return infos; } QList ImportImageModel::camItemIds(const QList& indexes) const { QList ids; foreach(const QModelIndex& index, indexes) { ids << camItemId(index); } return ids; } CamItemInfo ImportImageModel::camItemInfo(int row) const { if (row >= d->infos.size()) { return CamItemInfo(); } return d->infos.at(row); } CamItemInfo& ImportImageModel::camItemInfoRef(int row) const { return d->infos[row]; } qlonglong ImportImageModel::camItemId(int row) const { if (row < 0 || (row >= d->infos.size())) { return -1; } return d->infos.at(row).id; } QModelIndex ImportImageModel::indexForCamItemInfo(const CamItemInfo& info) const { return indexForCamItemId(info.id); } QList ImportImageModel::indexesForCamItemInfo(const CamItemInfo& info) const { return indexesForCamItemId(info.id); } QModelIndex ImportImageModel::indexForCamItemId(qlonglong id) const { int index = d->idHash.value(id, 0); if (index != -1) { return createIndex(index, 0); } return QModelIndex(); } QList ImportImageModel::indexesForCamItemId(qlonglong id) const { QList indexes; QHash::const_iterator it; for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it) { indexes << createIndex(it.value(), 0); } return indexes; } int ImportImageModel::numberOfIndexesForCamItemInfo(const CamItemInfo& info) const { return numberOfIndexesForCamItemId(info.id); } int ImportImageModel::numberOfIndexesForCamItemId(qlonglong id) const { int count = 0; QHash::const_iterator it; for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it) { ++count; } return count; } // static method CamItemInfo ImportImageModel::retrieveCamItemInfo(const QModelIndex& index) { if (!index.isValid()) { return CamItemInfo(); } ImportImageModel* const model = index.data(ImportImageModelPointerRole).value(); int row = index.data(ImportImageModelInternalId).toInt(); if (!model) { return CamItemInfo(); } return model->camItemInfo(row); } // static method qlonglong ImportImageModel::retrieveCamItemId(const QModelIndex& index) { if (!index.isValid()) { return -1; } ImportImageModel* const model = index.data(ImportImageModelPointerRole).value(); int row = index.data(ImportImageModelInternalId).toInt(); if (!model) { return -1; } return model->camItemId(row); } QModelIndex ImportImageModel::indexForUrl(const QUrl& fileUrl) const { if (d->keepFileUrlCache) { return indexForCamItemId(d->fileUrlHash.value(fileUrl.toLocalFile())); } else { const int size = d->infos.size(); for (int i = 0 ; i < size ; ++i) { if (d->infos.at(i).url() == fileUrl) { return createIndex(i, 0); } } } return QModelIndex(); } QList ImportImageModel::indexesForUrl(const QUrl& fileUrl) const { if (d->keepFileUrlCache) { return indexesForCamItemId(d->fileUrlHash.value(fileUrl.toLocalFile())); } else { QList indexes; const int size = d->infos.size(); for (int i = 0 ; i < size ; ++i) { if (d->infos.at(i).url() == fileUrl) { indexes << createIndex(i, 0); } } return indexes; } } CamItemInfo ImportImageModel::camItemInfo(const QUrl& fileUrl) const { if (d->keepFileUrlCache) { qlonglong id = d->fileUrlHash.value(fileUrl.toLocalFile()); if (id) { int index = d->idHash.value(id, -1); if (index != -1) { return d->infos.at(index); } } } else { foreach(const CamItemInfo& info, d->infos) { if (info.url() == fileUrl) { return info; } } } return CamItemInfo(); } QList ImportImageModel::camItemInfos(const QUrl& fileUrl) const { QList infos; if (d->keepFileUrlCache) { qlonglong id = d->fileUrlHash.value(fileUrl.toLocalFile()); if (id) { foreach(int index, d->idHash.values(id)) { infos << d->infos.at(index); } } } else { foreach(const CamItemInfo& info, d->infos) { if (info.url() == fileUrl) { infos << info; } } } return infos; } void ImportImageModel::addCamItemInfo(const CamItemInfo& info) { addCamItemInfos(QList() << info); } void ImportImageModel::addCamItemInfos(const CamItemInfoList& infos) { if (infos.isEmpty()) { return; } if (d->incrementalUpdater) { d->incrementalUpdater->appendInfos(infos); } else { appendInfos(infos); } } void ImportImageModel::addCamItemInfoSynchronously(const CamItemInfo& info) { addCamItemInfosSynchronously(QList() << info); } void ImportImageModel::addCamItemInfosSynchronously(const CamItemInfoList& infos) { if (infos.isEmpty()) { return; } publiciseInfos(infos); emit processAdded(infos); } void ImportImageModel::clearCamItemInfos() { + beginResetModel(); + d->infos.clear(); d->idHash.clear(); d->fileUrlHash.clear(); delete d->incrementalUpdater; d->incrementalUpdater = 0; d->reAdding = false; d->refreshing = false; d->incrementalRefreshRequested = false; - beginResetModel(); camItemInfosCleared(); endResetModel(); } // TODO unused void ImportImageModel::setCamItemInfos(const CamItemInfoList& infos) { clearCamItemInfos(); addCamItemInfos(infos); } QList ImportImageModel::camItemInfos() const { return d->infos; } QList ImportImageModel::camItemIds() const { return d->idHash.keys(); } QList ImportImageModel::uniqueCamItemInfos() const { QList uniqueInfos; const int size = d->infos.size(); for (int i = 0 ; i < size ; ++i) { const CamItemInfo& info = d->infos.at(i); if (d->idHash.value(info.id) == i) { uniqueInfos << info; } } return uniqueInfos; } bool ImportImageModel::hasImage(qlonglong id) const { return d->idHash.contains(id); } bool ImportImageModel::hasImage(const CamItemInfo& info) const { return d->fileUrlHash.contains(info.url().toLocalFile()); } void ImportImageModel::emitDataChangedForAll() { if (d->infos.isEmpty()) { return; } QModelIndex first = createIndex(0, 0); QModelIndex last = createIndex(d->infos.size() - 1, 0); emit dataChanged(first, last); } void ImportImageModel::emitDataChangedForSelections(const QItemSelection& selection) { if (!selection.isEmpty()) { foreach(const QItemSelectionRange& range, selection) { emit dataChanged(range.topLeft(), range.bottomRight()); } } } void ImportImageModel::appendInfos(const CamItemInfoList& infos) { if (infos.isEmpty()) { return; } publiciseInfos(infos); } void ImportImageModel::reAddCamItemInfos(const CamItemInfoList& infos) { publiciseInfos(infos); } void ImportImageModel::reAddingFinished() { d->reAdding = false; cleanSituationChecks(); } void ImportImageModel::slotFileDeleted(const QString& folder, const QString& file, bool status) { Q_UNUSED(status) QUrl url = QUrl::fromLocalFile(folder); url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + QLatin1Char('/') + file); CamItemInfo info = camItemInfo(url); removeCamItemInfo(info); } void ImportImageModel::slotFileUploaded(const CamItemInfo& info) { addCamItemInfo(info); } void ImportImageModel::startRefresh() { d->refreshing = true; } void ImportImageModel::finishRefresh() { d->refreshing = false; cleanSituationChecks(); } bool ImportImageModel::isRefreshing() const { return d->refreshing; } void ImportImageModel::cleanSituationChecks() { // For starting an incremental refresh we want a clear situation: // Any remaining batches from non-incremental refreshing subclasses have been received in appendInfos(), // any batches sent to preprocessor for re-adding have been re-added. if (d->refreshing || d->reAdding) { return; } if (d->incrementalRefreshRequested) { d->incrementalRefreshRequested = false; emit readyForIncrementalRefresh(); } else { emit allRefreshingFinished(); } } void ImportImageModel::publiciseInfos(const CamItemInfoList& infos) { if (infos.isEmpty()) { return; } emit itemInfosAboutToBeAdded(infos); const int firstNewIndex = d->infos.size(); const int lastNewIndex = d->infos.size() + infos.size() -1; beginInsertRows(QModelIndex(), firstNewIndex, lastNewIndex); d->infos << infos; for (int i = firstNewIndex ; i <= lastNewIndex ; ++i) { CamItemInfo& info = d->infos[i]; // TODO move this to a separate thread, see CameraHistoryUpdater // TODO can we/do we want to differentiate at all between whether the status is unknown and not downloaded? info.downloaded = CoreDbDownloadHistory::status(QString::fromUtf8(d->controller->cameraMD5ID()), info.name, info.size, info.ctime); // TODO is this safe? if so, is there a need to store this inside idHash separately? info.id = i; qlonglong id = info.id; d->idHash.insertMulti(id, i); if (d->keepFileUrlCache) { d->fileUrlHash[info.url().toLocalFile()] = id; } } endInsertRows(); emit processAdded(infos); emit itemInfosAdded(infos); } void ImportImageModel::requestIncrementalRefresh() { if (d->reAdding) { d->incrementalRefreshRequested = true; } else { emit readyForIncrementalRefresh(); } } bool ImportImageModel::hasIncrementalRefreshPending() const { return d->incrementalRefreshRequested; } void ImportImageModel::startIncrementalRefresh() { delete d->incrementalUpdater; d->incrementalUpdater = new ImportImageModelIncrementalUpdater(d); } void ImportImageModel::finishIncrementalRefresh() { if (!d->incrementalUpdater) { return; } // remove old entries QList > pairs = d->incrementalUpdater->oldIndexes(); removeRowPairs(pairs); // add new indexes appendInfos(d->incrementalUpdater->newInfos); delete d->incrementalUpdater; d->incrementalUpdater = 0; } template static bool pairsContain(const List& list, T value) { typename List::const_iterator middle; typename List::const_iterator begin = list.begin(); typename List::const_iterator end = list.end(); int n = int(end - begin); while (n > 0) { int half = n >> 1; middle = begin + half; if ((middle->first <= value) && (middle->second >= value)) { return true; } else if (middle->second < value) { begin = middle + 1; n -= half + 1; } else { n = half; } } return false; } void ImportImageModel::removeIndex(const QModelIndex& index) { removeIndexs(QList() << index); } void ImportImageModel::removeIndexs(const QList& indexes) { QList indexesList; foreach(const QModelIndex& index, indexes) { if (d->isValid(index)) { indexesList << index.row(); } } if (indexesList.isEmpty()) { return; } removeRowPairsWithCheck(ImportImageModelIncrementalUpdater::toContiguousPairs(indexesList)); } void ImportImageModel::removeCamItemInfo(const CamItemInfo& info) { removeCamItemInfos(QList() << info); } void ImportImageModel::removeCamItemInfos(const QList& infos) { QList indexesList; foreach(const CamItemInfo& info, infos) { QModelIndex index = indexForCamItemId(info.id); if (index.isValid()) { indexesList << index.row(); } } removeRowPairsWithCheck(ImportImageModelIncrementalUpdater::toContiguousPairs(indexesList)); } void ImportImageModel::setSendRemovalSignals(bool send) { d->sendRemovalSignals = send; } void ImportImageModel::removeRowPairsWithCheck(const QList >& toRemove) { if (d->incrementalUpdater) { d->incrementalUpdater->aboutToBeRemovedInModel(toRemove); } removeRowPairs(toRemove); } void ImportImageModel::removeRowPairs(const QList >& toRemove) { if (toRemove.isEmpty()) { return; } // Remove old indexes // Keep in mind that when calling beginRemoveRows all structures announced to be removed // must still be valid, and this includes our hashes as well, which limits what we can optimize int removedRows = 0; int offset = 0; foreach(const IntPair& pair, toRemove) { const int begin = pair.first - offset; const int end = pair.second - offset; removedRows = end - begin + 1; // when removing from the list, all subsequent indexes are affected offset += removedRows; QList removedInfos; if (d->sendRemovalSignals) { std::copy(d->infos.begin() + begin, d->infos.begin() + end, removedInfos.begin()); emit itemInfosAboutToBeRemoved(removedInfos); } itemInfosAboutToBeRemoved(begin, end); beginRemoveRows(QModelIndex(), begin, end); // update idHash - which points to indexes of d->infos QHash::iterator it; for (it = d->idHash.begin() ; it != d->idHash.end() ; ) { if (it.value() >= begin) { if (it.value() > end) { // after the removed interval, adjust index it.value() -= removedRows; } else { // in the removed interval it = d->idHash.erase(it); continue; } } ++it; } // remove from list d->infos.erase(d->infos.begin() + begin, d->infos.begin() + (end + 1)); endRemoveRows(); if (d->sendRemovalSignals) { emit itemInfosRemoved(removedInfos); } } // tidy up: remove old indexes from file path hash now if (d->keepFileUrlCache) { QHash::iterator it; for (it = d->fileUrlHash.begin() ; it!= d->fileUrlHash.end() ; ) { if (pairsContain(toRemove, it.value())) { it = d->fileUrlHash.erase(it); } else { ++it; } } } } // ------------ ImportImageModelIncrementalUpdater ------------ ImportImageModelIncrementalUpdater::ImportImageModelIncrementalUpdater(ImportImageModel::Private* const d) { oldIds = d->idHash; } void ImportImageModelIncrementalUpdater::aboutToBeRemovedInModel(const IntPairList& toRemove) { modelRemovals << toRemove; } void ImportImageModelIncrementalUpdater::appendInfos(const QList& infos) { for (int i = 0 ; i < infos.size() ; ++i) { const CamItemInfo& info = infos.at(i); bool found = false; QHash::iterator it; for (it = oldIds.find(info.id) ; it != oldIds.end() ; ++it) { if (it.key() == info.id) { found = true; break; } } if (found) { oldIds.erase(it); } else { newInfos << info; } } } QList > ImportImageModelIncrementalUpdater::toContiguousPairs(const QList& unsorted) { // Take the given indices and return them as contiguous pairs [begin, end] QList > pairs; if (unsorted.isEmpty()) { return pairs; } QList indices(unsorted); std::sort(indices.begin(), indices.end()); QPair pair(indices.first(), indices.first()); for (int i = 1 ; i < indices.size() ; ++i) { const int &index = indices.at(i); if (index == pair.second + 1) { pair.second = index; continue; } pairs << pair; // insert last pair pair.first = index; pair.second = index; } pairs << pair; return pairs; } QList > ImportImageModelIncrementalUpdater::oldIndexes() { // first, apply all changes to indexes by direct removal in model // while the updater was active foreach(const IntPairList& list, modelRemovals) { int removedRows = 0; int offset = 0; foreach(const IntPair& pair, list) { const int begin = pair.first - offset; const int end = pair.second - offset; // inclusive removedRows = end - begin + 1; // when removing from the list, all subsequent indexes are affected offset += removedRows; // update idHash - which points to indexes of d->infos, and these change now! QHash::iterator it; for (it = oldIds.begin() ; it != oldIds.end() ; ) { if (it.value() >= begin) { if (it.value() > end) { // after the removed interval: adjust index it.value() -= removedRows; } else { // in the removed interval it = oldIds.erase(it); continue; } } ++it; } } } modelRemovals.clear(); return toContiguousPairs(oldIds.values()); } // ------------ QAbstractItemModel implementation ------------- QVariant ImportImageModel::data(const QModelIndex& index, int role) const { if (!d->isValid(index)) { return QVariant(); } switch(role) { case Qt::DisplayRole: case Qt::ToolTipRole: return d->infos.at(index.row()).name; break; case ImportImageModelPointerRole: return QVariant::fromValue(const_cast(this)); break; case ImportImageModelInternalId: return index.row(); break; } return QVariant(); } QVariant ImportImageModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(section) Q_UNUSED(orientation) Q_UNUSED(role) return QVariant(); } int ImportImageModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } return d->infos.size(); } Qt::ItemFlags ImportImageModel::flags(const QModelIndex& index) const { if (!d->isValid(index)) { return 0; } Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled; f |= dragDropFlags(index); return f; } QModelIndex ImportImageModel::index(int row, int column, const QModelIndex& parent) const { if (column != 0 || row < 0 || parent.isValid() || row >= d->infos.size()) { return QModelIndex(); } return createIndex(row, 0); } } // namespace Digikam diff --git a/core/utilities/setup/collections/setupcollectionview.cpp b/core/utilities/setup/collections/setupcollectionview.cpp index 8ede072384..ab8aa08884 100644 --- a/core/utilities/setup/collections/setupcollectionview.cpp +++ b/core/utilities/setup/collections/setupcollectionview.cpp @@ -1,1110 +1,1111 @@ /* ============================================================ * * This file is a part of digiKam project * http://www.digikam.org * * Date : 2008-11-15 * Description : collections setup tab model/view * * Copyright (C) 2008-2012 by Marcel Wiesweg * Copyright (C) 2005-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. * * ============================================================ */ #define INTERNALID 65535 #include "setupcollectionview.h" // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE includes #include // Local includes #include "dmessagebox.h" #include "dfiledialog.h" #include "applicationsettings.h" #include "collectionlocation.h" #include "collectionmanager.h" #include "newitemsfinder.h" namespace Digikam { SetupCollectionDelegate::SetupCollectionDelegate(QAbstractItemView* const view, QObject* const parent) : DWItemDelegate(view, parent), m_categoryMaxStyledWidth(0) { // We keep a standard delegate that does all the normal drawing work for us // DWItemDelegate handles the widgets, for the rest of the work we act as a proxy to m_styledDelegate m_styledDelegate = new QStyledItemDelegate(parent); // forward all signals connect(m_styledDelegate, SIGNAL(closeEditor(QWidget*,QAbstractItemDelegate::EndEditHint)), this, SIGNAL(closeEditor(QWidget*,QAbstractItemDelegate::EndEditHint))); connect(m_styledDelegate, SIGNAL(commitData(QWidget*)), this, SIGNAL(commitData(QWidget*))); connect(m_styledDelegate, SIGNAL(sizeHintChanged(QModelIndex)), this, SIGNAL(sizeHintChanged(QModelIndex))); // For size hint. To get a valid size hint, the widgets seem to need a parent widget m_samplePushButton = new QPushButton(view); m_samplePushButton->hide(); m_sampleToolButton = new QToolButton(view); m_sampleToolButton->hide(); // Implement mapping of signals. Every button gets a mapping ID from the model m_categoryButtonMapper = new QSignalMapper(this); m_buttonMapper = new QSignalMapper(this); connect(m_categoryButtonMapper, SIGNAL(mapped(int)), this, SIGNAL(categoryButtonPressed(int))); connect(m_buttonMapper, SIGNAL(mapped(int)), this, SIGNAL(buttonPressed(int))); } SetupCollectionDelegate::~SetupCollectionDelegate() { } QList SetupCollectionDelegate::createItemWidgets(const QModelIndex& /*index*/) const { // We only need a push button for certain indexes and a tool button for others, // but we have no index here, but need to provide the widgets for each index QList list; QPushButton* const pushButton = new QPushButton(); list << pushButton; connect(pushButton, SIGNAL(clicked()), m_categoryButtonMapper, SLOT(map())); QToolButton* const toolButton = new QToolButton(); toolButton->setAutoRaise(true); list << toolButton; connect(toolButton, SIGNAL(clicked()), m_buttonMapper, SLOT(map())); return list; } QSize SetupCollectionDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { // get default size hint QSize hint = m_styledDelegate->sizeHint(option, index); // We need to handle those two cases where we display widgets if (index.data(SetupCollectionModel::IsCategoryRole).toBool()) { // get the largest size hint for the icon/text of all category entries int maxStyledWidth = 0; foreach(const QModelIndex& catIndex, static_cast(index.model())->categoryIndexes()) { maxStyledWidth = qMax(maxStyledWidth, m_styledDelegate->sizeHint(option, catIndex).width()); } const_cast(this)->m_categoryMaxStyledWidth = maxStyledWidth; // set real text on sample button to compute correct size hint m_samplePushButton->setText(index.data(SetupCollectionModel::CategoryButtonDisplayRole).toString()); QSize widgetHint = m_samplePushButton->sizeHint(); // add largest of the icon/text sizes (so that all buttons are aligned) and our button size hint hint.setWidth(m_categoryMaxStyledWidth + widgetHint.width()); hint.setHeight(qMax(hint.height(), widgetHint.height())); } else if (index.data(SetupCollectionModel::IsButtonRole).toBool()) { // set real pixmap on sample button to compute correct size hint QIcon pix = index.data(SetupCollectionModel::ButtonDecorationRole).value(); m_sampleToolButton->setIcon(index.data(SetupCollectionModel::ButtonDecorationRole).value()); QSize widgetHint = m_sampleToolButton->sizeHint(); // combine hints hint.setWidth(hint.width() + widgetHint.width()); hint.setHeight(qMax(hint.height(), widgetHint.height())); } return hint; } void SetupCollectionDelegate::updateItemWidgets(const QList widgets, const QStyleOptionViewItem& option, const QPersistentModelIndex& index) const { QPushButton* const pushButton = static_cast(widgets.at(0)); QToolButton* const toolButton = static_cast(widgets.at(1)); if (index.data(SetupCollectionModel::IsCategoryRole).toBool()) { // set text from model pushButton->setText(index.data(SetupCollectionModel::CategoryButtonDisplayRole).toString()); // resize according to size hint pushButton->resize(pushButton->sizeHint()); // move to position in line. We have cached the icon/text size hint from sizeHint() pushButton->move(m_categoryMaxStyledWidth, (option.rect.height() - pushButton->height()) / 2); pushButton->show(); toolButton->hide(); pushButton->setEnabled(itemView()->isEnabled()); // get the mapping id from model. The signal mapper will associate the id with the signal from this button. m_categoryButtonMapper->setMapping(pushButton, index.data(SetupCollectionModel::CategoryButtonMapId).toInt()); } else if (index.data(SetupCollectionModel::IsButtonRole).toBool()) { toolButton->setIcon(index.data(SetupCollectionModel::ButtonDecorationRole).value()); toolButton->resize(toolButton->sizeHint()); toolButton->move(0, (option.rect.height() - toolButton->height()) / 2); toolButton->show(); pushButton->hide(); toolButton->setEnabled(itemView()->isEnabled()); m_buttonMapper->setMapping(toolButton, index.data(SetupCollectionModel::ButtonMapId).toInt()); } else { pushButton->hide(); toolButton->hide(); } } QWidget* SetupCollectionDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { return m_styledDelegate->createEditor(parent, option, index); } bool SetupCollectionDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) { return static_cast(m_styledDelegate)->editorEvent(event, model, option, index); } void SetupCollectionDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { m_styledDelegate->paint(painter, option, index); } void SetupCollectionDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { m_styledDelegate->setEditorData(editor, index); } void SetupCollectionDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { m_styledDelegate->setModelData(editor, model, index); } void SetupCollectionDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const { m_styledDelegate->updateEditorGeometry(editor, option, index); } // ------------- View ----------------- // SetupCollectionTreeView::SetupCollectionTreeView(QWidget* const parent) : QTreeView(parent) { setRootIsDecorated(false); setExpandsOnDoubleClick(false); setHeaderHidden(true); // Set custom delegate setItemDelegate(new SetupCollectionDelegate(this, this)); } void SetupCollectionTreeView::setModel(SetupCollectionModel* collectionModel) { if (model()) { disconnect(model(), 0, this, 0); } // we need to do some things after the model has loaded its data connect(collectionModel, SIGNAL(collectionsLoaded()), this, SLOT(modelLoadedCollections())); // connect button click signals from the delegate to the model connect(static_cast(itemDelegate()), SIGNAL(categoryButtonPressed(int)), collectionModel, SLOT(slotCategoryButtonPressed(int))); connect(static_cast(itemDelegate()), SIGNAL(buttonPressed(int)), collectionModel, SLOT(slotButtonPressed(int))); // give model a widget to use as parent for message boxes collectionModel->setParentWidgetForDialogs(this); QTreeView::setModel(collectionModel); } void SetupCollectionTreeView::modelLoadedCollections() { // make category entries span the whole line for (int i = 0; i < model()->rowCount(QModelIndex()); ++i) { setFirstColumnSpanned(i, QModelIndex(), true); } // show all entries expandAll(); // Resize name and path column header()->setSectionResizeMode(SetupCollectionModel::ColumnName, QHeaderView::Stretch); header()->setSectionResizeMode(SetupCollectionModel::ColumnPath, QHeaderView::Stretch); // Resize last column, so that delete button is always rightbound header()->setStretchLastSection(false); // defaults to true header()->setSectionResizeMode(SetupCollectionModel::ColumnDeleteButton, QHeaderView::Fixed); resizeColumnToContents(SetupCollectionModel::ColumnDeleteButton); // Resize first column // This is more difficult because we need to ignore the width of the category entries, // which are formally location in the first column (although spanning the whole line). // resizeColumnToContents fails therefore. SetupCollectionModel* const collectionModel = static_cast(model()); QModelIndex categoryIndex = collectionModel->indexForCategory(SetupCollectionModel::CategoryLocal); QModelIndex firstChildOfFirstCategory = collectionModel->index(0, SetupCollectionModel::ColumnStatus, categoryIndex); QSize hint = sizeHintForIndex(firstChildOfFirstCategory); setColumnWidth(SetupCollectionModel::ColumnStatus, hint.width() + indentation()); } // ------------- Model ----------------- // SetupCollectionModel::Item::Item() : parentId(INTERNALID), deleted(false) { } SetupCollectionModel::Item::Item(const CollectionLocation& location) : location(location), deleted(false) { parentId = SetupCollectionModel::typeToCategory(location.type()); } SetupCollectionModel::Item::Item(const QString& path, const QString& label, SetupCollectionModel::Category category) : label(label), path(path), parentId(category), deleted(false) { } /* Internal data structure: The category entries get a model index with INTERNALID and are identified by their row(). The item entries get the index in m_collections as INTERNALID. No item is ever removed from m_collections, deleted entries are only marked as such. Items have a location, a parentId, and a name and label field. parentId always contains the category, needed to implement parent(). The location is the location if it exists, or is null if the item was added. Name and label are null if unchanged, then the values from location are used. They are valid if edited (label) or the location was added (both valid, location null). */ SetupCollectionModel::SetupCollectionModel(QObject* const parent) : QAbstractItemModel(parent), m_dialogParentWidget(0) { } SetupCollectionModel::~SetupCollectionModel() { } void SetupCollectionModel::loadCollections() { + beginResetModel(); + m_collections.clear(); QList locations = CollectionManager::instance()->allLocations(); foreach(const CollectionLocation& location, locations) { m_collections << Item(location); } - beginResetModel(); endResetModel(); emit collectionsLoaded(); } void SetupCollectionModel::apply() { QList newItems, deletedItems, renamedItems; for (int i = 0; i < m_collections.count(); ++i) { const Item& item = m_collections.at(i); if (item.deleted && !item.location.isNull()) // if item was deleted and had a valid location, i.e. exists in DB { deletedItems << i; } else if (!item.deleted && item.location.isNull()) // if item has no valid location, i.e. does not yet exist in db { newItems << i; } else if (!item.deleted && !item.location.isNull()) { // if item has a valid location, is not deleted, and has changed its label if (!item.label.isNull() && item.label != item.location.label()) { renamedItems << i; } } } // Delete deleted items foreach (int i, deletedItems) { Item& item = m_collections[i]; CollectionManager::instance()->removeLocation(item.location); item.location = CollectionLocation(); } // Add added items QList failedItems; foreach (int i, newItems) { Item& item = m_collections[i]; CollectionLocation location; if (item.parentId == CategoryRemote) { location = CollectionManager::instance()->addNetworkLocation(QUrl::fromLocalFile(item.path), item.label); } else { location = CollectionManager::instance()->addLocation(QUrl::fromLocalFile(item.path), item.label); } if (location.isNull()) { failedItems << item; } else { item.location = location; item.path.clear(); item.label.clear(); } } // Rename collections foreach (int i, renamedItems) { Item& item = m_collections[i]; CollectionManager::instance()->setLabel(item.location, item.label); item.label.clear(); } // Handle any errors if (!failedItems.isEmpty()) { QStringList failedPaths; foreach (const Item& item, failedItems) { failedPaths << QDir::toNativeSeparators(item.path); } DMessageBox::showInformationList(QMessageBox::Critical, m_dialogParentWidget, qApp->applicationName(), i18n("It was not possible to add a collection for the following paths:"), failedPaths); } // Trigger collection scan if (!newItems.isEmpty() || !deletedItems.isEmpty()) { NewItemsFinder* const tool = new NewItemsFinder(); tool->start(); } } void SetupCollectionModel::setParentWidgetForDialogs(QWidget* widget) { m_dialogParentWidget = widget; } void SetupCollectionModel::slotCategoryButtonPressed(int mappedId) { addCollection(mappedId); } void SetupCollectionModel::slotButtonPressed(int mappedId) { deleteCollection(mappedId); } void SetupCollectionModel::addCollection(int category) { if (category < 0 || category >= NumberOfCategories) { return; } QString picturesPath; if (m_collections.count() > 0) { const Item& item = m_collections[0]; picturesPath = item.path; } else { picturesPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); } QUrl curl = DFileDialog::getExistingDirectoryUrl(m_dialogParentWidget, i18n("Choose the folder containing your collection"), QUrl::fromLocalFile(picturesPath)); if (curl.isEmpty()) { return; } // Check path: First check with manager QString messageFromManager, deviceIcon; QList assumeDeleted; foreach(const Item& item, m_collections) { if (item.deleted && !item.location.isNull()) { assumeDeleted << item.location; } } CollectionManager::LocationCheckResult result; if (category == CategoryRemote) result = CollectionManager::instance()->checkNetworkLocation(curl, assumeDeleted, &messageFromManager, &deviceIcon); else result = CollectionManager::instance()->checkLocation(curl, assumeDeleted, &messageFromManager, &deviceIcon); QString path = QDir::fromNativeSeparators(curl.toDisplayString(QUrl::PreferLocalFile )); // If there are other added collections then CollectionManager does not know about them. Check here. foreach (const Item& item, m_collections) { if (!item.deleted && item.location.isNull()) { if (!item.path.isEmpty() && path.startsWith(item.path)) { if (path == item.path || path.startsWith(item.path + QLatin1Char('/'))) { messageFromManager = i18n("You have previously added a collection " "that contains the path \"%1\".", QDir::toNativeSeparators(path)); result = CollectionManager::LocationNotAllowed; break; } } } } // If check failed, display sorry message QString iconName; switch (result) { case CollectionManager::LocationAllRight: iconName = QLatin1String("dialog-ok-apply"); break; case CollectionManager::LocationHasProblems: iconName = QLatin1String("dialog-information"); break; case CollectionManager::LocationNotAllowed: case CollectionManager::LocationInvalidCheck: QMessageBox::warning(m_dialogParentWidget, i18n("Problem Adding Collection"), messageFromManager); // fail return; } // Create a dialog that displays volume information and allows to change the name of the collection QDialog* const dialog = new QDialog(m_dialogParentWidget); dialog->setWindowTitle(i18n("Adding Collection")); QWidget* const mainWidget = new QWidget(dialog); QLabel* const nameLabel = new QLabel; nameLabel->setText(i18n("Your new collection will be created with this name:")); nameLabel->setWordWrap(true); // lineedit for collection name QLineEdit* const nameEdit = new QLineEdit; nameEdit->setClearButtonEnabled(true); nameLabel->setBuddy(nameEdit); // label for the icon showing the type of storage (hard disk, CD, USB drive) QLabel* const deviceIconLabel = new QLabel; deviceIconLabel->setPixmap(QIcon::fromTheme(deviceIcon).pixmap(64)); QGroupBox* const infoBox = new QGroupBox; //infoBox->setTitle(i18n("More Information")); // label either signalling everything is all right, or raising awareness to some problems // (like handling of CD identified by a label) QLabel* const iconLabel = new QLabel; iconLabel->setPixmap(QIcon::fromTheme(iconName).pixmap(48)); QLabel* const infoLabel = new QLabel; infoLabel->setText(messageFromManager); infoLabel->setWordWrap(true); QHBoxLayout* const hbox1 = new QHBoxLayout; hbox1->addWidget(iconLabel); hbox1->addWidget(infoLabel); infoBox->setLayout(hbox1); QGridLayout* const grid1 = new QGridLayout; grid1->addWidget(deviceIconLabel, 0, 0, 3, 1); grid1->addWidget(nameLabel, 0, 1); grid1->addWidget(nameEdit, 1, 1); grid1->addWidget(infoBox, 2, 1); mainWidget->setLayout(grid1); QVBoxLayout* const vbx = new QVBoxLayout(dialog); QDialogButtonBox* const buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dialog); vbx->addWidget(mainWidget); vbx->addWidget(buttons); dialog->setLayout(vbx); connect(buttons->button(QDialogButtonBox::Ok), SIGNAL(clicked()), dialog, SLOT(accept())); connect(buttons->button(QDialogButtonBox::Cancel), SIGNAL(clicked()), dialog, SLOT(reject())); // default to directory name as collection name QDir dir(path); nameEdit->setText(dir.dirName()); if (dialog->exec() == QDialog::Accepted) { // Add new item to model. Adding to CollectionManager is done in apply()! QModelIndex parent = indexForCategory((Category)category); int row = rowCount(parent); QString label = nameEdit->text(); if (label.isEmpty()) { label.clear(); } beginInsertRows(parent, row, row); m_collections << Item(path, label, (Category)category); endInsertRows(); // only workaround for bug 182753 emit layoutChanged(); } } /* //This code works, but is currently not used. Was intended as a workaround for 219876. void SetupCollectionModel::emitDataChangedForChildren(const QModelIndex& parent) { int rows = rowCount(parent); int columns = columnCount(parent); emit dataChanged(index(0, 0, parent), index(rows, columns, parent)); for (int r = 0; r < rows; ++r) { for (int c = 0; c < columns; ++c) { QModelIndex i = index(r, c, parent); if (i.isValid()) emitDataChangedForChildren(i); } } } */ void SetupCollectionModel::deleteCollection(int internalId) { QModelIndex index = indexForId(internalId, (int)ColumnStatus); QModelIndex parentIndex = parent(index); if (!index.isValid() || internalId >= m_collections.count()) { return; } Item& item = m_collections[index.internalId()]; // Ask for confirmation QString label = data(indexForId(internalId, (int)ColumnName), Qt::DisplayRole).toString(); int result = QMessageBox::warning(m_dialogParentWidget, i18n("Remove Collection?"), i18n("Do you want to remove the collection \"%1\" from your list of collections?", label), QMessageBox::Yes | QMessageBox::No); if (result == QMessageBox::Yes) { // Remove from model. Removing from CollectionManager is done in apply()! beginRemoveRows(parentIndex, index.row(), index.row()); item.deleted = true; endRemoveRows(); // only workaround for bug 182753 emit layoutChanged(); } } QVariant SetupCollectionModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } if (index.internalId() == INTERNALID) { if (index.column() == 0) { switch (role) { case Qt::DisplayRole: switch (index.row()) { case CategoryLocal: return i18n("Local Collections"); case CategoryRemovable: return i18n("Collections on Removable Media"); case CategoryRemote: return i18n("Collections on Network Shares"); } break; case Qt::DecorationRole: switch (index.row()) { case CategoryLocal: return QIcon::fromTheme(QLatin1String("drive-harddisk")); case CategoryRemovable: return QIcon::fromTheme(QLatin1String("drive-removable-media")); case CategoryRemote: return QIcon::fromTheme(QLatin1String("network-wired-activated")); } break; case IsCategoryRole: return true; case CategoryButtonDisplayRole: return i18n("Add Collection"); case CategoryButtonMapId: return categoryButtonMapId(index); default: break; } } } else { const Item& item = m_collections.at(index.internalId()); switch (index.column()) { case ColumnName: if (role == Qt::DisplayRole || role == Qt::EditRole) { if (!item.label.isNull()) { return item.label; } if (!item.location.label().isNull()) { return item.location.label(); } return i18n("Col. %1", index.row()); } break; case ColumnPath: if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { if (!item.path.isNull()) { return QDir::toNativeSeparators(item.path); } //TODO: Path can be empty for items not available, // query more info from CollectionManager return QDir::toNativeSeparators(item.location.albumRootPath()); } break; case ColumnStatus: if (role == Qt::DecorationRole) { if (item.deleted) { return QIcon::fromTheme(QLatin1String("edit-delete")); } if (item.location.isNull()) { return QIcon::fromTheme(QLatin1String("folder-new")); } switch (item.location.status()) { case CollectionLocation::LocationAvailable: return QIcon::fromTheme(QLatin1String("dialog-ok-apply")); case CollectionLocation::LocationHidden: return QIcon::fromTheme(QLatin1String("object-locked")); case CollectionLocation::LocationUnavailable: switch (item.parentId) { case CategoryLocal: return QIcon::fromTheme(QLatin1String("drive-harddisk")).pixmap(16, QIcon::Disabled); case CategoryRemovable: return QIcon::fromTheme(QLatin1String("drive-removable-media-usb")).pixmap(16, QIcon::Disabled); case CategoryRemote: return QIcon::fromTheme(QLatin1String("network-wired-activated")).pixmap(16, QIcon::Disabled); } case CollectionLocation::LocationNull: case CollectionLocation::LocationDeleted: return QIcon::fromTheme(QLatin1String("edit-delete")); } } else if (role == Qt::ToolTipRole) { switch (item.location.status()) { case CollectionLocation::LocationUnavailable: return i18n("This collection is currently not available."); case CollectionLocation::LocationAvailable: return i18n("No problems found, enjoy this collection."); case CollectionLocation::LocationHidden: return i18n("This collection is hidden."); default: break; } } break; case ColumnDeleteButton: switch (role) { case Qt::ToolTipRole: return i18n("Remove collection"); case IsButtonRole: return true; case ButtonDecorationRole: return QIcon::fromTheme(QLatin1String("edit-delete")); case ButtonMapId: return buttonMapId(index); } break; } } return QVariant(); } QVariant SetupCollectionModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role == Qt::DisplayRole && orientation == Qt::Horizontal && section < NumberOfColumns) { switch (section) { case ColumnName: return i18n("Name"); case ColumnPath: return i18n("Path"); case ColumnStatus: return i18n("Status"); case ColumnDeleteButton: break; } } return QVariant(); } int SetupCollectionModel::rowCount(const QModelIndex& parent) const { if (!parent.isValid()) { return NumberOfCategories; // Level 0: the three top level items } if (parent.column() != 0) { return 0; } if (parent.internalId() != INTERNALID) { return 0; // Level 2: no children } // Level 1: item children count int parentId = parent.row(); int rowCount = 0; foreach (const Item& item, m_collections) { if (!item.deleted && item.parentId == parentId) { ++rowCount; } } return rowCount; } int SetupCollectionModel::columnCount(const QModelIndex& /*parent*/) const { return 4; } Qt::ItemFlags SetupCollectionModel::flags(const QModelIndex& index) const { if (!index.isValid()) { return Qt::NoItemFlags; } if (index.internalId() == INTERNALID) { return Qt::ItemIsEnabled; } else { switch (index.column()) { case ColumnName: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; default: return Qt::ItemIsEnabled | Qt::ItemIsSelectable; } } } bool SetupCollectionModel::setData(const QModelIndex& index, const QVariant& value, int role) { // only editable in one case if (index.isValid() && index.internalId() != INTERNALID && index.column() == ColumnName && role == Qt::EditRole) { Item& item = m_collections[index.internalId()]; item.label = value.toString(); emit dataChanged(index, index); } return false; } QModelIndex SetupCollectionModel::index(int row, int column, const QModelIndex& parent) const { if (!parent.isValid()) { if (row < NumberOfCategories && row >= 0 && column == 0) { return createIndex(row, 0, INTERNALID); } } else if (row >= 0 && column < 4) { // m_collections is a flat list with all entries, of all categories and also deleted entries. // The model indices contain as internal id the index to this list. int parentId = parent.row(); int rowCount = 0; for (int i = 0 ; i < m_collections.count() ; ++i) { const Item& item = m_collections.at(i); if (!item.deleted && item.parentId == parentId) { if (rowCount == row) { return createIndex(row, column, i); } ++rowCount; } } } return QModelIndex(); } QModelIndex SetupCollectionModel::parent(const QModelIndex& index) const { if (!index.isValid()) { return QModelIndex(); } if (index.internalId() == INTERNALID) { return QModelIndex(); // one of the three toplevel items } const Item& item = m_collections.at(index.internalId()); return createIndex(item.parentId, 0, INTERNALID); } QModelIndex SetupCollectionModel::indexForCategory(Category category) const { return index(category, 0, QModelIndex()); } QList SetupCollectionModel::categoryIndexes() const { QList list; for (int cat = 0 ; cat < NumberOfCategories ; ++cat) { list << index(cat, 0, QModelIndex()); } return list; } QModelIndex SetupCollectionModel::indexForId(int id, int column) const { int row = 0; const Item& indexItem = m_collections.at(id); for (int i = 0 ; i < m_collections.count() ; ++i) { const Item& item = m_collections.at(i); if (!item.deleted && item.parentId == indexItem.parentId) { if (i == id) { return createIndex(row, column, i); } ++row; } } return QModelIndex(); } SetupCollectionModel::Category SetupCollectionModel::typeToCategory(CollectionLocation::Type type) { switch (type) { default: case CollectionLocation::TypeVolumeHardWired: return CategoryLocal; case CollectionLocation::TypeVolumeRemovable: return CategoryRemovable; case CollectionLocation::TypeNetwork: return CategoryRemote; } } int SetupCollectionModel::categoryButtonMapId(const QModelIndex& index) const { if (!index.isValid() || index.parent().isValid()) { return INTERNALID; } return index.row(); } int SetupCollectionModel::buttonMapId(const QModelIndex& index) const { if (!index.isValid() || index.internalId() == INTERNALID) { return INTERNALID; } return index.internalId(); } } // namespace Digikam