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 }