File indexing completed on 2024-04-28 05:08:21

0001 /***************************************************************************
0002     Copyright (C) 2003-2020 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include <config.h>
0026 
0027 #include "fetchdialog.h"
0028 #include "fetch/fetchmanager.h"
0029 #include "fetch/fetcher.h"
0030 #include "fetch/fetchresult.h"
0031 #include "config/tellico_config.h"
0032 #include "entryview.h"
0033 #include "utils/isbnvalidator.h"
0034 #include "utils/upcvalidator.h"
0035 #include "tellico_kernel.h"
0036 #include "core/filehandler.h"
0037 #include "collection.h"
0038 #include "entry.h"
0039 #include "document.h"
0040 #include "field.h"
0041 #include "fieldformat.h"
0042 #include "gui/combobox.h"
0043 #include "utils/cursorsaver.h"
0044 #include "utils/stringset.h"
0045 #include "images/image.h"
0046 #include "tellico_debug.h"
0047 
0048 #ifdef ENABLE_WEBCAM
0049 #include "barcode/barcode.h"
0050 #endif
0051 
0052 #include <KLocalizedString>
0053 #include <KSharedConfig>
0054 #include <KAcceleratorManager>
0055 #include <KTextEdit>
0056 #include <KMessageBox>
0057 #include <KStandardGuiItem>
0058 #include <KWindowConfig>
0059 
0060 #include <QDialogButtonBox>
0061 #include <QPushButton>
0062 #include <QLineEdit>
0063 #include <QGroupBox>
0064 #include <QSplitter>
0065 #include <QTimer>
0066 #include <QCheckBox>
0067 #include <QImage>
0068 #include <QLabel>
0069 #include <QHBoxLayout>
0070 #include <QVBoxLayout>
0071 #include <QProgressBar>
0072 #include <QTreeWidget>
0073 #include <QHeaderView>
0074 #include <QApplication>
0075 #include <QDesktopWidget>
0076 #include <QFileDialog>
0077 #include <QStatusBar>
0078 
0079 namespace {
0080   static const int FETCH_MIN_WIDTH = 600;
0081 
0082   static const char* FETCH_STRING_SEARCH = I18N_NOOP("&Search");
0083   static const char* FETCH_STRING_STOP   = I18N_NOOP("&Stop");
0084 
0085   static const int StringDataType = QEvent::User;
0086   static const int ImageDataType = QEvent::User+1;
0087 
0088   class StringDataEvent : public QEvent {
0089   public:
0090     StringDataEvent(const QString& str) : QEvent(static_cast<QEvent::Type>(StringDataType)), m_string(str) {}
0091     QString string() const { return m_string; }
0092   private:
0093     Q_DISABLE_COPY(StringDataEvent)
0094     QString m_string;
0095   };
0096 
0097   class ImageDataEvent : public QEvent {
0098   public:
0099     ImageDataEvent(const QImage& img) : QEvent(static_cast<QEvent::Type>(ImageDataType)), m_image(img) {}
0100     QImage image() const { return m_image; }
0101   private:
0102     Q_DISABLE_COPY(ImageDataEvent)
0103     QImage m_image;
0104   };
0105 
0106   // class exists just to make sizeHintForColumn() public
0107   class TreeWidget : public QTreeWidget {
0108   public:
0109     TreeWidget(QWidget* p) : QTreeWidget(p) {}
0110     virtual int sizeHintForColumn(int c) const Q_DECL_OVERRIDE { return QTreeWidget::sizeHintForColumn(c); }
0111   };
0112 }
0113 
0114 using Tellico::FetchDialog;
0115 using barcodeRecognition::barcodeRecognitionThread;
0116 
0117 class FetchDialog::FetchResultItem : public QTreeWidgetItem {
0118   friend class FetchDialog;
0119   // always add to end
0120   FetchResultItem(QTreeWidget* lv, Fetch::FetchResult* r)
0121       : QTreeWidgetItem(lv), m_result(r) {
0122     setData(1, Qt::DisplayRole, r->title);
0123     setData(2, Qt::DisplayRole, r->desc);
0124     setData(3, Qt::DisplayRole, r->fetcher()->source());
0125     setData(3, Qt::DecorationRole, Fetch::Manager::self()->fetcherIcon(r->fetcher()));
0126   }
0127   Fetch::FetchResult* m_result;
0128 
0129 private:
0130   Q_DISABLE_COPY(FetchResultItem)
0131 };
0132 
0133 FetchDialog::FetchDialog(QWidget* parent_)
0134     : QDialog(parent_)
0135     , m_timer(new QTimer(this))
0136     , m_started(false)
0137     , m_resultCount(0)
0138     , m_treeWasResized(false)
0139     , m_barcodePreview(nullptr)
0140     , m_barcodeRecognitionThread(nullptr) {
0141   setModal(false);
0142   setWindowTitle(i18n("Internet Search"));
0143 
0144   QVBoxLayout* mainLayout = new QVBoxLayout();
0145   setLayout(mainLayout);
0146 
0147   m_collType = Kernel::self()->collectionType();
0148 
0149   QWidget* mainWidget = new QWidget(this);
0150   mainLayout->addWidget(mainWidget);
0151   QBoxLayout* topLayout = new QVBoxLayout(mainWidget);
0152 
0153   QGroupBox* queryBox = new QGroupBox(i18n("Search Query"), mainWidget);
0154   QBoxLayout* queryLayout = new QVBoxLayout(queryBox);
0155   topLayout->addWidget(queryBox);
0156 
0157   QWidget* box1 = new QWidget(queryBox);
0158   QHBoxLayout* box1HBoxLayout = new QHBoxLayout(box1);
0159   box1HBoxLayout->setMargin(0);
0160   queryLayout->addWidget(box1);
0161 
0162   QLabel* label = new QLabel(i18nc("Start the search", "S&earch:"), box1);
0163   box1HBoxLayout->addWidget(label);
0164 
0165   m_valueLineEdit = new QLineEdit(box1);
0166   box1HBoxLayout->addWidget(m_valueLineEdit);
0167   label->setBuddy(m_valueLineEdit);
0168   m_valueLineEdit->setWhatsThis(i18n("Enter a search value. An ISBN search must include the full ISBN."));
0169 
0170   m_keyCombo = new GUI::ComboBox(box1);
0171   box1HBoxLayout->addWidget(m_keyCombo);
0172   Fetch::KeyMap map = Fetch::Manager::self()->keyMap();
0173   for(Fetch::KeyMap::ConstIterator it = map.constBegin(); it != map.constEnd(); ++it) {
0174     m_keyCombo->addItem(it.value(), it.key());
0175   }
0176   void (QComboBox::* activatedInt)(int) = &QComboBox::activated;
0177   connect(m_keyCombo, activatedInt, this, &FetchDialog::slotKeyChanged);
0178   m_keyCombo->setWhatsThis(i18n("Choose the type of search"));
0179 
0180   m_searchButton = new QPushButton(box1);
0181   box1HBoxLayout->addWidget(m_searchButton);
0182   KGuiItem::assign(m_searchButton, KGuiItem(i18n(FETCH_STRING_STOP),
0183                                             QIcon::fromTheme(QStringLiteral("dialog-cancel"))));
0184   connect(m_searchButton, &QAbstractButton::clicked, this, &FetchDialog::slotSearchClicked);
0185   m_searchButton->setWhatsThis(i18n("Click to start or stop the search"));
0186 
0187   // the search button's text changes from search to stop
0188   // I don't want it resizing, so figure out the maximum size and set that
0189   m_searchButton->ensurePolished();
0190   int maxWidth = m_searchButton->sizeHint().width();
0191   int maxHeight = m_searchButton->sizeHint().height();
0192   KGuiItem::assign(m_searchButton, KGuiItem(i18n(FETCH_STRING_SEARCH),
0193                                             QIcon::fromTheme(QStringLiteral("edit-find"))));
0194   maxWidth = qMax(maxWidth, m_searchButton->sizeHint().width());
0195   maxHeight = qMax(maxHeight, m_searchButton->sizeHint().height());
0196   m_searchButton->setMinimumWidth(maxWidth);
0197   m_searchButton->setMinimumHeight(maxHeight);
0198 
0199   QWidget* box2 = new QWidget(queryBox);
0200   QHBoxLayout* box2HBoxLayout = new QHBoxLayout(box2);
0201   box2HBoxLayout->setMargin(0);
0202   queryLayout->addWidget(box2);
0203 
0204   m_multipleISBN = new QCheckBox(i18n("&Multiple ISBN/UPC search"), box2);
0205   box2HBoxLayout->addWidget(m_multipleISBN);
0206   m_multipleISBN->setWhatsThis(i18n("Check this box to search for multiple ISBN or UPC values."));
0207   connect(m_multipleISBN, &QAbstractButton::toggled, this, &FetchDialog::slotMultipleISBN);
0208 
0209   m_editISBN = new QPushButton(box2);
0210   KGuiItem::assign(m_editISBN, KGuiItem(i18n("Edit ISBN/UPC values..."),
0211                                         QIcon::fromTheme(QStringLiteral("format-justify-fill"))));
0212   box2HBoxLayout->addWidget(m_editISBN);
0213   m_editISBN->setEnabled(false);
0214   m_editISBN->setWhatsThis(i18n("Click to open a text edit box for entering or editing multiple ISBN or UPC values."));
0215   connect(m_editISBN, &QAbstractButton::clicked, this, &FetchDialog::slotEditMultipleISBN);
0216 
0217   // add for spacing
0218   box2HBoxLayout->addStretch(10);
0219 
0220   label = new QLabel(i18n("Search s&ource:"), box2);
0221   box2HBoxLayout->addWidget(label);
0222   m_sourceCombo = new KComboBox(box2);
0223   box2HBoxLayout->addWidget(m_sourceCombo);
0224   label->setBuddy(m_sourceCombo);
0225   Fetch::FetcherVec sources = Fetch::Manager::self()->fetchers(m_collType);
0226   foreach(Fetch::Fetcher::Ptr fetcher, sources) {
0227     m_sourceCombo->addItem(Fetch::Manager::self()->fetcherIcon(fetcher.data()), fetcher->source());
0228   }
0229 #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
0230   void (QComboBox::* activatedString)(const QString&) = &QComboBox::activated;
0231   connect(m_sourceCombo, activatedString, this, &FetchDialog::slotSourceChanged);
0232 #else
0233   connect(m_sourceCombo, &QComboBox::textActivated, this, &FetchDialog::slotSourceChanged);
0234 #endif
0235   m_sourceCombo->setWhatsThis(i18n("Select the database to search"));
0236 
0237   // for whatever reason, the dialog window could get shrunk and truncate the text
0238   box2->setMinimumWidth(box2->minimumSizeHint().width());
0239 
0240   QSplitter* split = new QSplitter(Qt::Vertical, mainWidget);
0241   topLayout->addWidget(split);
0242   split->setChildrenCollapsible(false);
0243 
0244   // using <anonymous>::TreeWidget as a lazy step to make a protected method public
0245   m_treeWidget = new TreeWidget(split);
0246   m_treeWidget->sortItems(1, Qt::AscendingOrder);
0247   m_treeWidget->setAllColumnsShowFocus(true);
0248   m_treeWidget->setSortingEnabled(true);
0249   m_treeWidget->setRootIsDecorated(false);
0250   m_treeWidget->setSelectionMode(QAbstractItemView::ExtendedSelection);
0251   m_treeWidget->setHeaderLabels(QStringList() << QString()
0252                                               << i18n("Title")
0253                                               << i18n("Description")
0254                                               << i18n("Source"));
0255   m_treeWidget->model()->setHeaderData(0, Qt::Horizontal, Qt::AlignHCenter, Qt::TextAlignmentRole); // align checkmark in middle
0256   m_treeWidget->viewport()->installEventFilter(this);
0257   m_treeWidget->header()->setSortIndicatorShown(true);
0258   m_treeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); // will show a check mark when added
0259   m_treeWidget->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
0260 
0261   connect(m_treeWidget, &QTreeWidget::itemSelectionChanged, this, &FetchDialog::slotShowEntry);
0262   // double clicking should add the entry
0263   connect(m_treeWidget, &QTreeWidget::itemDoubleClicked, this, &FetchDialog::slotAddEntry);
0264   connect(m_treeWidget->header(), &QHeaderView::sectionResized, this, &FetchDialog::columnResized);
0265   m_treeWidget->setWhatsThis(i18n("As results are found, they are added to this list. Selecting one "
0266                                   "will fetch the complete entry and show it in the view below."));
0267 
0268   m_entryView = new EntryView(split);
0269   // don't bother creating funky gradient images for compact view
0270   m_entryView->setUseGradientImages(false);
0271   // set the xslt file AFTER setting the gradient image option
0272   m_entryView->setXSLTFile(QStringLiteral("Compact.xsl"));
0273   m_entryView->addXSLTStringParam("skip-fields", "id,mdate,cdate");
0274   m_entryView->
0275 #ifdef USE_KHTML
0276                view()->
0277 #endif
0278                setWhatsThis(i18n("An entry may be shown here before adding it to the "
0279                                  "current collection by selecting it in the list above"));
0280 
0281   QWidget* box3 = new QWidget(mainWidget);
0282   QHBoxLayout* box3HBoxLayout = new QHBoxLayout(box3);
0283   box3HBoxLayout->setMargin(0);
0284   topLayout->addWidget(box3);
0285 
0286   m_addButton = new QPushButton(i18n("&Add Entry"), box3);
0287   box3HBoxLayout->addWidget(m_addButton);
0288   m_addButton->setEnabled(false);
0289   m_addButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
0290   connect(m_addButton, &QAbstractButton::clicked, this, &FetchDialog::slotAddEntry);
0291   m_addButton->setWhatsThis(i18n("Add the selected entry to the current collection"));
0292 
0293   m_moreButton = new QPushButton(box3);
0294   KGuiItem::assign(m_moreButton, KGuiItem(i18n("Get More Results"), QIcon::fromTheme(QStringLiteral("edit-find"))));
0295   box3HBoxLayout->addWidget(m_moreButton);
0296   m_moreButton->setEnabled(false);
0297   connect(m_moreButton, &QAbstractButton::clicked, this, &FetchDialog::slotMoreClicked);
0298   m_moreButton->setWhatsThis(i18n("Fetch more results from the current data source"));
0299 
0300   QPushButton* clearButton = new QPushButton(box3);
0301   KGuiItem::assign(clearButton, KStandardGuiItem::clear());
0302   box3HBoxLayout->addWidget(clearButton);
0303   connect(clearButton, &QAbstractButton::clicked, this, &FetchDialog::slotClearClicked);
0304   clearButton->setWhatsThis(i18n("Clear all search fields and results"));
0305 
0306   QWidget* bottombox = new QWidget(mainWidget);
0307   QHBoxLayout* bottomboxHBoxLayout = new QHBoxLayout(bottombox);
0308   bottomboxHBoxLayout->setMargin(0);
0309   topLayout->addWidget(bottombox);
0310 
0311   m_statusBar = new QStatusBar(bottombox);
0312   bottomboxHBoxLayout->addWidget(m_statusBar);
0313   m_statusLabel = new QLabel(m_statusBar);
0314   m_statusBar->addPermanentWidget(m_statusLabel, 1);
0315   m_progress = new QProgressBar(m_statusBar);
0316   m_progress->setFormat(i18nc("%p is the percent value, % is the percent sign", "%p%"));
0317   m_progress->setMaximum(0);
0318   m_progress->setFixedHeight(fontMetrics().height()+2);
0319   m_progress->hide();
0320   m_statusBar->addPermanentWidget(m_progress);
0321   m_statusBar->setSizeGripEnabled(false);
0322 
0323   QPushButton* closeButton = new QPushButton(bottombox);
0324   KGuiItem::assign(closeButton, KStandardGuiItem::close());
0325   bottomboxHBoxLayout->addWidget(closeButton);
0326   connect(closeButton, &QAbstractButton::clicked, this, &QDialog::accept);
0327 
0328   connect(m_timer, &QTimer::timeout, this, &FetchDialog::slotMoveProgress);
0329 
0330   setMinimumWidth(qMax(minimumWidth(), qMax(FETCH_MIN_WIDTH, minimumSizeHint().width())));
0331   setStatus(i18n("Ready."));
0332 
0333   KConfigGroup config(KSharedConfig::openConfig(), "Fetch Dialog Options");
0334   QList<int> splitList = config.readEntry("Splitter Sizes", QList<int>());
0335   if(!splitList.empty()) {
0336     split->setSizes(splitList);
0337   }
0338 
0339   connect(Fetch::Manager::self(), &Fetch::Manager::signalResultFound,
0340                                   this, &FetchDialog::slotResultFound);
0341   connect(Fetch::Manager::self(), &Fetch::Manager::signalStatus,
0342                                   this, &FetchDialog::slotStatus);
0343   connect(Fetch::Manager::self(), &Fetch::Manager::signalDone,
0344                                   this, &FetchDialog::slotFetchDone);
0345 
0346   KAcceleratorManager::manage(this);
0347   // initialize combos
0348   QTimer::singleShot(0, this, &FetchDialog::slotInit);
0349 }
0350 
0351 FetchDialog::~FetchDialog() {
0352 #ifdef ENABLE_WEBCAM
0353   if(m_barcodeRecognitionThread) {
0354     m_barcodeRecognitionThread->stop();
0355     if(!m_barcodeRecognitionThread->wait( 1000 )) {
0356       m_barcodeRecognitionThread->terminate();
0357     }
0358     delete m_barcodeRecognitionThread;
0359     m_barcodeRecognitionThread = nullptr;
0360   }
0361   if(m_barcodePreview) {
0362     delete m_barcodePreview;
0363     m_barcodePreview = nullptr;
0364   }
0365 #endif
0366 
0367   qDeleteAll(m_results);
0368   m_results.clear();
0369 
0370   // we might have downloaded a lot of images we don't need to keep
0371   Data::EntryList entriesToCheck;
0372   foreach(Data::EntryPtr entry, m_entries) {
0373     entriesToCheck.append(entry);
0374   }
0375   // no additional entries to check images to keep though
0376   Data::Document::self()->removeImagesNotInCollection(entriesToCheck, Data::EntryList());
0377 
0378   KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("Fetch Dialog Options"));
0379   KWindowConfig::saveWindowSize(windowHandle(), config);
0380 
0381   config.writeEntry("Splitter Sizes", static_cast<QSplitter*>(m_treeWidget->parentWidget())->sizes());
0382   config.writeEntry("Search Key", m_keyCombo->currentData().toInt());
0383   config.writeEntry("Search Source", m_sourceCombo->currentText());
0384 }
0385 
0386 void FetchDialog::closeEvent(QCloseEvent* event_) { // stop fetchers when the dialog is closed
0387   if(m_started) {
0388     QTimer::singleShot(0, Fetch::Manager::self(), &Fetch::Manager::stop);
0389   }
0390   QDialog::closeEvent(event_);
0391 }
0392 
0393 void FetchDialog::slotSearchClicked() {
0394   m_valueLineEdit->selectAll();
0395   if(m_started) {
0396     setStatus(i18n("Cancelling the search..."));
0397     Fetch::Manager::self()->stop();
0398   } else {
0399     const QString value = m_valueLineEdit->text().simplified();
0400     if(value.isEmpty()) {
0401       return;
0402     }
0403     m_resultCount = 0;
0404     m_oldSearch = value;
0405     m_started = true;
0406     KGuiItem::assign(m_searchButton, KGuiItem(i18n(FETCH_STRING_STOP),
0407                                               QIcon::fromTheme(QStringLiteral("dialog-cancel"))));
0408     startProgress();
0409     setStatus(i18n("Searching..."));
0410     qApp->processEvents();
0411     Fetch::Manager::self()->startSearch(m_sourceCombo->currentText(),
0412                                         static_cast<Fetch::FetchKey>(m_keyCombo->currentData().toInt()),
0413                                         value,
0414                                         Data::Document::self()->collection()->type());
0415   }
0416 }
0417 
0418 void FetchDialog::slotClearClicked() {
0419   fetchDone(false);
0420   m_treeWidget->clear();
0421   m_entryView->clear();
0422   Fetch::Manager::self()->stop();
0423   m_multipleISBN->setChecked(false);
0424   m_valueLineEdit->clear();
0425   m_valueLineEdit->setFocus();
0426   m_addButton->setEnabled(false);
0427   m_moreButton->setEnabled(false);
0428   m_isbnList.clear();
0429   m_statusMessages.clear();
0430   setStatus(i18n("Ready.")); // because slotFetchDone() writes text
0431 }
0432 
0433 void FetchDialog::slotStatus(const QString& status_) {
0434   m_statusMessages.push_back(status_);
0435   // if the queue was empty, start the timer
0436   if(m_statusMessages.count() == 1) {
0437     // wait 2 seconds
0438     QTimer::singleShot(2000, this, &FetchDialog::slotUpdateStatus);
0439   }
0440 }
0441 
0442 void FetchDialog::slotUpdateStatus() {
0443   if(m_statusMessages.isEmpty()) {
0444     return;
0445   }
0446   setStatus(m_statusMessages.front());
0447   m_statusMessages.pop_front();
0448   if(!m_statusMessages.isEmpty()) {
0449     // wait 2 seconds
0450     QTimer::singleShot(2000, this, &FetchDialog::slotUpdateStatus);
0451   }
0452 }
0453 
0454 void FetchDialog::setStatus(const QString& text_) {
0455   m_statusLabel->setText(QLatin1Char(' ') + text_);
0456 }
0457 
0458 void FetchDialog::slotFetchDone() {
0459   fetchDone(true);
0460 }
0461 
0462 void FetchDialog::fetchDone(bool checkISBN_) {
0463 //  myDebug() << "fetchDone";
0464   m_started = false;
0465   KGuiItem::assign(m_searchButton, KGuiItem(i18n(FETCH_STRING_SEARCH),
0466                                             QIcon::fromTheme(QStringLiteral("edit-find"))));
0467   stopProgress();
0468   if(m_resultCount == 0) {
0469     slotStatus(i18n("The search returned no items."));
0470   } else {
0471     /* TRANSLATORS: This is a plural form, you need to translate both lines (except "_n: ") */
0472     slotStatus(i18np("The search returned 1 item.",
0473                      "The search returned %1 items.",
0474                      m_resultCount));
0475   }
0476   m_moreButton->setEnabled(Fetch::Manager::self()->hasMoreResults());
0477 
0478   // if we're not checking isbn values, then, ok to return
0479   if(!checkISBN_) {
0480     return;
0481   }
0482 
0483   const Fetch::FetchKey key = static_cast<Fetch::FetchKey>(m_keyCombo->currentData().toInt());
0484   // no way to currently check EAN/UPC values for non-book items
0485   if(m_collType & (Data::Collection::Book | Data::Collection::Bibtex) &&
0486      m_multipleISBN->isChecked() &&
0487      (key == Fetch::ISBN || key == Fetch::UPC)) {
0488     QStringList searchValues = FieldFormat::splitValue(m_oldSearch.simplified());
0489     QStringList resultValues;
0490     resultValues.reserve(m_treeWidget->topLevelItemCount());
0491     for(int i = 0; i < m_treeWidget->topLevelItemCount(); ++i) {
0492       resultValues << static_cast<FetchResultItem*>(m_treeWidget->topLevelItem(i))->m_result->isbn;
0493     }
0494     // Google Book Search can have an error, returning a different ISBN in the initial search
0495     // than the one returned by fetchEntryHook(). As a small workaround, if only a single ISBN value
0496     // is in the search term, then don't show
0497     if(searchValues.count() > 1) {
0498       const QStringList valuesNotFound = ISBNValidator::listDifference(searchValues, resultValues);
0499       if(!valuesNotFound.isEmpty()) {
0500         KMessageBox::informationList(this,
0501                                      i18n("No results were found for the following ISBN values:"),
0502                                      valuesNotFound,
0503                                      i18n("No Results"));
0504       }
0505     }
0506   }
0507 }
0508 
0509 void FetchDialog::slotResultFound(Tellico::Fetch::FetchResult* result_) {
0510   m_results.append(result_);
0511   (void) new FetchResultItem(m_treeWidget, result_);
0512   // resize final column to size of contents if the user has never resized anything before
0513   if(!m_treeWasResized) {
0514     m_treeWidget->header()->setStretchLastSection(false);
0515 
0516     // do math to try to make a nice resizing, emphasizing sections 1 and 2 over 3
0517     const int w0 = m_treeWidget->columnWidth(0);
0518     const int w1 = m_treeWidget->columnWidth(1);
0519     const int w2 = m_treeWidget->columnWidth(2);
0520     const int w3 = m_treeWidget->columnWidth(3);
0521     const int wt = m_treeWidget->width();
0522 //    myDebug() << "OLD:" << w0 << w1 << w2 << w3 << wt << (w0+w1+w2+w3);
0523 
0524     // whatever is leftover from resizing 3, split between 1 and 2
0525     if(wt > (w0 + w1 + w2 + w3)) {
0526       const int w1rec = static_cast<TreeWidget*>(m_treeWidget)->sizeHintForColumn(1);
0527       const int w2rec = static_cast<TreeWidget*>(m_treeWidget)->sizeHintForColumn(2);
0528       const int w3rec = static_cast<TreeWidget*>(m_treeWidget)->sizeHintForColumn(3);
0529       if(w1 < w1rec || w2 < w2rec) {
0530         const int w3new = qMin(w3, w3rec);
0531         const int diff = wt - w0 - w1 - w2 - w3new;
0532         const int w1new = qBound(w1, w1rec, w1 + diff/2 - 4);
0533         const int w2new = qBound(w2, wt - w0 - w1new - w3new, w2rec);
0534         m_treeWidget->setColumnWidth(1, w1new);
0535         m_treeWidget->setColumnWidth(2, w2new);
0536         m_treeWidget->setColumnWidth(3, w3new);
0537 //        myDebug() << "New:" << w0 << w1new << w2new << w3new << wt << (w0+w1new+w2new+w3new);
0538       }
0539     }
0540     m_treeWidget->header()->setStretchLastSection(true);
0541     // because calling setColumnWidth() will change this
0542     m_treeWasResized = false;
0543   }
0544   ++m_resultCount;
0545 }
0546 
0547 void FetchDialog::slotAddEntry() {
0548   GUI::CursorSaver cs;
0549   Data::EntryList vec;
0550   foreach(QTreeWidgetItem* item_, m_treeWidget->selectedItems()) {
0551     FetchResultItem* item = static_cast<FetchResultItem*>(item_);
0552 
0553     Fetch::FetchResult* r = item->m_result;
0554     Data::EntryPtr entry = m_entries.value(r->uid);
0555     if(!entry) {
0556       setStatus(i18n("Fetching %1...", r->title));
0557       startProgress();
0558       entry = r->fetchEntry();
0559       if(!entry) {
0560         continue;
0561       }
0562       m_entries.insert(r->uid, entry);
0563       stopProgress();
0564       setStatus(i18n("Ready."));
0565     }
0566     if(entry->collection()->hasField(QStringLiteral("fetchdialog_source"))) {
0567       entry->collection()->removeField(QStringLiteral("fetchdialog_source"));
0568     }
0569     // add a copy, intentionally allowing multiple copies to be added
0570     vec.append(Data::EntryPtr(new Data::Entry(*entry)));
0571     item->setData(0, Qt::DecorationRole,
0572                   QIcon::fromTheme(QStringLiteral("checkmark"), QIcon(QLatin1String(":/icons/checkmark"))));
0573   }
0574   if(!vec.isEmpty()) {
0575     Kernel::self()->addEntries(vec, true);
0576   }
0577 }
0578 
0579 void FetchDialog::slotMoreClicked() {
0580   if(m_started) {
0581     myDebug() << "can't continue while running";
0582     return;
0583   }
0584 
0585   m_started = true;
0586   KGuiItem::assign(m_searchButton, KGuiItem(i18n(FETCH_STRING_STOP),
0587                                             QIcon::fromTheme(QStringLiteral("dialog-cancel"))));
0588   startProgress();
0589   setStatus(i18n("Searching..."));
0590   qApp->processEvents();
0591   Fetch::Manager::self()->continueSearch();
0592 }
0593 
0594 void FetchDialog::slotShowEntry() {
0595   // just in case
0596   m_statusMessages.clear();
0597 
0598   QList<QTreeWidgetItem*> items = m_treeWidget->selectedItems();
0599   if(items.isEmpty()) {
0600     m_addButton->setEnabled(false);
0601     return;
0602   }
0603 
0604   m_addButton->setEnabled(true);
0605   if(items.count() > 1) {
0606     m_entryView->clear();
0607     return;
0608   }
0609 
0610   FetchResultItem* item = static_cast<FetchResultItem*>(items.first());
0611   Fetch::FetchResult* r = item->m_result;
0612   setStatus(i18n("Fetching %1...", r->title));
0613   Data::EntryPtr entry = m_entries.value(r->uid);
0614   if(!entry) {
0615     GUI::CursorSaver cs;
0616     startProgress();
0617     entry = r->fetchEntry();
0618     if(entry) { // might conceivably be null
0619       m_entries.insert(r->uid, entry);
0620     }
0621     stopProgress();
0622   }
0623   if(!entry || !entry->collection())  {
0624     myDebug() << "no entry or collection pointer";
0625     setStatus(i18n("Ready."));
0626     return;
0627   }
0628   if(!entry->collection()->hasField(QStringLiteral("fetchdialog_source"))) {
0629     Data::FieldPtr f(new Data::Field(QStringLiteral("fetchdialog_source"), i18n("Attribution"), Data::Field::Para));
0630     entry->collection()->addField(f);
0631   }
0632 
0633   const QPixmap sourceIcon = Fetch::Manager::self()->fetcherIcon(r->fetcher());
0634   const QByteArray ba = Data::Image::byteArray(sourceIcon.toImage(), "PNG");
0635   QString text = QStringLiteral("<qt><img style='vertical-align: top' src='data:image/png;base64,%1'/> %2<br/>%3</qt>")
0636                  .arg(QLatin1String(ba.toBase64()), r->fetcher()->source(), r->fetcher()->attribution());
0637   entry->setField(QStringLiteral("fetchdialog_source"), text);
0638 
0639   setStatus(i18n("Ready."));
0640 
0641   m_entryView->showEntry(entry);
0642 }
0643 
0644 void FetchDialog::startProgress() {
0645   m_progress->show();
0646   m_timer->start(100);
0647 }
0648 
0649 void FetchDialog::slotMoveProgress() {
0650   m_progress->setValue(m_progress->value()+5);
0651 }
0652 
0653 void FetchDialog::stopProgress() {
0654   m_timer->stop();
0655   m_progress->hide();
0656 }
0657 
0658 void FetchDialog::slotInit() {
0659   // do this in the singleShot slot so it works
0660   // see note in entryeditdialog.cpp (Feb 2017)
0661   KConfigGroup config(KSharedConfig::openConfig(), "Fetch Dialog Options");
0662   KWindowConfig::restoreWindowSize(windowHandle(), config);
0663 
0664   if(!Fetch::Manager::self()->canFetch(Data::Document::self()->collection()->type())) {
0665     m_searchButton->setEnabled(false);
0666     Kernel::self()->sorry(i18n("No Internet sources are available for your current collection type."), this);
0667   }
0668 
0669   int key = config.readEntry("Search Key", int(Fetch::FetchFirst));
0670   // only change key if valid
0671   if(key > Fetch::FetchFirst) {
0672     m_keyCombo->setCurrentData(key);
0673   }
0674   slotKeyChanged(m_keyCombo->currentIndex());
0675 
0676   QString source = config.readEntry("Search Source");
0677   if(!source.isEmpty()) {
0678     int idx = m_sourceCombo->findText(source);
0679     if(idx > -1) {
0680       m_sourceCombo->setCurrentIndex(idx);
0681     }
0682   }
0683   slotSourceChanged(m_sourceCombo->currentText());
0684 
0685   m_valueLineEdit->setFocus();
0686   m_searchButton->setDefault(true);
0687 }
0688 
0689 void FetchDialog::slotKeyChanged(int idx_) {
0690   int key = m_keyCombo->itemData(idx_).toInt();
0691   if(key == Fetch::ISBN || key == Fetch::UPC || key == Fetch::LCCN) {
0692     m_multipleISBN->setEnabled(true);
0693     if(key == Fetch::ISBN) {
0694       m_valueLineEdit->setValidator(new ISBNValidator(this));
0695     } else {
0696       UPCValidator* upc = new UPCValidator(this);
0697       connect(upc, &UPCValidator::signalISBN, this, &FetchDialog::slotUPC2ISBN);
0698       m_valueLineEdit->setValidator(upc);
0699       // only want to convert to ISBN if ISBN is accepted by the fetcher
0700       Fetch::KeyMap map = Fetch::Manager::self()->keyMap(m_sourceCombo->currentText());
0701       upc->setCheckISBN(map.contains(Fetch::ISBN));
0702     }
0703   } else {
0704     m_multipleISBN->setChecked(false);
0705     m_multipleISBN->setEnabled(false);
0706 //    slotMultipleISBN(false);
0707     m_valueLineEdit->setValidator(nullptr);
0708   }
0709 
0710   if(key == Fetch::ISBN || key == Fetch::UPC) {
0711     openBarcodePreview();
0712   } else {
0713     closeBarcodePreview();
0714   }
0715 }
0716 
0717 void FetchDialog::slotSourceChanged(const QString& source_) {
0718   int curr = m_keyCombo->currentData().toInt();
0719   m_keyCombo->clear();
0720   Fetch::KeyMap map = Fetch::Manager::self()->keyMap(source_);
0721   for(Fetch::KeyMap::ConstIterator it = map.constBegin(); it != map.constEnd(); ++it) {
0722     m_keyCombo->addItem(it.value(), it.key());
0723   }
0724   m_keyCombo->setCurrentData(curr);
0725   slotKeyChanged(m_keyCombo->currentIndex());
0726 }
0727 
0728 void FetchDialog::slotMultipleISBN(bool toggle_) {
0729   bool wasEnabled = m_valueLineEdit->isEnabled();
0730   m_valueLineEdit->setEnabled(!toggle_);
0731   if(!wasEnabled && m_valueLineEdit->isEnabled()) {
0732     // if we enable it, it probably had multiple isbn values
0733     // the validator doesn't like that, so only keep the first value
0734     QString val = m_valueLineEdit->text().section(QLatin1Char(';'), 0, 0);
0735     m_valueLineEdit->setText(val);
0736   }
0737   m_editISBN->setEnabled(toggle_);
0738   if(toggle_) {
0739     // if we're editing multiple values, it makes sense to popup the dialog now
0740     slotEditMultipleISBN();
0741   }
0742 }
0743 
0744 void FetchDialog::slotEditMultipleISBN() {
0745   QDialog dlg(this);
0746   dlg.setModal(true);
0747   dlg.setWindowTitle(i18n("Edit ISBN/UPC Values"));
0748 
0749   QVBoxLayout* mainLayout = new QVBoxLayout();
0750   dlg.setLayout(mainLayout);
0751 
0752   QWidget* box = new QWidget(&dlg);
0753   QVBoxLayout* boxVBoxLayout = new QVBoxLayout(box);
0754   boxVBoxLayout->setMargin(0);
0755   boxVBoxLayout->setSpacing(10);
0756   mainLayout->addWidget(box);
0757 
0758   QString s = i18n("<qt>Enter the ISBN or UPC values, one per line.</qt>");
0759   QLabel* l = new QLabel(s, box);
0760   boxVBoxLayout->addWidget(l);
0761   m_isbnTextEdit = new KTextEdit(box);
0762   boxVBoxLayout->addWidget(m_isbnTextEdit);
0763   if(m_isbnList.isEmpty()) {
0764     m_isbnTextEdit->setText(m_valueLineEdit->text());
0765   } else {
0766     m_isbnTextEdit->setText(m_isbnList.join(QLatin1String("\n")));
0767   }
0768   m_isbnTextEdit->setWhatsThis(s);
0769   connect(m_isbnTextEdit.data(), &QTextEdit::textChanged, this, &FetchDialog::slotISBNTextChanged);
0770 
0771   QPushButton* fromFileBtn = new QPushButton(box);
0772   boxVBoxLayout->addWidget(fromFileBtn);
0773   KGuiItem::assign(fromFileBtn, KStandardGuiItem::open());
0774   fromFileBtn->setText(i18n("&Load From File..."));
0775   fromFileBtn->setWhatsThis(i18n("<qt>Load the list from a text file.</qt>"));
0776   connect(fromFileBtn, &QAbstractButton::clicked, this, &FetchDialog::slotLoadISBNList);
0777 
0778   QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel);
0779   boxVBoxLayout->addWidget(buttonBox);
0780   connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
0781   connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
0782 
0783   dlg.setMinimumWidth(qMax(dlg.minimumWidth(), FETCH_MIN_WIDTH*2/3));
0784 
0785   if(dlg.exec() == QDialog::Accepted) {
0786     m_isbnList = m_isbnTextEdit->toPlainText().split(QStringLiteral("\n"));
0787     const QValidator* val = m_valueLineEdit->validator();
0788     if(val) {
0789       for(QStringList::Iterator it = m_isbnList.begin(); it != m_isbnList.end(); ++it) {
0790         val->fixup(*it);
0791         if((*it).isEmpty()) {
0792           it = m_isbnList.erase(it);
0793           // this is next item, shift backward
0794           --it;
0795         }
0796       }
0797     }
0798     if(m_isbnList.count() > 100) {
0799       Kernel::self()->sorry(i18n("<qt>An ISBN search can contain a maximum of 100 ISBN values. Only the "
0800                                  "first 100 values in your list will be used.</qt>"), this);
0801       m_isbnList = m_isbnList.mid(0, 100);
0802     }
0803     m_valueLineEdit->setText(m_isbnList.join(FieldFormat::delimiterString()));
0804   }
0805   m_isbnTextEdit = nullptr; // gets auto-deleted
0806 }
0807 
0808 void FetchDialog::slotLoadISBNList() {
0809   if(!m_isbnTextEdit) {
0810     return;
0811   }
0812   QUrl u = QUrl::fromLocalFile(QFileDialog::getOpenFileName(this, QString(), QString(), QString()));
0813   if(u.isValid()) {
0814     m_isbnTextEdit->setText(m_isbnTextEdit->toPlainText() + FileHandler::readTextFile(u));
0815     m_isbnTextEdit->moveCursor(QTextCursor::End);
0816     m_isbnTextEdit->ensureCursorVisible();
0817   }
0818 }
0819 
0820 void FetchDialog::slotISBNTextChanged() {
0821   if(!m_isbnTextEdit) {
0822     return;
0823   }
0824   const QValidator* val = m_valueLineEdit->validator();
0825   if(!val) {
0826     return;
0827   }
0828   const QString text = m_isbnTextEdit->toPlainText();
0829   if(text.isEmpty())  {
0830     return;
0831   }
0832   const QTextCursor cursor = m_isbnTextEdit->textCursor();
0833   // only try to validate if char before cursor is an eol
0834   if(cursor.atStart() || text.at(cursor.position()-1) != QLatin1Char('\n')) {
0835     return;
0836   }
0837   QStringList lines = text.left(cursor.position()-1).split(QStringLiteral("\n"));
0838   QString newLine = lines.last();
0839   int pos = 0;
0840   // validate() changes the input
0841   if(val->validate(newLine, pos) != QValidator::Acceptable) {
0842     return;
0843   }
0844   lines.replace(lines.count()-1, newLine);
0845   QString newText = lines.join(QLatin1String("\n")) + text.mid(cursor.position()-1);
0846   if(newText == text) {
0847     return;
0848   }
0849 
0850   if(newText.isEmpty()) {
0851     m_isbnTextEdit->clear();
0852   } else {
0853     m_isbnTextEdit->blockSignals(true);
0854     m_isbnTextEdit->setPlainText(newText);
0855     m_isbnTextEdit->setTextCursor(cursor);
0856     m_isbnTextEdit->blockSignals(false);
0857   }
0858 }
0859 
0860 void FetchDialog::slotUPC2ISBN() {
0861   int key = m_keyCombo->currentData().toInt();
0862   if(key == Fetch::UPC) {
0863     m_keyCombo->setCurrentData(Fetch::ISBN);
0864     slotKeyChanged(m_keyCombo->currentIndex());
0865   }
0866 }
0867 
0868 void FetchDialog::columnResized(int column_) {
0869   // only care about the middle two. First is the checkmark icon, last is not resizeable
0870   if(column_ == 1 || column_ == 2) {
0871     m_treeWasResized = true;
0872   }
0873 }
0874 
0875 void FetchDialog::slotResetCollection() {
0876   if(m_collType == Kernel::self()->collectionType()) {
0877     return;
0878   }
0879   m_collType = Kernel::self()->collectionType();
0880   m_sourceCombo->clear();
0881   Fetch::FetcherVec sources = Fetch::Manager::self()->fetchers(m_collType);
0882   foreach(Fetch::Fetcher::Ptr fetcher, sources) {
0883     m_sourceCombo->addItem(Fetch::Manager::self()->fetcherIcon(fetcher.data()), fetcher->source());
0884   }
0885 
0886   if(Fetch::Manager::self()->canFetch(Data::Document::self()->collection()->type())) {
0887     m_searchButton->setEnabled(true);
0888   } else {
0889     m_searchButton->setEnabled(false);
0890     Kernel::self()->sorry(i18n("No Internet sources are available for your current collection type."), this);
0891   }
0892 }
0893 
0894 void FetchDialog::slotBarcodeRecognized(const QString& string_) {
0895   // attention: this slot is called in the context of another thread => do not use GUI-functions!
0896   StringDataEvent* e = new StringDataEvent(string_);
0897   qApp->postEvent(this, e); // the event loop will call FetchDialog::customEvent() in the context of the GUI thread
0898 }
0899 
0900 void FetchDialog::slotBarcodeGotImage(const QImage& img_)  {
0901   // attention: this slot is called in the context of another thread => do not use GUI-functions!
0902   ImageDataEvent* e = new ImageDataEvent(img_);
0903   qApp->postEvent(this, e); // the event loop will call FetchDialog::customEvent() in the context of the GUI thread
0904 }
0905 
0906 void FetchDialog::openBarcodePreview() {
0907   if(!Config::enableWebcam()) {
0908     return;
0909   }
0910 #ifdef ENABLE_WEBCAM
0911   if(m_barcodePreview) {
0912     m_barcodePreview->show();
0913     m_barcodeRecognitionThread->start();
0914     return;
0915   }
0916 
0917   // barcode recognition
0918   m_barcodeRecognitionThread = new barcodeRecognitionThread();
0919   if(m_barcodeRecognitionThread->isWebcamAvailable()) {
0920     m_barcodePreview = new QLabel(nullptr);
0921     m_barcodePreview->resize(m_barcodeRecognitionThread->getPreviewSize());
0922     m_barcodePreview->move(QApplication::desktop()->screenGeometry(m_barcodePreview).width()
0923                            - m_barcodePreview->frameGeometry().width(), 30);
0924     m_barcodePreview->show();
0925 
0926     connect(m_barcodeRecognitionThread, &barcodeRecognition::barcodeRecognitionThread::recognized,
0927             this, &FetchDialog::slotBarcodeRecognized);
0928     connect(m_barcodeRecognitionThread, &barcodeRecognition::barcodeRecognitionThread::gotImage,
0929             this, &FetchDialog::slotBarcodeGotImage);
0930 //    connect( m_barcodePreview, SIGNAL(destroyed(QObject *)), this, SLOT(slotBarcodeStop()) );
0931     m_barcodeRecognitionThread->start();
0932   }
0933 #endif
0934 }
0935 
0936 void FetchDialog::closeBarcodePreview() {
0937 #ifdef ENABLE_WEBCAM
0938   if(!m_barcodePreview || !m_barcodeRecognitionThread) {
0939     return;
0940   }
0941 
0942   m_barcodePreview->hide();
0943   m_barcodeRecognitionThread->stop();
0944 #endif
0945 }
0946 
0947 void FetchDialog::customEvent(QEvent* e) {
0948   if(!e) {
0949     return;
0950   }
0951   if(e->type() == StringDataType) {
0952     // slotBarcodeRecognized() queued call
0953     qApp->beep();
0954     m_valueLineEdit->setText(static_cast<StringDataEvent*>(e)->string());
0955     m_searchButton->animateClick();
0956   } else if(e->type() == ImageDataType) {
0957     // slotBarcodegotImage() queued call
0958     m_barcodePreview->setPixmap(QPixmap::fromImage(static_cast<ImageDataEvent*>(e)->image()));
0959   }
0960 }