File indexing completed on 2025-04-27 03:58:40

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2010-06-13
0007  * Description : A QCompleter for AbstractAlbumModels
0008  *
0009  * SPDX-FileCopyrightText: 2007-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  * SPDX-FileCopyrightText: 2009-2010 by Johannes Wienke <languitar at semipol dot de>
0011  * SPDX-FileCopyrightText: 2010-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0012  *
0013  * SPDX-License-Identifier: GPL-2.0-or-later
0014  *
0015  * ============================================================ */
0016 
0017 #include "modelcompleter.h"
0018 
0019 // Qt includes
0020 
0021 #include <QTimer>
0022 #include <QPointer>
0023 #include <QStringListModel>
0024 
0025 // Local includes
0026 
0027 #include "digikam_debug.h"
0028 
0029 namespace Digikam
0030 {
0031 
0032 class Q_DECL_HIDDEN ModelCompleter::Private
0033 {
0034 public:
0035 
0036     explicit Private()
0037       : displayRole      (Qt::DisplayRole),
0038         uniqueIdRole     (Qt::DisplayRole),
0039         delayedModelTimer(nullptr),
0040         stringModel      (nullptr),
0041         model            (nullptr)
0042     {
0043     }
0044 
0045     int                          displayRole;
0046     int                          uniqueIdRole;
0047 
0048     QTimer*                      delayedModelTimer;
0049     QStringListModel*            stringModel;
0050     QPointer<QAbstractItemModel> model;
0051 
0052     /**
0053      * This map maps model indexes to their current text representation in the
0054      * completion object. This is needed because if data changes in one index,
0055      * the old text value is not known anymore, so that it cannot be removed
0056      * from the completion object.
0057      * TODO: if we want to use models that return unique strings but not integer, add support
0058      */
0059     QHash<int, QString>          idToTextHash;
0060 };
0061 
0062 ModelCompleter::ModelCompleter(QObject* const parent)
0063     : QCompleter(parent),
0064       d         (new Private)
0065 {
0066     d->stringModel = new QStringListModel(this);
0067     setModel(d->stringModel);
0068 
0069     setModelSorting(CaseSensitivelySortedModel);
0070     setCaseSensitivity(Qt::CaseInsensitive);
0071     setCompletionMode(PopupCompletion);
0072     setCompletionRole(Qt::DisplayRole);
0073     setFilterMode(Qt::MatchContains);
0074     setMaxVisibleItems(10);
0075     setCompletionColumn(0);
0076 
0077     d->delayedModelTimer = new QTimer(this);
0078     d->delayedModelTimer->setInterval(1000);
0079     d->delayedModelTimer->setSingleShot(true);
0080 
0081     connect(d->delayedModelTimer, SIGNAL(timeout()),
0082             this, SLOT(slotDelayedModelTimer()));
0083 
0084     connect(this, SIGNAL(activated(QModelIndex)),
0085             this, SIGNAL(signalActivated()));
0086 
0087     connect(this, SIGNAL(highlighted(QModelIndex)),
0088             this, SLOT(slotHighlighted(QModelIndex)));
0089 }
0090 
0091 ModelCompleter::~ModelCompleter()
0092 {
0093     delete d;
0094 }
0095 
0096 void ModelCompleter::setItemModel(QAbstractItemModel* const model, int uniqueIdRole, int displayRole)
0097 {
0098     // first release old model
0099 
0100     if (d->model)
0101     {
0102         disconnect(d->model);
0103         d->idToTextHash.clear();
0104         d->stringModel->setStringList(QStringList());
0105     }
0106 
0107     d->model        = model;
0108     d->displayRole  = displayRole;
0109     d->uniqueIdRole = uniqueIdRole;
0110 
0111     // connect to the new model
0112 
0113     if (d->model)
0114     {
0115         connect(d->model, SIGNAL(rowsInserted(QModelIndex,int,int)),
0116                 this, SLOT(slotRowsInserted(QModelIndex,int,int)));
0117 
0118         connect(d->model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
0119                 this, SLOT(slotRowsAboutToBeRemoved(QModelIndex,int,int)));
0120 
0121         connect(d->model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
0122                 this, SLOT(slotDataChanged(QModelIndex,QModelIndex)));
0123 
0124         connect(d->model, SIGNAL(modelReset()),
0125                 this, SLOT(slotModelReset()));
0126 
0127         // do an initial sync wit the new model
0128 
0129         sync(d->model);
0130     }
0131 }
0132 
0133 QAbstractItemModel* ModelCompleter::itemModel() const
0134 {
0135     return d->model;
0136 }
0137 
0138 void ModelCompleter::addItem(const QString& item)
0139 {
0140     QStringList list = d->stringModel->stringList();
0141     setList(list << item);
0142 }
0143 
0144 void ModelCompleter::setList(const QStringList& list)
0145 {
0146     d->stringModel->setStringList(list);
0147     d->stringModel->sort(0);
0148 }
0149 
0150 QStringList ModelCompleter::items() const
0151 {
0152     return d->stringModel->stringList();
0153 }
0154 
0155 void ModelCompleter::slotDelayedModelTimer()
0156 {
0157     QStringList list = d->idToTextHash.values();
0158     list.removeDuplicates();
0159     setList(list);
0160 }
0161 
0162 void ModelCompleter::slotRowsInserted(const QModelIndex& parent, int start, int end)
0163 {
0164     for (int i = start ; i <= end ; ++i)
0165     {
0166         // this cannot work if this is called from rowsAboutToBeInserted
0167         // because then the model doesn't know the index yet. So never do this
0168         // ;)
0169 
0170         const QModelIndex child = d->model->index(i, 0, parent);
0171 
0172         if (child.isValid())
0173         {
0174             sync(d->model, child);
0175         }
0176         else
0177         {
0178             qCDebug(DIGIKAM_WIDGETS_LOG) << "inserted rows are not valid for parent" << parent
0179                                          << parent.data(d->displayRole).toString()
0180                                          << "and child" << child;
0181         }
0182     }
0183 
0184     d->delayedModelTimer->start();
0185 }
0186 
0187 void ModelCompleter::slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end)
0188 {
0189     for (int i = start ; i <= end ; ++i)
0190     {
0191         QModelIndex index = d->model->index(i, 0, parent);
0192 
0193         if (!index.isValid())
0194         {
0195             qCDebug(DIGIKAM_WIDGETS_LOG) << "Received an invalid index to be removed";
0196             continue;
0197         }
0198 
0199         int id = index.data(d->uniqueIdRole).toInt();
0200 
0201         if (d->idToTextHash.contains(id))
0202         {
0203             QString itemName = d->idToTextHash.value(id);
0204             d->idToTextHash.remove(id);
0205 
0206             // only delete an item in the completion object if there is no other
0207             // item with the same display name
0208 
0209             if (d->idToTextHash.keys(itemName).isEmpty())
0210             {
0211                 d->delayedModelTimer->start();
0212             }
0213         }
0214         else
0215         {
0216             qCWarning(DIGIKAM_WIDGETS_LOG) << "idToTextHash seems to be out of sync with the model."
0217                                            << "There is no entry for model index" << index;
0218         }
0219     }
0220 }
0221 
0222 void ModelCompleter::slotModelReset()
0223 {
0224     sync(d->model);
0225 }
0226 
0227 void ModelCompleter::slotDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight)
0228 {
0229     for (int row = topLeft.row() ; row <= bottomRight.row() ; ++row)
0230     {
0231         if (!d->model->hasIndex(row, topLeft.column(), topLeft.parent()))
0232         {
0233             qCDebug(DIGIKAM_WIDGETS_LOG) << "Got wrong change event for index with row" << row
0234                                          << ", column"   << topLeft.column()
0235                                          << "and parent" << topLeft.parent()
0236                                          << "in model"   << d->model
0237                                          << ". Ignoring it.";
0238             continue;
0239         }
0240 
0241         QModelIndex index  = d->model->index(row, topLeft.column(), topLeft.parent());
0242 
0243         if (!index.isValid())
0244         {
0245             qCDebug(DIGIKAM_WIDGETS_LOG) << "illegal index in changed data";
0246             continue;
0247         }
0248 
0249         int id              = index.data(d->uniqueIdRole).toInt();
0250         QString itemName    = index.data(d->displayRole).toString();
0251         d->idToTextHash[id] = itemName;
0252 
0253         d->delayedModelTimer->start();
0254     }
0255 }
0256 
0257 void ModelCompleter::sync(QAbstractItemModel* const model)
0258 {
0259     d->idToTextHash.clear();
0260 
0261     for (int i = 0 ; i < model->rowCount() ; ++i)
0262     {
0263         const QModelIndex index = model->index(i, 0);
0264         sync(model, index);
0265     }
0266 
0267     d->delayedModelTimer->start();
0268 }
0269 
0270 void ModelCompleter::sync(QAbstractItemModel* const model, const QModelIndex& index)
0271 {
0272     QString itemName = index.data(d->displayRole).toString();
0273     d->idToTextHash.insert(index.data(d->uniqueIdRole).toInt(), itemName);
0274 
0275     for (int i = 0 ; i < model->rowCount(index) ; ++i)
0276     {
0277         const QModelIndex child = model->index(i, 0, index);
0278         sync(model, child);
0279     }
0280 }
0281 
0282 void ModelCompleter::slotHighlighted(const QModelIndex& index)
0283 {
0284     if (index.isValid())
0285     {
0286         QString itemName = index.data().toString();
0287 
0288         if (d->idToTextHash.values().count(itemName) == 1)
0289         {
0290             Q_EMIT signalHighlighted(d->idToTextHash.key(itemName));
0291         }
0292     }
0293 }
0294 
0295 } // namespace Digikam
0296 
0297 #include "moc_modelcompleter.cpp"