File indexing completed on 2024-04-28 16:31:59

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->setMaximum(0);
0317   m_progress->setFixedHeight(fontMetrics().height()+2);
0318   m_progress->hide();
0319   m_statusBar->addPermanentWidget(m_progress);
0320   m_statusBar->setSizeGripEnabled(false);
0321 
0322   QPushButton* closeButton = new QPushButton(bottombox);
0323   KGuiItem::assign(closeButton, KStandardGuiItem::close());
0324   bottomboxHBoxLayout->addWidget(closeButton);
0325   connect(closeButton, &QAbstractButton::clicked, this, &QDialog::accept);
0326 
0327   connect(m_timer, &QTimer::timeout, this, &FetchDialog::slotMoveProgress);
0328 
0329   setMinimumWidth(qMax(minimumWidth(), qMax(FETCH_MIN_WIDTH, minimumSizeHint().width())));
0330   setStatus(i18n("Ready."));
0331 
0332   KConfigGroup config(KSharedConfig::openConfig(), "Fetch Dialog Options");
0333   QList<int> splitList = config.readEntry("Splitter Sizes", QList<int>());
0334   if(!splitList.empty()) {
0335     split->setSizes(splitList);
0336   }
0337 
0338   connect(Fetch::Manager::self(), &Fetch::Manager::signalResultFound,
0339                                   this, &FetchDialog::slotResultFound);
0340   connect(Fetch::Manager::self(), &Fetch::Manager::signalStatus,
0341                                   this, &FetchDialog::slotStatus);
0342   connect(Fetch::Manager::self(), &Fetch::Manager::signalDone,
0343                                   this, &FetchDialog::slotFetchDone);
0344 
0345   KAcceleratorManager::manage(this);
0346   // initialize combos
0347   QTimer::singleShot(0, this, &FetchDialog::slotInit);
0348 }
0349 
0350 FetchDialog::~FetchDialog() {
0351 #ifdef ENABLE_WEBCAM
0352   if(m_barcodeRecognitionThread) {
0353     m_barcodeRecognitionThread->stop();
0354     if(!m_barcodeRecognitionThread->wait( 1000 )) {
0355       m_barcodeRecognitionThread->terminate();
0356     }
0357     delete m_barcodeRecognitionThread;
0358     m_barcodeRecognitionThread = nullptr;
0359   }
0360   if(m_barcodePreview) {
0361     delete m_barcodePreview;
0362     m_barcodePreview = nullptr;
0363   }
0364 #endif
0365 
0366   qDeleteAll(m_results);
0367   m_results.clear();
0368 
0369   // we might have downloaded a lot of images we don't need to keep
0370   Data::EntryList entriesToCheck;
0371   foreach(Data::EntryPtr entry, m_entries) {
0372     entriesToCheck.append(entry);
0373   }
0374   // no additional entries to check images to keep though
0375   Data::Document::self()->removeImagesNotInCollection(entriesToCheck, Data::EntryList());
0376 
0377   KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("Fetch Dialog Options"));
0378   KWindowConfig::saveWindowSize(windowHandle(), config);
0379 
0380   config.writeEntry("Splitter Sizes", static_cast<QSplitter*>(m_treeWidget->parentWidget())->sizes());
0381   config.writeEntry("Search Key", m_keyCombo->currentData().toInt());
0382   config.writeEntry("Search Source", m_sourceCombo->currentText());
0383 }
0384 
0385 void FetchDialog::closeEvent(QCloseEvent* event_) { // stop fetchers when the dialog is closed
0386   if(m_started) {
0387     QTimer::singleShot(0, Fetch::Manager::self(), &Fetch::Manager::stop);
0388   }
0389   QDialog::closeEvent(event_);
0390 }
0391 
0392 void FetchDialog::slotSearchClicked() {
0393   m_valueLineEdit->selectAll();
0394   if(m_started) {
0395     setStatus(i18n("Cancelling the search..."));
0396     Fetch::Manager::self()->stop();
0397   } else {
0398     const QString value = m_valueLineEdit->text().simplified();
0399     if(value.isEmpty()) {
0400       return;
0401     }
0402     m_resultCount = 0;
0403     m_oldSearch = value;
0404     m_started = true;
0405     KGuiItem::assign(m_searchButton, KGuiItem(i18n(FETCH_STRING_STOP),
0406                                               QIcon::fromTheme(QStringLiteral("dialog-cancel"))));
0407     startProgress();
0408     setStatus(i18n("Searching..."));
0409     qApp->processEvents();
0410     Fetch::Manager::self()->startSearch(m_sourceCombo->currentText(),
0411                                         static_cast<Fetch::FetchKey>(m_keyCombo->currentData().toInt()),
0412                                         value,
0413                                         Data::Document::self()->collection()->type());
0414   }
0415 }
0416 
0417 void FetchDialog::slotClearClicked() {
0418   fetchDone(false);
0419   m_treeWidget->clear();
0420   m_entryView->clear();
0421   Fetch::Manager::self()->stop();
0422   m_multipleISBN->setChecked(false);
0423   m_valueLineEdit->clear();
0424   m_valueLineEdit->setFocus();
0425   m_addButton->setEnabled(false);
0426   m_moreButton->setEnabled(false);
0427   m_isbnList.clear();
0428   m_statusMessages.clear();
0429   setStatus(i18n("Ready.")); // because slotFetchDone() writes text
0430 }
0431 
0432 void FetchDialog::slotStatus(const QString& status_) {
0433   m_statusMessages.push_back(status_);
0434   // if the queue was empty, start the timer
0435   if(m_statusMessages.count() == 1) {
0436     // wait 2 seconds
0437     QTimer::singleShot(2000, this, &FetchDialog::slotUpdateStatus);
0438   }
0439 }
0440 
0441 void FetchDialog::slotUpdateStatus() {
0442   if(m_statusMessages.isEmpty()) {
0443     return;
0444   }
0445   setStatus(m_statusMessages.front());
0446   m_statusMessages.pop_front();
0447   if(!m_statusMessages.isEmpty()) {
0448     // wait 2 seconds
0449     QTimer::singleShot(2000, this, &FetchDialog::slotUpdateStatus);
0450   }
0451 }
0452 
0453 void FetchDialog::setStatus(const QString& text_) {
0454   m_statusLabel->setText(QLatin1Char(' ') + text_);
0455 }
0456 
0457 void FetchDialog::slotFetchDone() {
0458   fetchDone(true);
0459 }
0460 
0461 void FetchDialog::fetchDone(bool checkISBN_) {
0462 //  myDebug() << "fetchDone";
0463   m_started = false;
0464   KGuiItem::assign(m_searchButton, KGuiItem(i18n(FETCH_STRING_SEARCH),
0465                                             QIcon::fromTheme(QStringLiteral("edit-find"))));
0466   stopProgress();
0467   if(m_resultCount == 0) {
0468     slotStatus(i18n("The search returned no items."));
0469   } else {
0470     /* TRANSLATORS: This is a plural form, you need to translate both lines (except "_n: ") */
0471     slotStatus(i18np("The search returned 1 item.",
0472                      "The search returned %1 items.",
0473                      m_resultCount));
0474   }
0475   m_moreButton->setEnabled(Fetch::Manager::self()->hasMoreResults());
0476 
0477   // if we're not checking isbn values, then, ok to return
0478   if(!checkISBN_) {
0479     return;
0480   }
0481 
0482   const Fetch::FetchKey key = static_cast<Fetch::FetchKey>(m_keyCombo->currentData().toInt());
0483   // no way to currently check EAN/UPC values for non-book items
0484   if(m_collType & (Data::Collection::Book | Data::Collection::Bibtex) &&
0485      m_multipleISBN->isChecked() &&
0486      (key == Fetch::ISBN || key == Fetch::UPC)) {
0487     QStringList searchValues = FieldFormat::splitValue(m_oldSearch.simplified());
0488     QStringList resultValues;
0489     resultValues.reserve(m_treeWidget->topLevelItemCount());
0490     for(int i = 0; i < m_treeWidget->topLevelItemCount(); ++i) {
0491       resultValues << static_cast<FetchResultItem*>(m_treeWidget->topLevelItem(i))->m_result->isbn;
0492     }
0493     // Google Book Search can have an error, returning a different ISBN in the initial search
0494     // than the one returned by fetchEntryHook(). As a small workaround, if only a single ISBN value
0495     // is in the search term, then don't show
0496     if(searchValues.count() > 1) {
0497       const QStringList valuesNotFound = ISBNValidator::listDifference(searchValues, resultValues);
0498       if(!valuesNotFound.isEmpty()) {
0499         KMessageBox::informationList(this,
0500                                      i18n("No results were found for the following ISBN values:"),
0501                                      valuesNotFound,
0502                                      i18n("No Results"));
0503       }
0504     }
0505   }
0506 }
0507 
0508 void FetchDialog::slotResultFound(Tellico::Fetch::FetchResult* result_) {
0509   m_results.append(result_);
0510   (void) new FetchResultItem(m_treeWidget, result_);
0511   // resize final column to size of contents if the user has never resized anything before
0512   if(!m_treeWasResized) {
0513     m_treeWidget->header()->setStretchLastSection(false);
0514 
0515     // do math to try to make a nice resizing, emphasizing sections 1 and 2 over 3
0516     const int w0 = m_treeWidget->columnWidth(0);
0517     const int w1 = m_treeWidget->columnWidth(1);
0518     const int w2 = m_treeWidget->columnWidth(2);
0519     const int w3 = m_treeWidget->columnWidth(3);
0520     const int wt = m_treeWidget->width();
0521 //    myDebug() << "OLD:" << w0 << w1 << w2 << w3 << wt << (w0+w1+w2+w3);
0522 
0523     // whatever is leftover from resizing 3, split between 1 and 2
0524     if(wt > (w0 + w1 + w2 + w3)) {
0525       const int w1rec = static_cast<TreeWidget*>(m_treeWidget)->sizeHintForColumn(1);
0526       const int w2rec = static_cast<TreeWidget*>(m_treeWidget)->sizeHintForColumn(2);
0527       const int w3rec = static_cast<TreeWidget*>(m_treeWidget)->sizeHintForColumn(3);
0528       if(w1 < w1rec || w2 < w2rec) {
0529         const int w3new = qMin(w3, w3rec);
0530         const int diff = wt - w0 - w1 - w2 - w3new;
0531         const int w1new = qBound(w1, w1rec, w1 + diff/2 - 4);
0532         const int w2new = qBound(w2, wt - w0 - w1new - w3new, w2rec);
0533         m_treeWidget->setColumnWidth(1, w1new);
0534         m_treeWidget->setColumnWidth(2, w2new);
0535         m_treeWidget->setColumnWidth(3, w3new);
0536 //        myDebug() << "New:" << w0 << w1new << w2new << w3new << wt << (w0+w1new+w2new+w3new);
0537       }
0538     }
0539     m_treeWidget->header()->setStretchLastSection(true);
0540     // because calling setColumnWidth() will change this
0541     m_treeWasResized = false;
0542   }
0543   ++m_resultCount;
0544 }
0545 
0546 void FetchDialog::slotAddEntry() {
0547   GUI::CursorSaver cs;
0548   Data::EntryList vec;
0549   foreach(QTreeWidgetItem* item_, m_treeWidget->selectedItems()) {
0550     FetchResultItem* item = static_cast<FetchResultItem*>(item_);
0551 
0552     Fetch::FetchResult* r = item->m_result;
0553     Data::EntryPtr entry = m_entries.value(r->uid);
0554     if(!entry) {
0555       setStatus(i18n("Fetching %1...", r->title));
0556       startProgress();
0557       entry = r->fetchEntry();
0558       if(!entry) {
0559         continue;
0560       }
0561       m_entries.insert(r->uid, entry);
0562       stopProgress();
0563       setStatus(i18n("Ready."));
0564     }
0565     if(entry->collection()->hasField(QStringLiteral("fetchdialog_source"))) {
0566       entry->collection()->removeField(QStringLiteral("fetchdialog_source"));
0567     }
0568     // add a copy, intentionally allowing multiple copies to be added
0569     vec.append(Data::EntryPtr(new Data::Entry(*entry)));
0570     item->setData(0, Qt::DecorationRole,
0571                   QIcon::fromTheme(QStringLiteral("checkmark"), QIcon(QLatin1String(":/icons/checkmark"))));
0572   }
0573   if(!vec.isEmpty()) {
0574     Kernel::self()->addEntries(vec, true);
0575   }
0576 }
0577 
0578 void FetchDialog::slotMoreClicked() {
0579   if(m_started) {
0580     myDebug() << "can't continue while running";
0581     return;
0582   }
0583 
0584   m_started = true;
0585   KGuiItem::assign(m_searchButton, KGuiItem(i18n(FETCH_STRING_STOP),
0586                                             QIcon::fromTheme(QStringLiteral("dialog-cancel"))));
0587   startProgress();
0588   setStatus(i18n("Searching..."));
0589   qApp->processEvents();
0590   Fetch::Manager::self()->continueSearch();
0591 }
0592 
0593 void FetchDialog::slotShowEntry() {
0594   // just in case
0595   m_statusMessages.clear();
0596 
0597   QList<QTreeWidgetItem*> items = m_treeWidget->selectedItems();
0598   if(items.isEmpty()) {
0599     m_addButton->setEnabled(false);
0600     return;
0601   }
0602 
0603   m_addButton->setEnabled(true);
0604   if(items.count() > 1) {
0605     m_entryView->clear();
0606     return;
0607   }
0608 
0609   FetchResultItem* item = static_cast<FetchResultItem*>(items.first());
0610   Fetch::FetchResult* r = item->m_result;
0611   setStatus(i18n("Fetching %1...", r->title));
0612   Data::EntryPtr entry = m_entries.value(r->uid);
0613   if(!entry) {
0614     GUI::CursorSaver cs;
0615     startProgress();
0616     entry = r->fetchEntry();
0617     if(entry) { // might conceivably be null
0618       m_entries.insert(r->uid, entry);
0619     }
0620     stopProgress();
0621   }
0622   if(!entry || !entry->collection())  {
0623     myDebug() << "no entry or collection pointer";
0624     setStatus(i18n("Ready."));
0625     return;
0626   }
0627   if(!entry->collection()->hasField(QStringLiteral("fetchdialog_source"))) {
0628     Data::FieldPtr f(new Data::Field(QStringLiteral("fetchdialog_source"), i18n("Attribution"), Data::Field::Para));
0629     entry->collection()->addField(f);
0630   }
0631 
0632   const QPixmap sourceIcon = Fetch::Manager::self()->fetcherIcon(r->fetcher());
0633   const QByteArray ba = Data::Image::byteArray(sourceIcon.toImage(), "PNG");
0634   QString text = QStringLiteral("<qt><img style='vertical-align: top' src='data:image/png;base64,%1'/> %2<br/>%3</qt>")
0635                  .arg(QLatin1String(ba.toBase64()), r->fetcher()->source(), r->fetcher()->attribution());
0636   entry->setField(QStringLiteral("fetchdialog_source"), text);
0637 
0638   setStatus(i18n("Ready."));
0639 
0640   m_entryView->showEntry(entry);
0641 }
0642 
0643 void FetchDialog::startProgress() {
0644   m_progress->show();
0645   m_timer->start(100);
0646 }
0647 
0648 void FetchDialog::slotMoveProgress() {
0649   m_progress->setValue(m_progress->value()+5);
0650 }
0651 
0652 void FetchDialog::stopProgress() {
0653   m_timer->stop();
0654   m_progress->hide();
0655 }
0656 
0657 void FetchDialog::slotInit() {
0658   // do this in the singleShot slot so it works
0659   // see note in entryeditdialog.cpp (Feb 2017)
0660   KConfigGroup config(KSharedConfig::openConfig(), "Fetch Dialog Options");
0661   KWindowConfig::restoreWindowSize(windowHandle(), config);
0662 
0663   if(!Fetch::Manager::self()->canFetch(Data::Document::self()->collection()->type())) {
0664     m_searchButton->setEnabled(false);
0665     Kernel::self()->sorry(i18n("No Internet sources are available for your current collection type."), this);
0666   }
0667 
0668   int key = config.readEntry("Search Key", int(Fetch::FetchFirst));
0669   // only change key if valid
0670   if(key > Fetch::FetchFirst) {
0671     m_keyCombo->setCurrentData(key);
0672   }
0673   slotKeyChanged(m_keyCombo->currentIndex());
0674 
0675   QString source = config.readEntry("Search Source");
0676   if(!source.isEmpty()) {
0677     int idx = m_sourceCombo->findText(source);
0678     if(idx > -1) {
0679       m_sourceCombo->setCurrentIndex(idx);
0680     }
0681   }
0682   slotSourceChanged(m_sourceCombo->currentText());
0683 
0684   m_valueLineEdit->setFocus();
0685   m_searchButton->setDefault(true);
0686 }
0687 
0688 void FetchDialog::slotKeyChanged(int idx_) {
0689   int key = m_keyCombo->itemData(idx_).toInt();
0690   if(key == Fetch::ISBN || key == Fetch::UPC || key == Fetch::LCCN) {
0691     m_multipleISBN->setEnabled(true);
0692     if(key == Fetch::ISBN) {
0693       m_valueLineEdit->setValidator(new ISBNValidator(this));
0694     } else {
0695       UPCValidator* upc = new UPCValidator(this);
0696       connect(upc, &UPCValidator::signalISBN, this, &FetchDialog::slotUPC2ISBN);
0697       m_valueLineEdit->setValidator(upc);
0698       // only want to convert to ISBN if ISBN is accepted by the fetcher
0699       Fetch::KeyMap map = Fetch::Manager::self()->keyMap(m_sourceCombo->currentText());
0700       upc->setCheckISBN(map.contains(Fetch::ISBN));
0701     }
0702   } else {
0703     m_multipleISBN->setChecked(false);
0704     m_multipleISBN->setEnabled(false);
0705 //    slotMultipleISBN(false);
0706     m_valueLineEdit->setValidator(nullptr);
0707   }
0708 
0709   if(key == Fetch::ISBN || key == Fetch::UPC) {
0710     openBarcodePreview();
0711   } else {
0712     closeBarcodePreview();
0713   }
0714 }
0715 
0716 void FetchDialog::slotSourceChanged(const QString& source_) {
0717   int curr = m_keyCombo->currentData().toInt();
0718   m_keyCombo->clear();
0719   Fetch::KeyMap map = Fetch::Manager::self()->keyMap(source_);
0720   for(Fetch::KeyMap::ConstIterator it = map.constBegin(); it != map.constEnd(); ++it) {
0721     m_keyCombo->addItem(it.value(), it.key());
0722   }
0723   m_keyCombo->setCurrentData(curr);
0724   slotKeyChanged(m_keyCombo->currentIndex());
0725 }
0726 
0727 void FetchDialog::slotMultipleISBN(bool toggle_) {
0728   bool wasEnabled = m_valueLineEdit->isEnabled();
0729   m_valueLineEdit->setEnabled(!toggle_);
0730   if(!wasEnabled && m_valueLineEdit->isEnabled()) {
0731     // if we enable it, it probably had multiple isbn values
0732     // the validator doesn't like that, so only keep the first value
0733     QString val = m_valueLineEdit->text().section(QLatin1Char(';'), 0, 0);
0734     m_valueLineEdit->setText(val);
0735   }
0736   m_editISBN->setEnabled(toggle_);
0737   if(toggle_) {
0738     // if we're editing multiple values, it makes sense to popup the dialog now
0739     slotEditMultipleISBN();
0740   }
0741 }
0742 
0743 void FetchDialog::slotEditMultipleISBN() {
0744   QDialog dlg(this);
0745   dlg.setModal(true);
0746   dlg.setWindowTitle(i18n("Edit ISBN/UPC Values"));
0747 
0748   QVBoxLayout* mainLayout = new QVBoxLayout();
0749   dlg.setLayout(mainLayout);
0750 
0751   QWidget* box = new QWidget(&dlg);
0752   QVBoxLayout* boxVBoxLayout = new QVBoxLayout(box);
0753   boxVBoxLayout->setMargin(0);
0754   boxVBoxLayout->setSpacing(10);
0755   mainLayout->addWidget(box);
0756 
0757   QString s = i18n("<qt>Enter the ISBN or UPC values, one per line.</qt>");
0758   QLabel* l = new QLabel(s, box);
0759   boxVBoxLayout->addWidget(l);
0760   m_isbnTextEdit = new KTextEdit(box);
0761   boxVBoxLayout->addWidget(m_isbnTextEdit);
0762   if(m_isbnList.isEmpty()) {
0763     m_isbnTextEdit->setText(m_valueLineEdit->text());
0764   } else {
0765     m_isbnTextEdit->setText(m_isbnList.join(QLatin1String("\n")));
0766   }
0767   m_isbnTextEdit->setWhatsThis(s);
0768   connect(m_isbnTextEdit.data(), &QTextEdit::textChanged, this, &FetchDialog::slotISBNTextChanged);
0769 
0770   QPushButton* fromFileBtn = new QPushButton(box);
0771   boxVBoxLayout->addWidget(fromFileBtn);
0772   KGuiItem::assign(fromFileBtn, KStandardGuiItem::open());
0773   fromFileBtn->setText(i18n("&Load From File..."));
0774   fromFileBtn->setWhatsThis(i18n("<qt>Load the list from a text file.</qt>"));
0775   connect(fromFileBtn, &QAbstractButton::clicked, this, &FetchDialog::slotLoadISBNList);
0776 
0777   QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel);
0778   boxVBoxLayout->addWidget(buttonBox);
0779   connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
0780   connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
0781 
0782   dlg.setMinimumWidth(qMax(dlg.minimumWidth(), FETCH_MIN_WIDTH*2/3));
0783 
0784   if(dlg.exec() == QDialog::Accepted) {
0785     m_isbnList = m_isbnTextEdit->toPlainText().split(QStringLiteral("\n"));
0786     const QValidator* val = m_valueLineEdit->validator();
0787     if(val) {
0788       for(QStringList::Iterator it = m_isbnList.begin(); it != m_isbnList.end(); ++it) {
0789         val->fixup(*it);
0790         if((*it).isEmpty()) {
0791           it = m_isbnList.erase(it);
0792           // this is next item, shift backward
0793           --it;
0794         }
0795       }
0796     }
0797     if(m_isbnList.count() > 100) {
0798       Kernel::self()->sorry(i18n("<qt>An ISBN search can contain a maximum of 100 ISBN values. Only the "
0799                                  "first 100 values in your list will be used.</qt>"), this);
0800       m_isbnList = m_isbnList.mid(0, 100);
0801     }
0802     m_valueLineEdit->setText(m_isbnList.join(FieldFormat::delimiterString()));
0803   }
0804   m_isbnTextEdit = nullptr; // gets auto-deleted
0805 }
0806 
0807 void FetchDialog::slotLoadISBNList() {
0808   if(!m_isbnTextEdit) {
0809     return;
0810   }
0811   QUrl u = QUrl::fromLocalFile(QFileDialog::getOpenFileName(this, QString(), QString(), QString()));
0812   if(u.isValid()) {
0813     m_isbnTextEdit->setText(m_isbnTextEdit->toPlainText() + FileHandler::readTextFile(u));
0814     m_isbnTextEdit->moveCursor(QTextCursor::End);
0815     m_isbnTextEdit->ensureCursorVisible();
0816   }
0817 }
0818 
0819 void FetchDialog::slotISBNTextChanged() {
0820   if(!m_isbnTextEdit) {
0821     return;
0822   }
0823   const QValidator* val = m_valueLineEdit->validator();
0824   if(!val) {
0825     return;
0826   }
0827   const QString text = m_isbnTextEdit->toPlainText();
0828   if(text.isEmpty())  {
0829     return;
0830   }
0831   const QTextCursor cursor = m_isbnTextEdit->textCursor();
0832   // only try to validate if char before cursor is an eol
0833   if(cursor.atStart() || text.at(cursor.position()-1) != QLatin1Char('\n')) {
0834     return;
0835   }
0836   QStringList lines = text.left(cursor.position()-1).split(QStringLiteral("\n"));
0837   QString newLine = lines.last();
0838   int pos = 0;
0839   // validate() changes the input
0840   if(val->validate(newLine, pos) != QValidator::Acceptable) {
0841     return;
0842   }
0843   lines.replace(lines.count()-1, newLine);
0844   QString newText = lines.join(QLatin1String("\n")) + text.mid(cursor.position()-1);
0845   if(newText == text) {
0846     return;
0847   }
0848 
0849   if(newText.isEmpty()) {
0850     m_isbnTextEdit->clear();
0851   } else {
0852     m_isbnTextEdit->blockSignals(true);
0853     m_isbnTextEdit->setPlainText(newText);
0854     m_isbnTextEdit->setTextCursor(cursor);
0855     m_isbnTextEdit->blockSignals(false);
0856   }
0857 }
0858 
0859 void FetchDialog::slotUPC2ISBN() {
0860   int key = m_keyCombo->currentData().toInt();
0861   if(key == Fetch::UPC) {
0862     m_keyCombo->setCurrentData(Fetch::ISBN);
0863     slotKeyChanged(m_keyCombo->currentIndex());
0864   }
0865 }
0866 
0867 void FetchDialog::columnResized(int column_) {
0868   // only care about the middle two. First is the checkmark icon, last is not resizeable
0869   if(column_ == 1 || column_ == 2) {
0870     m_treeWasResized = true;
0871   }
0872 }
0873 
0874 void FetchDialog::slotResetCollection() {
0875   if(m_collType == Kernel::self()->collectionType()) {
0876     return;
0877   }
0878   m_collType = Kernel::self()->collectionType();
0879   m_sourceCombo->clear();
0880   Fetch::FetcherVec sources = Fetch::Manager::self()->fetchers(m_collType);
0881   foreach(Fetch::Fetcher::Ptr fetcher, sources) {
0882     m_sourceCombo->addItem(Fetch::Manager::self()->fetcherIcon(fetcher.data()), fetcher->source());
0883   }
0884 
0885   if(Fetch::Manager::self()->canFetch(Data::Document::self()->collection()->type())) {
0886     m_searchButton->setEnabled(true);
0887   } else {
0888     m_searchButton->setEnabled(false);
0889     Kernel::self()->sorry(i18n("No Internet sources are available for your current collection type."), this);
0890   }
0891 }
0892 
0893 void FetchDialog::slotBarcodeRecognized(const QString& string_) {
0894   // attention: this slot is called in the context of another thread => do not use GUI-functions!
0895   StringDataEvent* e = new StringDataEvent(string_);
0896   qApp->postEvent(this, e); // the event loop will call FetchDialog::customEvent() in the context of the GUI thread
0897 }
0898 
0899 void FetchDialog::slotBarcodeGotImage(const QImage& img_)  {
0900   // attention: this slot is called in the context of another thread => do not use GUI-functions!
0901   ImageDataEvent* e = new ImageDataEvent(img_);
0902   qApp->postEvent(this, e); // the event loop will call FetchDialog::customEvent() in the context of the GUI thread
0903 }
0904 
0905 void FetchDialog::openBarcodePreview() {
0906   if(!Config::enableWebcam()) {
0907     return;
0908   }
0909 #ifdef ENABLE_WEBCAM
0910   if(m_barcodePreview) {
0911     m_barcodePreview->show();
0912     m_barcodeRecognitionThread->start();
0913     return;
0914   }
0915 
0916   // barcode recognition
0917   m_barcodeRecognitionThread = new barcodeRecognitionThread();
0918   if(m_barcodeRecognitionThread->isWebcamAvailable()) {
0919     m_barcodePreview = new QLabel(nullptr);
0920     m_barcodePreview->resize(m_barcodeRecognitionThread->getPreviewSize());
0921     m_barcodePreview->move(QApplication::desktop()->screenGeometry(m_barcodePreview).width()
0922                            - m_barcodePreview->frameGeometry().width(), 30);
0923     m_barcodePreview->show();
0924 
0925     connect(m_barcodeRecognitionThread, &barcodeRecognition::barcodeRecognitionThread::recognized,
0926             this, &FetchDialog::slotBarcodeRecognized);
0927     connect(m_barcodeRecognitionThread, &barcodeRecognition::barcodeRecognitionThread::gotImage,
0928             this, &FetchDialog::slotBarcodeGotImage);
0929 //    connect( m_barcodePreview, SIGNAL(destroyed(QObject *)), this, SLOT(slotBarcodeStop()) );
0930     m_barcodeRecognitionThread->start();
0931   }
0932 #endif
0933 }
0934 
0935 void FetchDialog::closeBarcodePreview() {
0936 #ifdef ENABLE_WEBCAM
0937   if(!m_barcodePreview || !m_barcodeRecognitionThread) {
0938     return;
0939   }
0940 
0941   m_barcodePreview->hide();
0942   m_barcodeRecognitionThread->stop();
0943 #endif
0944 }
0945 
0946 void FetchDialog::customEvent(QEvent* e) {
0947   if(!e) {
0948     return;
0949   }
0950   if(e->type() == StringDataType) {
0951     // slotBarcodeRecognized() queued call
0952     qApp->beep();
0953     m_valueLineEdit->setText(static_cast<StringDataEvent*>(e)->string());
0954     m_searchButton->animateClick();
0955   } else if(e->type() == ImageDataType) {
0956     // slotBarcodegotImage() queued call
0957     m_barcodePreview->setPixmap(QPixmap::fromImage(static_cast<ImageDataEvent*>(e)->image()));
0958   }
0959 }