File indexing completed on 2025-01-19 03:53:03

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2021-03-20
0007  * Description : a tool to export images to iNaturalist web service
0008  *
0009  * SPDX-FileCopyrightText: 2021      by Joerg Lohse <joergmlpts at gmail dot com>
0010  *
0011  * SPDX-License-Identifier: GPL-2.0-or-later
0012  *
0013  * ============================================================ */
0014 
0015 #include "inatsuggest.h"
0016 
0017 // Qt includes
0018 
0019 #include <QLabel>
0020 #include <QHeaderView>
0021 
0022 // KDE includes
0023 
0024 #include <klocalizedstring.h>
0025 
0026 // Local includes
0027 
0028 #include "inattalker.h"
0029 #include "inatutils.h"
0030 
0031 namespace DigikamGenericINatPlugin
0032 {
0033 
0034 enum
0035 {
0036     ITEM_PHOTO_IDX = 0,
0037     ITEM_NAME_IDX  = 1
0038 };
0039 
0040 struct TaxonAndFlags
0041 {
0042     explicit TaxonAndFlags(const Taxon& taxon,
0043                            bool visuallySimilar = false,
0044                            bool seenNearby = false)
0045         : m_taxon          (taxon),
0046           m_seenNearby     (seenNearby),
0047           m_visuallySimilar(visuallySimilar)
0048     {
0049     }
0050 
0051     Taxon m_taxon;
0052     bool  m_seenNearby;
0053     bool  m_visuallySimilar;
0054 };
0055 
0056 struct Completions
0057 {
0058     explicit Completions(bool fromVision)
0059         : m_fromVision(fromVision)
0060     {
0061     }
0062 
0063     Taxon                m_commonAncestor;
0064     QList<TaxonAndFlags> m_taxa;
0065     bool                 m_fromVision;
0066 };
0067 
0068 // ----------------------------------------------------------------------------
0069 
0070 class Q_DECL_HIDDEN SuggestTaxonCompletion::Private
0071 {
0072 public:
0073 
0074     Private()
0075         : editor    (nullptr),
0076           talker    (nullptr),
0077           popup     (nullptr),
0078           fromVision(false)
0079     {
0080     }
0081 
0082     TaxonEdit*                    editor;
0083     INatTalker*                   talker;
0084     QTreeWidget*                  popup;
0085     bool                          fromVision;
0086     QVector<Taxon>                completionTaxa;
0087     QTimer                        timer;
0088     QHash<QUrl, QTreeWidgetItem*> url2item;
0089 };
0090 
0091 SuggestTaxonCompletion::SuggestTaxonCompletion(TaxonEdit* const parent)
0092     : QObject(parent),
0093       d      (new Private)
0094 {
0095     d->editor = parent;
0096     d->popup  = new QTreeWidget;
0097     d->popup->setWindowFlags(Qt::Popup);
0098     d->popup->setFocusPolicy(Qt::NoFocus);
0099     d->popup->setFocusProxy(parent);
0100     d->popup->setMouseTracking(true);
0101 
0102     d->popup->setUniformRowHeights(true);
0103     d->popup->setRootIsDecorated(false);
0104     d->popup->setEditTriggers(QTreeWidget::NoEditTriggers);
0105     d->popup->setSelectionBehavior(QTreeWidget::SelectRows);
0106     d->popup->setFrameStyle(QFrame::Box | QFrame::Plain);
0107     d->popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0108     d->popup->header()->hide();
0109 
0110     d->popup->installEventFilter(this);
0111 
0112     connect(d->popup, SIGNAL(itemPressed(QTreeWidgetItem*,int)),
0113             this, SLOT(slotDoneCompletion()));
0114 
0115     d->timer.setSingleShot(true);
0116     d->timer.setInterval(500);
0117 
0118     connect(&d->timer, SIGNAL(timeout()),
0119             this, SLOT(slotAutoSuggest()));
0120 
0121     connect(d->editor, SIGNAL(textEdited(QString)),
0122             SLOT(slotTextEdited(QString)));
0123 }
0124 
0125 SuggestTaxonCompletion::~SuggestTaxonCompletion()
0126 {
0127     delete d->popup;
0128     delete d;
0129 }
0130 
0131 void SuggestTaxonCompletion::slotTextEdited(const QString&)
0132 {
0133     Q_EMIT signalTaxonDeselected();
0134     d->timer.start();
0135 }
0136 
0137 void SuggestTaxonCompletion::setTalker(INatTalker* const inatTalker)
0138 {
0139     d->talker = inatTalker;
0140 
0141     connect(d->talker, SIGNAL(signalTaxonAutoCompletions(AutoCompletions)),
0142             this, SLOT(slotTaxonAutoCompletions(AutoCompletions)));
0143 
0144     connect(d->talker, SIGNAL(signalComputerVisionResults(ImageScores)),
0145             this, SLOT(slotComputerVisionResults(ImageScores)));
0146 
0147     connect(d->editor, SIGNAL(inFocus()),
0148             this, SLOT(slotInFocus()));
0149 
0150     connect(d->talker, SIGNAL(signalLoadUrlSucceeded(QUrl,QByteArray)),
0151             this, SLOT(slotImageLoaded(QUrl,QByteArray)));
0152 }
0153 
0154 void SuggestTaxonCompletion::slotInFocus()
0155 {
0156     Q_EMIT signalTaxonDeselected();
0157     d->timer.start();
0158 }
0159 
0160 bool SuggestTaxonCompletion::eventFilter(QObject* obj, QEvent* ev)
0161 {
0162     if (obj != d->popup)
0163     {
0164         return false;
0165     }
0166 
0167     if (ev->type() == QEvent::MouseButtonPress)
0168     {
0169         d->popup->hide();
0170         d->editor->setFocus();
0171 
0172         return true;
0173     }
0174 
0175     if (ev->type() == QEvent::KeyPress)
0176     {
0177         bool consumed = false;
0178         int key       = static_cast<QKeyEvent*>(ev)->key();
0179 
0180         switch (key)
0181         {
0182             case Qt::Key_Enter:
0183             case Qt::Key_Return:
0184             {
0185                 slotDoneCompletion();
0186                 consumed = true;
0187                 break;
0188             }
0189 
0190             case Qt::Key_Escape:
0191             {
0192                 d->editor->setFocus();
0193                 d->popup->hide();
0194                 consumed = true;
0195                 break;
0196             }
0197 
0198             case Qt::Key_Up:
0199             case Qt::Key_Down:
0200             case Qt::Key_Home:
0201             case Qt::Key_End:
0202             case Qt::Key_PageUp:
0203             case Qt::Key_PageDown:
0204             {
0205                 break;
0206             }
0207 
0208             default:
0209             {
0210                 d->editor->setFocus();
0211                 d->editor->event(ev);
0212                 d->popup->hide();
0213                 break;
0214             }
0215         }
0216 
0217         return consumed;
0218     }
0219 
0220     return false;
0221 }
0222 
0223 void SuggestTaxonCompletion::taxon2Item(const Taxon& taxon,
0224                                         QTreeWidgetItem* item,
0225                                         const QString& info)
0226 {
0227     QString htmlText = taxon.htmlName() + QLatin1String("<br/>") +
0228                        taxon.commonName() +
0229                        QLatin1String("<br/><font color=\"#74ac00\">") +
0230                        info + QLatin1String("</font>");
0231     d->popup->setItemWidget(item, ITEM_NAME_IDX, new QLabel(htmlText));
0232 
0233     // photo
0234 
0235     const QUrl& photoUrl = taxon.squareUrl();
0236 
0237     if (!photoUrl.isEmpty())
0238     {
0239         d->url2item.insert(photoUrl, item);
0240         d->talker->loadUrl(photoUrl);
0241     }
0242 }
0243 
0244 void SuggestTaxonCompletion::showCompletion(const Completions& choices)
0245 {
0246     d->popup->setUpdatesEnabled(false);
0247     d->popup->clear();
0248     d->popup->setIconSize(QSize(75, 75));
0249     d->fromVision = choices.m_fromVision;
0250     int columns   = choices.m_taxa.isEmpty() ? 1 : 2;
0251     d->popup->setColumnCount(columns);
0252     d->url2item.clear();
0253 
0254     if (choices.m_commonAncestor.isValid())
0255     {
0256         const Taxon& taxon = choices.m_commonAncestor;
0257 
0258         Q_ASSERT(choices.m_fromVision);
0259 
0260         auto item          = new QTreeWidgetItem(d->popup);
0261         taxon2Item(taxon, item, i18n("We're pretty sure it's in this %1.",
0262                                      localizedTaxonomicRank(taxon.rank())));
0263     }
0264 
0265     for (const auto& choice : choices.m_taxa)
0266     {
0267         QString info;
0268 
0269         if      (choice.m_visuallySimilar && choice.m_seenNearby)
0270         {
0271             info = i18n("Visually Similar") + QLatin1String(" / ") +
0272                    i18n("Seen Nearby");
0273         }
0274         else if (choice.m_visuallySimilar)
0275         {
0276             info = i18n("Visually Similar");
0277         }
0278         else if (choice.m_seenNearby)
0279         {
0280             info = i18n("Seen Nearby");
0281         }
0282 
0283         auto item = new QTreeWidgetItem(d->popup);
0284         taxon2Item(choice.m_taxon, item, info);
0285     }
0286 
0287     if (choices.m_taxa.isEmpty())
0288     {
0289         auto item  = new QTreeWidgetItem(d->popup);
0290         QFont font = item->font(0);
0291         font.setBold(true);
0292         item->setForeground(0, QColor(Qt::red));
0293         item->setText(0, i18n("invalid name"));
0294         item->setFont(0, font);
0295     }
0296 
0297     d->popup->setCurrentItem(d->popup->topLevelItem(0));
0298 
0299     for (int i = 0 ; i < columns ; ++i)
0300     {
0301         d->popup->resizeColumnToContents(i);
0302     }
0303 
0304     d->popup->setUpdatesEnabled(true);
0305     d->popup->setMinimumWidth(d->editor->width());
0306     d->popup->move(d->editor->mapToGlobal(QPoint(0, d->editor->height())));
0307     d->popup->setFocus();
0308     d->popup->show();
0309 }
0310 
0311 void SuggestTaxonCompletion::slotDoneCompletion()
0312 {
0313     d->timer.stop();
0314     d->url2item.clear();
0315     d->popup->hide();
0316     d->editor->setFocus();
0317 
0318     if (d->completionTaxa.count() == 0)
0319     {
0320         return;
0321     }
0322 
0323     QTreeWidgetItem* const item = d->popup->currentItem();
0324 
0325     if (item)
0326     {
0327         int idx = item->treeWidget()->indexOfTopLevelItem(item);
0328 
0329         if (idx < d->completionTaxa.count())
0330         {
0331             const Taxon& taxon = d->completionTaxa[idx];
0332 
0333             if (taxon.commonName().isEmpty())
0334             {
0335                 d->editor->setText(taxon.name());
0336             }
0337             else
0338             {
0339                 // combine scientific name and common name
0340 
0341                 d->editor->setText(taxon.name() + QLatin1String(" (") +
0342                                    taxon.commonName() + QLatin1String(")"));
0343             }
0344 
0345             QMetaObject::invokeMethod(d->editor, "returnPressed");
0346 
0347             Q_EMIT signalTaxonSelected(taxon, d->fromVision);
0348         }
0349     }
0350 }
0351 
0352 QString SuggestTaxonCompletion::getText() const
0353 {
0354     QString str = d->editor->text().simplified();
0355 
0356     // When we have "scientific name (common name)" we only
0357     // send the scientific name to auto-completion.
0358 
0359     int idx = str.indexOf(QLatin1String(" ("));
0360 
0361     if (idx >= 0)
0362     {
0363         str.truncate(idx);
0364     }
0365 
0366     return str;
0367 }
0368 
0369 void SuggestTaxonCompletion::slotAutoSuggest()
0370 {
0371     QString str = getText();
0372 
0373     if (str.count() > 0)
0374     {
0375         d->talker->taxonAutoCompletions(str);
0376     }
0377     else
0378     {
0379         Q_EMIT signalComputerVision();
0380     }
0381 }
0382 
0383 void SuggestTaxonCompletion::slotPreventSuggest()
0384 {
0385     d->timer.stop();
0386 }
0387 
0388 void SuggestTaxonCompletion::slotTaxonAutoCompletions(const AutoCompletions& taxa)
0389 {
0390     if (getText() != taxa.first)
0391     {
0392         return;
0393     }
0394 
0395     Completions completions(false);
0396 
0397     d->completionTaxa.clear();
0398 
0399     for (const auto& taxon : taxa.second)
0400     {
0401         completions.m_taxa << TaxonAndFlags(taxon);
0402         d->completionTaxa.append(taxon);
0403     }
0404 
0405     showCompletion(completions);
0406 }
0407 
0408 void SuggestTaxonCompletion::slotComputerVisionResults(const ImageScores& scores)
0409 {
0410     if (!d->editor->text().simplified().isEmpty())
0411     {
0412         return;
0413     }
0414 
0415     Completions completions(true);
0416 
0417     d->completionTaxa.clear();
0418 
0419     for (const auto& score : scores.second)
0420     {
0421         if (score.getTaxon().ancestors().isEmpty())
0422         {
0423             Q_ASSERT(!completions.m_commonAncestor.isValid());
0424 
0425             completions.m_commonAncestor = score.getTaxon();
0426         }
0427         else
0428         {
0429             completions.m_taxa << TaxonAndFlags(score.getTaxon(),
0430                                                 score.visuallySimilar(),
0431                                                 score.seenNearby());
0432         }
0433 
0434         d->completionTaxa.append(score.getTaxon());
0435     }
0436 
0437     showCompletion(completions);
0438 }
0439 
0440 void SuggestTaxonCompletion::slotImageLoaded(const QUrl& url, const QByteArray& data)
0441 {
0442     if (d->url2item.contains(url))
0443     {
0444         QTreeWidgetItem* const item = d->url2item[url];
0445         QImage image;
0446         image.loadFromData(data);
0447         QIcon icon(QPixmap::fromImage(image));
0448         item->setIcon(ITEM_PHOTO_IDX, icon);
0449         d->popup->resizeColumnToContents(ITEM_PHOTO_IDX);
0450         d->popup->resizeColumnToContents(ITEM_NAME_IDX);
0451     }
0452 }
0453 
0454 } // namespace DigikamGenericINatPlugin
0455 
0456 #include "moc_inatsuggest.cpp"