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 }