File indexing completed on 2024-05-19 04:27:42

0001 /*
0002  * SPDX-FileCopyrightText: 2020 Boudewijn Rempt <boud@valdyas.org>
0003  * SPDX-FileCopyrightText: 2023 L. E. Segovia <amy@amyspark.me>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.0-or-later
0006  */
0007 #include "KisTagResourceModel.h"
0008 
0009 #include <QtSql>
0010 #include <QMap>
0011 
0012 #include <KisResourceLocator.h>
0013 #include <KisResourceModel.h>
0014 #include <KisResourceModelProvider.h>
0015 #include <KisResourceQueryMapper.h>
0016 #include <KisStorageModel.h>
0017 #include <kis_assert.h>
0018 
0019 struct KisAllTagResourceModel::Private {
0020     QString resourceType;
0021     QSqlQuery query;
0022     int columnCount { TagName + 1 };
0023     int cachedRowCount {-1};
0024 };
0025 
0026 
0027 KisAllTagResourceModel::KisAllTagResourceModel(const QString &resourceType, QObject *parent)
0028     : QAbstractTableModel(parent)
0029     , d(new Private())
0030 {
0031     d->resourceType = resourceType;
0032     resetQuery();
0033 
0034     connect(KisResourceLocator::instance(), SIGNAL(storageAdded(const QString&)), this, SLOT(addStorage(const QString&)));
0035     connect(KisResourceLocator::instance(), SIGNAL(storageRemoved(const QString&)), this, SLOT(removeStorage(const QString&)));
0036     connect(KisStorageModel::instance(), SIGNAL(storageEnabled(const QString&)), this, SLOT(addStorage(const QString&)));
0037     connect(KisStorageModel::instance(), SIGNAL(storageDisabled(const QString&)), this, SLOT(removeStorage(const QString&)));
0038     connect(KisResourceLocator::instance(), SIGNAL(resourceActiveStateChanged(const QString&, int)), this, SLOT(slotResourceActiveStateChanged(const QString&, int)));
0039 
0040     /**
0041      * TODO: connect to beginExternalResourceImport() and beginExternalResourceRemove
0042      *       as well. It seems to work without them somehow, but I guess it is just a
0043      *       coincidence or UB
0044      */
0045 }
0046 
0047 KisAllTagResourceModel::~KisAllTagResourceModel()
0048 {
0049     delete d;
0050 }
0051 
0052 int KisAllTagResourceModel::rowCount(const QModelIndex &parent) const
0053 {
0054     if (parent.isValid()) {
0055         return 0;
0056     }
0057 
0058     if (d->cachedRowCount < 0) {
0059         QSqlQuery q;
0060         const bool r = q.prepare("SELECT COUNT(DISTINCT resource_tags.tag_id || resources.name || resources.filename || resources.md5sum)\n"
0061                   "FROM   resource_tags\n"
0062                   ",      resources\n"
0063                   ",      resource_types\n"
0064                   "WHERE  resource_tags.resource_id = resources.id\n"
0065                   "AND    resources.resource_type_id = resource_types.id\n"
0066                   "AND    resource_types.name = :resource_type\n"
0067                   "AND    resource_tags.active = 1\n");
0068 
0069         if (!r) {
0070             qWarning() << "Could not execute resource/tags rowcount query" << q.lastError();
0071             return 0;
0072         }
0073 
0074         q.bindValue(":resource_type", d->resourceType);
0075 
0076         if (!q.exec()) {
0077             qWarning() << "Could not execute resource/tags rowcount query" << q.lastError();
0078             return 0;
0079         }
0080 
0081         q.first();
0082 
0083         const_cast<KisAllTagResourceModel*>(this)->d->cachedRowCount = q.value(0).toInt();
0084     }
0085     return d->cachedRowCount;
0086 }
0087 
0088 int KisAllTagResourceModel::columnCount(const QModelIndex &parent) const
0089 {
0090     if (parent.isValid()) {
0091         return 0;
0092     }
0093 
0094     return d->columnCount;
0095 }
0096 
0097 QVariant KisAllTagResourceModel::data(const QModelIndex &index, int role) const
0098 {
0099     QVariant v;
0100 
0101     if (!index.isValid()) { return v; }
0102     if (index.row() > rowCount()) { return v; }
0103     if (index.column() > d->columnCount) { return v;}
0104 
0105     bool pos = const_cast<KisAllTagResourceModel*>(this)->d->query.seek(index.row());
0106     if (!pos) {return v;}
0107 
0108     if (role < Qt::UserRole + TagId && index.column() < TagId) {
0109         return KisResourceQueryMapper::variantFromResourceQuery(d->query, index.column(), role, true);
0110     }
0111 
0112     if (role == Qt::FontRole) {
0113         return QFont();
0114     }
0115 
0116     if (index.column() >= TagId) {
0117         // trick to get the correct value without writing everything again
0118         // this is used for example in case of sorting
0119         role = Qt::UserRole + index.column();
0120     }
0121 
0122     // These are not shown, but needed for the filter
0123     switch(role) {
0124     case Qt::UserRole + TagId:
0125     {
0126         v = d->query.value("tag_id");
0127         break;
0128     }
0129     case Qt::UserRole + ResourceId:
0130     {
0131         v = d->query.value("resource_id");
0132         break;
0133     }
0134     case Qt::UserRole + Tag:
0135     {
0136         KisTagSP tag = KisResourceLocator::instance()->tagForUrl(d->query.value("tag_url").toString(), d->resourceType);
0137         v = QVariant::fromValue(tag);
0138         break;
0139     }
0140     case Qt::UserRole + Resource:
0141     {
0142         v = QVariant::fromValue(KisResourceLocator::instance()->resourceForId(d->query.value("resource_id").toInt()));
0143         break;
0144     }
0145     case Qt::UserRole + ResourceActive:
0146     {
0147         v = d->query.value("resource_active");
0148         break;
0149     }
0150     case Qt::UserRole + TagActive:
0151     {
0152         v = d->query.value("tag_active");
0153         break;
0154     }
0155     case Qt::UserRole + ResourceStorageActive:
0156     {
0157         v = d->query.value("resource_storage_active");
0158         break;
0159     }
0160     case Qt::UserRole + ResourceName:
0161     {
0162         v = d->query.value("resource_name");
0163         break;
0164     }
0165     case Qt::UserRole + TagName:
0166     {
0167         v = d->query.value("translated_name");
0168         if (v.isNull()) {
0169             v = d->query.value("tag_name");
0170         }
0171         break;
0172     }
0173 
0174     default:
0175         ;
0176     }
0177     return v;
0178 }
0179 
0180 bool KisAllTagResourceModel::tagResources(const KisTagSP tag, const QVector<int>& resourceIds)
0181 {
0182     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(tag && tag->valid() && tag->id() >= 0, false);
0183 
0184     // notes for performance:
0185     // the only two costly parts are:
0186     // - executing the query constructed by createQuery()
0187     // - running endInsertRows() (because it updates all the views and filter proxies etc...)
0188 
0189     QVector<int> resourceIdsToAdd;
0190     QVector<int> resourceIdsToUpdate;
0191 
0192     // looks expensive but actually isn't
0193     for (int i = 0; i < resourceIds.count(); i++) {
0194         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(resourceIds[i] >= 0, false);
0195         int taggedState = isResourceTagged(tag, resourceIds[i]);
0196         switch (taggedState) {
0197         case -1:
0198             // never tagged
0199             resourceIdsToAdd.append(resourceIds[i]);
0200             break;
0201         case 0:
0202             // tagged but then untagged
0203             resourceIdsToUpdate.append(resourceIds[i]);
0204             break;
0205         case 1:
0206             // it means the resource is already tagged; do nothing
0207             break;
0208         }
0209     }
0210 
0211     if (resourceIdsToAdd.isEmpty() && resourceIdsToUpdate.isEmpty()) {
0212         // Is already tagged, let's do nothing
0213         return true;
0214     }
0215 
0216     int howManyTimesBeginInsertedCalled = 0;
0217 
0218     if (resourceIdsToUpdate.count() > 0) {
0219 
0220         QSqlQuery allIndices;
0221         if (!allIndices.prepare(createQuery(false, true))) {
0222             qWarning() << "Could not prepare tagResource-allIndices query" << allIndices.lastError();
0223         }
0224 
0225         allIndices.bindValue(":resource_type", d->resourceType);
0226         allIndices.bindValue(":language", KisTag::currentLocale());
0227 
0228         if (!allIndices.exec()) {
0229             qWarning() << "Could not execute tagResource-allIndices query" << allIndices.lastError();
0230         }
0231 
0232         int activesRowId = -1;
0233         int lastActiveRowId = -1;
0234 
0235         // needed for beginInsertRows calculations
0236         QMap<int, int> resourcesCountForLastActiveRowId;
0237 
0238         int idd = 0;
0239 
0240         while (allIndices.next()) {
0241             idd++;
0242             bool isActive = allIndices.value("resource_tags_pair_active").toBool();
0243             if (isActive) {
0244                 activesRowId++;
0245                 lastActiveRowId = activesRowId;
0246             } else {
0247                 bool variantSuccess = true;
0248                 int rowTagId = allIndices.value("tag_id").toInt(&variantSuccess);
0249                 KIS_SAFE_ASSERT_RECOVER(variantSuccess) { rowTagId = -1; }
0250                 int rowResourceId = allIndices.value("resource_id").toInt(&variantSuccess);
0251                 KIS_SAFE_ASSERT_RECOVER(variantSuccess) { rowResourceId = -1; }
0252                 if (rowTagId == tag->id() && resourceIdsToUpdate.contains(rowResourceId)) {
0253                     if (!resourcesCountForLastActiveRowId.contains(lastActiveRowId)) {
0254                         resourcesCountForLastActiveRowId[lastActiveRowId] = 1;
0255                     } else {
0256                         resourcesCountForLastActiveRowId[lastActiveRowId] = resourcesCountForLastActiveRowId[lastActiveRowId] + 1;
0257                     }
0258                 }
0259             }
0260          }
0261 
0262         Q_FOREACH(const int key, resourcesCountForLastActiveRowId.keys()) {
0263             // when having multiple beginInsertRows:
0264             // let's say you have this model:
0265             // 0  A
0266             // 1  A
0267             // 2  A
0268             //  <- put 2 Bs here
0269             // 3  A
0270             //  <- put one B here
0271             // 4  A
0272             // and you want to add the two Bs and then add another B
0273             // then the signals should be:
0274             // beginRemoveRows(3, 4) <- new indices of the two Bs
0275             // beginRemoveRows(4, 4) <- new index of the one B ignoring the action before
0276 
0277 
0278             beginInsertRows(QModelIndex(), key + 1, key + resourcesCountForLastActiveRowId[key]);
0279             howManyTimesBeginInsertedCalled++;
0280         }
0281 
0282         // Resource was tagged, then untagged. Tag again;
0283         QSqlQuery q;
0284 
0285         if (!q.prepare("UPDATE resource_tags\n"
0286                        "SET    active = 1\n"
0287                        "WHERE  resource_id = :resource_id\n"
0288                        "AND    tag_id      = :tag_id")) {
0289 
0290 
0291             qWarning() << "Could not prepare update resource_tags to active statement" << q.lastError();
0292 
0293             return false;
0294         }
0295 
0296         QVariantList resourceIdsVariants;
0297         QVariantList tagIdVariants;
0298 
0299         for (int i = 0; i < resourceIdsToUpdate.count(); i++) {
0300             resourceIdsVariants << QVariant(resourceIdsToUpdate[i]);
0301             tagIdVariants << QVariant(tag->id());
0302         }
0303 
0304         q.bindValue(":resource_id", resourceIdsVariants);
0305         q.bindValue(":tag_id", tagIdVariants);
0306 
0307         if (!q.execBatch()) {
0308             qWarning() << "Could not execute update resource_tags to active statement" << q.lastError();
0309             for (int i = 0; i < howManyTimesBeginInsertedCalled; i++) {
0310                 endInsertRows();
0311             }
0312             return false;
0313         }
0314 
0315     }
0316 
0317     if (resourceIdsToAdd.count() > 0) {
0318 
0319         beginInsertRows(QModelIndex(), rowCount(), rowCount() + resourceIdsToAdd.count() - 1);
0320         howManyTimesBeginInsertedCalled++;
0321 
0322         // Resource was never tagged before, insert it. The active column is DEFAULT 1
0323         QSqlQuery q;
0324 
0325 
0326         QString values;
0327         for (int i = 0; i < resourceIdsToAdd.count(); i++) {
0328             if (i > 0) {
0329                 values.append(", ");
0330             }
0331             values.append("(?, ?, ?)");
0332         }
0333 
0334         if (!q.prepare(QString("INSERT INTO resource_tags\n"
0335                        "(resource_id, tag_id, active)\n"
0336                        "VALUES ") + values + QString(";\n"))) {
0337             qWarning() << "Could not prepare insert into resource tags statement" << q.lastError();
0338             for (int i = 0; i < howManyTimesBeginInsertedCalled; i++) {
0339                 endInsertRows();
0340             }
0341             return false;
0342         }
0343 
0344         for (int i = 0; i < resourceIdsToAdd.count(); i++) {
0345             q.addBindValue(resourceIdsToAdd[i]);
0346             q.addBindValue(tag->id());
0347             q.addBindValue(true);
0348 
0349         }
0350 
0351         if (!q.exec()) {
0352             qWarning() << "Could not execute insert into resource tags statement" << q.boundValues() << q.lastError();
0353             for (int i = 0; i < howManyTimesBeginInsertedCalled; i++) {
0354                 endInsertRows();
0355             }
0356             return false;
0357         }
0358     }
0359 
0360     resetQuery();
0361 
0362     for (int i = 0; i < howManyTimesBeginInsertedCalled; i++) {
0363         endInsertRows();
0364     }
0365 
0366     return true;
0367 }
0368 
0369 bool KisAllTagResourceModel::untagResources(const KisTagSP tag, const QVector<int> &resourceIds)
0370 {
0371     if (!tag || !tag->valid()) return false;
0372     if (!d->query.isSelect()) return false;
0373     if (rowCount() < 1) return false;
0374 
0375     int beginRemoveRowsCount = 0;
0376 
0377     QSqlQuery q;
0378 
0379     if (!q.prepare("UPDATE resource_tags\n"
0380                    "SET    active      = 0\n"
0381                    "WHERE  tag_id      = :tag_id\n"
0382                    "AND    resource_id = :resource_id")) {
0383         qWarning() << "Could not prepare untagResource-update query" << q.lastError();
0384         return false;
0385     }
0386 
0387     QSqlQuery allIndices;
0388     if (!allIndices.prepare(createQuery(true, true))) {
0389         qWarning() << "Could not prepare untagResource-allIndices query " << allIndices.lastError();
0390     }
0391 
0392     allIndices.bindValue(":resource_type", d->resourceType);
0393     allIndices.bindValue(":language", KisTag::currentLocale());
0394 
0395     if (!allIndices.exec()) {
0396         qCritical() << "Could not exec untagResource-allIndices query " << allIndices.lastError();
0397     }
0398 
0399     int activesRowId = -1;
0400     int lastActiveRowId = -1;
0401 
0402     // needed for beginInsertRows indices calculations
0403     QMap<int, int> resourcesCountForLastActiveRowId;
0404 
0405     int idd = 0;
0406 
0407     while (allIndices.next()) {
0408         idd++;
0409         bool variantSuccess = true;
0410 
0411         bool isActive = true; // all of them are active!
0412         KIS_SAFE_ASSERT_RECOVER(variantSuccess) { isActive = false; }
0413 
0414         int rowTagId = allIndices.value("tag_id").toInt(&variantSuccess);
0415         KIS_SAFE_ASSERT_RECOVER(variantSuccess) { rowTagId = -1; }
0416         int rowResourceId = allIndices.value("resource_id").toInt(&variantSuccess);
0417         KIS_SAFE_ASSERT_RECOVER(variantSuccess) { rowResourceId = -1; }
0418 
0419         bool willStayActive = isActive && (rowTagId != tag->id() || !resourceIds.contains(rowResourceId));
0420         activesRowId++;
0421         if (willStayActive) {
0422             lastActiveRowId = activesRowId;
0423         } else if (isActive) {
0424             // means we're removing it
0425             if (!resourcesCountForLastActiveRowId.contains(lastActiveRowId)) {
0426                 resourcesCountForLastActiveRowId[lastActiveRowId] = 0;
0427             }
0428             resourcesCountForLastActiveRowId[lastActiveRowId]++;
0429         }
0430     }
0431 
0432     Q_FOREACH(const int key, resourcesCountForLastActiveRowId.keys()) {
0433         // when having multiple beginRemoveRows:
0434         // let's say you have this model:
0435         // 0  A
0436         // 1  A
0437         // 2  A
0438         // 3  *B*
0439         // 4  *B*
0440         // 5  A
0441         // 6  *B*
0442         // 7  A
0443         // and you want to remove all Bs
0444         // then the signals should be:
0445         // beginRemoveRows(3, 4) <- first two indices in the obvious way
0446         // beginRemoveRows(4, 4) <- next index as if the first action was already done
0447         //  (so `5  A` already became `3  A`, so the *B* is `4  B`, not `6  B`)
0448 
0449         beginRemoveRows(QModelIndex(), key + 1, key + resourcesCountForLastActiveRowId[key]);
0450         beginRemoveRowsCount++;
0451     }
0452 
0453     QSqlDatabase::database().transaction();
0454     for (int i = 0; i < resourceIds.count(); i++) {
0455         int resourceId = resourceIds[i];
0456 
0457         if (resourceId < 0) continue;
0458         if (isResourceTagged(tag, resourceId) < 1) continue;
0459 
0460         q.bindValue(":tag_id", tag->id());
0461         q.bindValue(":resource_id", resourceId);
0462 
0463         if (!q.exec()) {
0464             qWarning() << "Could not execute untagResource-update query" << q.lastError() << q.boundValues();
0465             for (int i = 0; i < beginRemoveRowsCount; i++) {
0466                 endRemoveRows();
0467             }
0468             QSqlDatabase::database().rollback();
0469             return false;
0470         }
0471     }
0472     QSqlDatabase::database().commit();
0473 
0474     if (beginRemoveRowsCount > 0) {
0475         resetQuery();
0476         for (int i = 0; i < beginRemoveRowsCount; i++) {
0477             endRemoveRows();
0478         }
0479     }
0480 
0481     return true;
0482 }
0483 
0484 int KisAllTagResourceModel::isResourceTagged(const KisTagSP tag, const int resourceId)
0485 {
0486     QSqlQuery query;
0487     bool r = query.prepare("SELECT resource_tags.active\n"
0488                            "FROM   resource_tags\n"
0489                            "WHERE  resource_tags.resource_id = :resource_id\n"
0490                            "AND    resource_tags.tag_id = :tag_id\n");
0491 
0492     if (!r) {
0493         qWarning() << "Could not prepare bool KisAllTagResourceModel::checkResourceTaggedState query" << query.lastError();
0494         return false;
0495     }
0496 
0497     query.bindValue(":resource_id", resourceId);
0498     query.bindValue(":tag_id", tag->id());
0499 
0500     if (!query.exec()) {
0501         qWarning() << "Could not execute is resource tagged with a specific tag query" << query.boundValues() << query.lastError();
0502         return false;
0503     }
0504 
0505     r = query.first();
0506     if (!r) {
0507         // Resource was not tagged
0508         return -1;
0509     }
0510 
0511     return query.value(0).toInt() > 0;
0512 }
0513 
0514 void KisAllTagResourceModel::addStorage(const QString &location)
0515 {
0516     Q_UNUSED(location);
0517     beginInsertRows(QModelIndex(), rowCount(), rowCount());
0518     resetQuery();
0519     endInsertRows();
0520 }
0521 
0522 void KisAllTagResourceModel::removeStorage(const QString &location)
0523 {
0524     Q_UNUSED(location);
0525     beginRemoveRows(QModelIndex(), rowCount(), rowCount());
0526     resetQuery();
0527     endRemoveRows();
0528 }
0529 
0530 void KisAllTagResourceModel::slotResourceActiveStateChanged(const QString &resourceType, int resourceId)
0531 {
0532     if (resourceType != d->resourceType) return;
0533     if (resourceId < 0) return;
0534 
0535     resetQuery();
0536 
0537     /// The model has multiple rows for every resource, one row per tag,
0538     /// so we need to notify about the changes in all the tags
0539     QVector<QModelIndex> indexes;
0540 
0541     for (int i = 0; i < rowCount(); ++i)  {
0542         const QModelIndex idx = this->index(i, 0);
0543         KIS_ASSERT_RECOVER(idx.isValid()) { continue; }
0544 
0545         if (idx.data(Qt::UserRole + KisAllTagResourceModel::ResourceId).toInt() == resourceId) {
0546             indexes << idx;
0547         }
0548     }
0549 
0550     Q_FOREACH(const QModelIndex &index, indexes) {
0551         Q_EMIT dataChanged(index, index, {Qt::CheckStateRole, Qt::UserRole + KisAllTagResourceModel::ResourceActive});
0552     }
0553 }
0554 
0555 QString KisAllTagResourceModel::createQuery(bool onlyActive, bool returnADbIndexToo)
0556 {
0557     QString query = QString("WITH initial_selection AS (\n"
0558                             "    SELECT   tags.id\n"
0559                             "    ,        resources.name\n"
0560                             "    ,        resources.filename\n"
0561                             "    ,        resources.md5sum\n"
0562                             "    ,        resource_types.id            as    resource_type_id\n"
0563                             "    ,        resource_types.name          as    resource_type_name\n"
0564                             "    ,        min(resources.id)            as    resource_id\n"
0565                             ) + (returnADbIndexToo ? QString(", resource_tags.id   as   resource_tags_row_id\n") : QString("")) + QString( // include r_t row id
0566                             ) + (onlyActive ? QString("") : QString(", resource_tags.active   as   resource_tags_pair_active\n")) + QString( // include r_t row active info
0567                             "    FROM     resource_types\n"
0568                             "    JOIN     resource_tags\n   ON       resource_tags.resource_id    = resources.id\n"
0569                             ) + (onlyActive ? QString("    AND       resource_tags.active         = 1\n") : QString("")) + QString( // make sure only active tags are used
0570                             "    JOIN     resources         ON       resources.resource_type_id   = resource_types.id\n"
0571                             "    JOIN     tags              ON       tags.id                      = resource_tags.tag_id\n"
0572                             "                              AND       tags.resource_type_id        = resource_types.id\n"
0573                             "    WHERE    resource_types.name          = :resource_type\n"
0574                             "    GROUP BY tags.id\n"
0575                             "    ,        resources.name\n"
0576                             "    ,        resources.filename\n"
0577                             "    ,        resources.md5sum\n"
0578                             "    ,        resource_types.id\n"
0579                             "    ORDER BY resource_tags.id\n"
0580                             ")\n"
0581                             "SELECT \n"
0582                             "       initial_selection.id           as tag_id\n"
0583                             ",      initial_selection.name         as resource_name\n"
0584                             ",      initial_selection.filename     as resource_filename\n"
0585                             ",      initial_selection.md5sum       as resource_md5sum\n"
0586                             ",      initial_selection.resource_id  as resource_id\n"
0587                             ",      tags.url                       as tag_url"
0588                             ",      tags.active                    as tag_active"
0589                             ",      tags.name                      as tag_name"
0590                             ",      tags.comment                   as tag_comment"
0591                             ",      resources.status               as resource_active\n"
0592                             ",      resources.tooltip              as resource_tooltip\n"
0593                             ",      resources.status               as resource_active\n"
0594                             ",      resources.storage_id           as storage_id\n"
0595                             ",      storages.active                as resource_storage_active\n"
0596                             ",      storages.location              as location\n"
0597                             ",      tag_translations.name          as translated_name\n"
0598                             ",      tag_translations.comment       as translated_comment\n"
0599                             ",      initial_selection.resource_type_name as resource_type\n"
0600                             ) + (returnADbIndexToo ? QString(", initial_selection.resource_tags_row_id   as   resource_tags_row_id\n") : QString("")) + QString(
0601                             ) + (onlyActive ? QString("") : QString(", initial_selection.resource_tags_pair_active   as   resource_tags_pair_active\n")) + QString(
0602                             "FROM      initial_selection\n"
0603                             "JOIN      tags               ON   tags.id                     = initial_selection.id\n"
0604                             "                            AND   tags.resource_type_id       = initial_selection.resource_type_id\n"
0605                             "JOIN      resources          ON   resources.id                = resource_id\n"
0606                             "JOIN      storages           ON   storages.id                 = resources.storage_id\n"
0607                             "LEFT JOIN tag_translations   ON   tag_translations.tag_id     = initial_selection.id\n"
0608                             "                            AND   tag_translations.language   = :language\n");
0609 
0610     return query;
0611 }
0612 
0613 // Keep in sync with KisAllResourcesModel::headerData for sections 1-16
0614 // then KisAllTagResourceModel::data for 16-25
0615 QVariant KisAllTagResourceModel::headerData(int section, Qt::Orientation orientation, int role) const
0616 {
0617     if (orientation != Qt::Horizontal) {
0618         return {};
0619     }
0620     if (role != Qt::DisplayRole) {
0621         return {};
0622     }
0623 
0624     switch (section) {
0625     case KisAbstractResourceModel::Id:
0626         return i18n("Id");
0627     case KisAbstractResourceModel::StorageId:
0628         return i18n("Storage ID");
0629     case KisAbstractResourceModel::Name:
0630         return i18n("Name");
0631     case KisAbstractResourceModel::Filename:
0632         return i18n("File Name");
0633     case KisAbstractResourceModel::Tooltip:
0634         return i18n("Tooltip");
0635     case KisAbstractResourceModel::Thumbnail:
0636         return i18n("Image");
0637     case KisAbstractResourceModel::Status:
0638         return i18n("Status");
0639     case KisAbstractResourceModel::Location:
0640         return i18n("Location");
0641     case KisAbstractResourceModel::ResourceType:
0642         return i18n("Resource Type");
0643     case KisAbstractResourceModel::ResourceActive:
0644         return i18n("Active");
0645     case KisAbstractResourceModel::StorageActive:
0646         return i18n("Storage Active");
0647     case KisAbstractResourceModel::MD5:
0648         return i18n("md5sum");
0649     case KisAbstractResourceModel::Tags:
0650         return i18n("Tags");
0651     case KisAbstractResourceModel::LargeThumbnail:
0652         return i18n("Large Thumbnail");
0653     case KisAbstractResourceModel::Dirty:
0654         return i18n("Dirty");
0655     case KisAbstractResourceModel::MetaData:
0656         return i18n("Metadata");
0657     default:
0658         role = Qt::UserRole + section;
0659         break;
0660     }
0661 
0662     switch (role) {
0663     case Qt::UserRole + KisAllTagResourceModel::TagId:
0664         return i18n("Tag ID");
0665     case Qt::UserRole + KisAllTagResourceModel::ResourceId:
0666         return i18n("Resource ID");
0667     case Qt::UserRole + KisAllTagResourceModel::Tag:
0668         return i18n("Tag Url");
0669     case Qt::UserRole + KisAllTagResourceModel::Resource:
0670         return i18n("Resource");
0671     case Qt::UserRole + ResourceActive:
0672         return i18n("Resource Active");
0673     case Qt::UserRole + TagActive:
0674         return i18n("Tag Active");
0675     case Qt::UserRole + ResourceStorageActive:
0676         return i18n("Storage Active");
0677     case Qt::UserRole + ResourceName:
0678         return i18n("Resource Name");
0679     case Qt::UserRole + TagName:
0680         return i18n("Tag File Name");
0681     default:
0682         break;
0683     }
0684 
0685     return {};
0686 }
0687 
0688 bool KisAllTagResourceModel::resetQuery()
0689 {
0690     bool r = d->query.prepare(createQuery(true));
0691 
0692     if (!r) {
0693         qWarning() << "Could not prepare KisAllTagResourcesModel query" << d->query.lastError();
0694     }
0695 
0696     d->query.bindValue(":resource_type", d->resourceType);
0697     d->query.bindValue(":language", KisTag::currentLocale());
0698 
0699     r = d->query.exec();
0700 
0701     if (!r) {
0702         qWarning() << "Could not execute KisAllTagResourcesModel query" << d->query.lastError();
0703     }
0704 
0705     d->cachedRowCount = -1;
0706 
0707     return r;
0708 }
0709 
0710 
0711 struct KisTagResourceModel::Private {
0712     QString resourceType;
0713     KisAllTagResourceModel *sourceModel {0};
0714     QVector<int> tagIds;
0715     QVector<int> resourceIds;
0716     TagFilter tagFilter {ShowActiveTags};
0717     StorageFilter storageFilter {ShowActiveStorages};
0718     ResourceFilter resourceFilter {ShowActiveResources};
0719 };
0720 
0721 
0722 KisTagResourceModel::KisTagResourceModel(const QString &resourceType, QObject *parent)
0723     : QSortFilterProxyModel(parent)
0724     , d(new Private())
0725 {
0726     d->resourceType = resourceType;
0727     d->sourceModel = KisResourceModelProvider::tagResourceModel(resourceType);
0728     setSourceModel(d->sourceModel);
0729 
0730     connect(KisResourceLocator::instance(), SIGNAL(storageAdded(const QString &)), this, SLOT(storageChanged(const QString &)));
0731     connect(KisResourceLocator::instance(), SIGNAL(storageRemoved(const QString &)), this, SLOT(storageChanged(const QString &)));
0732     connect(KisStorageModel::instance(), SIGNAL(storageEnabled(const QString &)), this, SLOT(storageChanged(const QString &)));
0733     connect(KisStorageModel::instance(), SIGNAL(storageDisabled(const QString &)), this, SLOT(storageChanged(const QString &)));
0734 }
0735 
0736 KisTagResourceModel::~KisTagResourceModel()
0737 {
0738     delete d;
0739 }
0740 
0741 void KisTagResourceModel::setTagFilter(KisTagResourceModel::TagFilter filter)
0742 {
0743     d->tagFilter = filter;
0744     invalidateFilter();
0745 }
0746 
0747 void KisTagResourceModel::setResourceFilter(KisTagResourceModel::ResourceFilter filter)
0748 {
0749     d->resourceFilter = filter;
0750     invalidateFilter();
0751 }
0752 
0753 void KisTagResourceModel::setStorageFilter(KisTagResourceModel::StorageFilter filter)
0754 {
0755     d->storageFilter = filter;
0756     invalidateFilter();
0757 }
0758 
0759 bool KisTagResourceModel::tagResources(const KisTagSP tag, const QVector<int> &resourceIds)
0760 {
0761     bool r = d->sourceModel->tagResources(tag, resourceIds);
0762     return r;
0763 }
0764 
0765 bool KisTagResourceModel::untagResources(const KisTagSP tag, const QVector<int> &resourceIds)
0766 {
0767     return d->sourceModel->untagResources(tag, resourceIds);
0768 }
0769 
0770 int KisTagResourceModel::isResourceTagged(const KisTagSP tag, const int resourceId)
0771 {
0772     return d->sourceModel->isResourceTagged(tag, resourceId);
0773 }
0774 
0775 void KisTagResourceModel::setTagsFilter(const QVector<int> tagIds)
0776 {
0777     d->tagIds = tagIds;
0778     invalidateFilter();
0779 }
0780 
0781 void KisTagResourceModel::setResourcesFilter(const QVector<int> resourceIds)
0782 {
0783     d->resourceIds = resourceIds;
0784     invalidateFilter();
0785 }
0786 
0787 void KisTagResourceModel::setTagsFilter(const QVector<KisTagSP> tags)
0788 {
0789     d->tagIds.clear();
0790     Q_FOREACH(const KisTagSP tag, tags) {
0791         if (tag && tag->valid() && tag->id() > -1) {
0792             d->tagIds << tag->id();
0793         }
0794     }
0795     invalidateFilter();
0796 }
0797 
0798 void KisTagResourceModel::setResourcesFilter(const QVector<KoResourceSP> resources)
0799 {
0800     d->resourceIds.clear();
0801     Q_FOREACH(const KoResourceSP resource, resources) {
0802         if (resource->valid() && resource->resourceId() > -1) {
0803             d->resourceIds << resource->resourceId();
0804         }
0805     }
0806     invalidateFilter();
0807 }
0808 
0809 bool KisTagResourceModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
0810 {
0811     QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
0812     if (!idx.isValid()) return false;
0813 
0814     int tagId = idx.data(Qt::UserRole + KisAllTagResourceModel::TagId).toInt();
0815     int resourceId = idx.data(Qt::UserRole + KisAllTagResourceModel::ResourceId).toInt();
0816     bool tagActive = idx.data(Qt::UserRole + KisAllTagResourceModel::TagActive).toBool();
0817     bool resourceActive = idx.data(Qt::UserRole + KisAllTagResourceModel::ResourceActive).toBool();
0818     bool resourceStorageActive = idx.data(Qt::UserRole + KisAllTagResourceModel::ResourceStorageActive).toBool();
0819 
0820     if (d->tagFilter == ShowAllTags && d->resourceFilter == ShowAllResources && d->storageFilter == ShowAllStorages) {
0821         return ((d->tagIds.contains(tagId) || d->tagIds.isEmpty()) &&
0822                 (d->resourceIds.contains(resourceId) || d->resourceIds.isEmpty()));
0823     }
0824 
0825     if ((d->tagFilter == ShowActiveTags && !tagActive)
0826             || (d->tagFilter == ShowInactiveTags && tagActive)) {
0827         return false;
0828     }
0829 
0830     if ((d->resourceFilter == ShowActiveResources && !resourceActive)
0831             || (d->resourceFilter == ShowInactiveResources && resourceActive)) {
0832         return false;
0833     }
0834 
0835     if ((d->storageFilter == ShowActiveStorages && !resourceStorageActive)
0836             || (d->storageFilter == ShowInactiveStorages && resourceStorageActive)) {
0837         return false;
0838     }
0839 
0840     return ((d->tagIds.contains(tagId) || d->tagIds.isEmpty())
0841             && (d->resourceIds.contains(resourceId) || d->resourceIds.isEmpty()));
0842 }
0843 
0844 bool KisTagResourceModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
0845 {
0846     QString nameLeft = sourceModel()->data(source_left, Qt::UserRole + KisAllTagResourceModel::ResourceName).toString();
0847     QString nameRight = sourceModel()->data(source_right, Qt::UserRole + KisAllTagResourceModel::ResourceName).toString();
0848     return nameLeft.toLower() < nameRight.toLower();
0849 }
0850 
0851 void KisTagResourceModel::storageChanged(const QString &location)
0852 {
0853     Q_UNUSED(location);
0854     invalidateFilter();
0855 }
0856 
0857 KoResourceSP KisTagResourceModel::resourceForIndex(QModelIndex index) const
0858 {
0859     int id = data(index, Qt::UserRole + KisAllTagResourceModel::ResourceId).toInt();
0860     if (id < 1)  return nullptr;
0861     KoResourceSP res = KisResourceLocator::instance()->resourceForId(id);
0862     return res;
0863 }
0864 
0865 QModelIndex KisTagResourceModel::indexForResource(KoResourceSP resource) const
0866 {
0867     if (!resource || !resource->valid() || resource->resourceId() < 0) return QModelIndex();
0868 
0869     for (int i = 0; i < rowCount(); ++i)  {
0870         QModelIndex idx = index(i, Qt::UserRole + KisAllTagResourceModel::ResourceId);
0871         Q_ASSERT(idx.isValid());
0872         if (idx.data(Qt::UserRole + KisAllTagResourceModel::ResourceId).toInt() == resource->resourceId()) {
0873             return idx;
0874         }
0875     }
0876     return QModelIndex();
0877 }
0878 
0879 QModelIndex KisTagResourceModel::indexForResourceId(int resourceId) const
0880 {
0881     if (resourceId < 0) return QModelIndex();
0882     for (int i = 0; i < rowCount(); ++i)  {
0883         QModelIndex idx = index(i, Qt::UserRole + KisAllTagResourceModel::ResourceId);
0884         Q_ASSERT(idx.isValid());
0885         if (idx.data(Qt::UserRole + KisAllTagResourceModel::ResourceId).toInt() == resourceId) {
0886             return idx;
0887         }
0888     }
0889     return QModelIndex();
0890 }
0891 
0892 bool KisTagResourceModel::setResourceActive(const QModelIndex &index, bool value)
0893 {
0894     KisResourceModel resourceModel(d->resourceType);
0895     QModelIndex idx = resourceModel.indexForResource(resourceForIndex(index));
0896     return resourceModel.setResourceActive(idx, value);
0897 }
0898 
0899 KoResourceSP KisTagResourceModel::importResourceFile(const QString &filename, const bool allowOverwrite, const QString &storageId)
0900 {
0901     // Since we're importing the resource, there's no reason to add rows to the tags::resources table,
0902     // because the resource is untagged.
0903     KisResourceModel resourceModel(d->resourceType);
0904     return resourceModel.importResourceFile(filename, allowOverwrite, storageId);
0905 }
0906 
0907 KoResourceSP KisTagResourceModel::importResource(const QString &filename, QIODevice *device, const bool allowOverwrite, const QString &storageId)
0908 {
0909     // Since we're importing the resource, there's no reason to add rows to the tags::resources table,
0910     // because the resource is untagged.
0911     KisResourceModel resourceModel(d->resourceType);
0912     return resourceModel.importResource(filename, device, allowOverwrite, storageId);
0913 }
0914 
0915 bool KisTagResourceModel::importWillOverwriteResource(const QString &fileName, const QString &storageLocation) const
0916 {
0917     KisResourceModel resourceModel(d->resourceType);
0918     return resourceModel.importWillOverwriteResource(fileName, storageLocation);
0919 }
0920 
0921 bool KisTagResourceModel::exportResource(KoResourceSP resource, QIODevice *device)
0922 {
0923     KisResourceModel resourceModel(d->resourceType);
0924     return resourceModel.exportResource(resource, device);
0925 }
0926 
0927 bool KisTagResourceModel::addResource(KoResourceSP resource, const QString &storageId)
0928 {
0929     // Since we're importing the resource, there's no reason to add rows to the tags::resources table,
0930     // because the resource is untagged.
0931     KisResourceModel resourceModel(d->resourceType);
0932     return resourceModel.addResource(resource, storageId);
0933 }
0934 
0935 bool KisTagResourceModel::updateResource(KoResourceSP resource)
0936 {
0937     KisResourceModel resourceModel(d->resourceType);
0938     bool r = resourceModel.updateResource(resource);
0939     if (r) {
0940         QModelIndex index = indexForResource(resource);
0941         if (index.isValid()) {
0942             emit dataChanged(index, index, {Qt::EditRole});
0943         }
0944     }
0945     return r;
0946 }
0947 
0948 bool KisTagResourceModel::reloadResource(KoResourceSP resource)
0949 {
0950     KisResourceModel resourceModel(d->resourceType);
0951     bool r = resourceModel.reloadResource(resource);
0952     if (r) {
0953         QModelIndex index = indexForResource(resource);
0954         if (index.isValid()) {
0955             emit dataChanged(index, index, {Qt::EditRole});
0956         }
0957     }
0958     return r;
0959 }
0960 
0961 bool KisTagResourceModel::renameResource(KoResourceSP resource, const QString &name)
0962 {
0963     KisResourceModel resourceModel(d->resourceType);
0964     bool r = resourceModel.renameResource(resource, name);
0965     if (r) {
0966         QModelIndex index = indexForResource(resource);
0967         if (index.isValid()) {
0968             emit dataChanged(index, index, {Qt::EditRole});
0969         }
0970     }
0971     return r;
0972 }
0973 
0974 bool KisTagResourceModel::setResourceMetaData(KoResourceSP resource, QMap<QString, QVariant> metadata)
0975 {
0976     KisResourceModel resourceModel(d->resourceType);
0977     return resourceModel.setResourceMetaData(resource, metadata);
0978 }
0979 
0980 QVariant KisTagResourceModel::headerData(int section, Qt::Orientation orientation, int role) const
0981 {
0982     return d->sourceModel->headerData(section, orientation, role);
0983 }