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"