1 /***************************************************************************
2  *   Copyright (C) 2004-2017 by Thomas Fischer <fischer@unix-ag.uni-kl.de> *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
16  ***************************************************************************/
17 
18 #include "searchform.h"
19 
20 #include <QLayout>
21 #include <QMap>
22 #include <QLabel>
PrintDiag(const DiagnosticOptions & Opts,FullSourceLoc Loc)23 #include <QListWidget>
24 #include <QSpinBox>
25 #include <QStackedWidget>
26 #include <QTabWidget>
27 #include <QProgressBar>
28 #include <QMimeDatabase>
29 #include <QMimeType>
30 #include <QTimer>
31 #include <QSet>
32 #include <QAction>
33 #include <QScrollArea>
34 #include <QIcon>
35 #include <QPushButton>
36 #include <QDebug>
37 
38 #include <KLineEdit>
39 #include <KLocalizedString>
40 #include <KRun>
41 #include <KMessageBox>
42 #include <KParts/Part>
43 #include <KParts/ReadOnlyPart>
44 #include <KConfigGroup>
45 #include <KSharedConfig>
46 #include <kio_version.h>
47 
48 #include "element.h"
49 #include "file.h"
50 #include "comment.h"
51 #include "fileexporterbibtex.h"
52 #include "onlinesearchabstract.h"
53 #include "onlinesearchgeneral.h"
54 #include "onlinesearchbibsonomy.h"
55 #include "onlinesearchgooglescholar.h"
56 #include "onlinesearchpubmed.h"
57 #include "onlinesearchieeexplore.h"
58 #include "onlinesearchacmportal.h"
59 #include "onlinesearchsciencedirect.h"
60 #include "onlinesearchspringerlink.h"
61 #include "onlinesearcharxiv.h"
62 #include "onlinesearchjstor.h"
63 #include "onlinesearchmathscinet.h"
64 #include "onlinesearchmrlookup.h"
65 #include "onlinesearchinspirehep.h"
66 #include "onlinesearchcernds.h"
67 #include "onlinesearchingentaconnect.h"
68 #include "onlinesearchsoanasaads.h"
69 #include "onlinesearchisbndb.h"
70 #include "onlinesearchideasrepec.h"
71 #include "onlinesearchdoi.h"
72 #include "onlinesearchbiorxiv.h"
73 #include "openfileinfo.h"
74 #include "fileview.h"
75 #include "models/filemodel.h"
76 #include "searchresults.h"
77 #include "logging_program.h"
78 
79 class SearchForm::SearchFormPrivate
80 {
81 private:
82     SearchForm *p;
83     QStackedWidget *queryTermsStack;
84     QWidget *listContainer;
85     QListWidget *enginesList;
86     QLabel *whichEnginesLabel;
87     QAction *actionOpenHomepage;
88 
89 public:
90     KSharedConfigPtr config;
91     const QString configGroupName;
92 
93     SearchResults *sr;
94     QMap<QListWidgetItem *, OnlineSearchAbstract *> itemToOnlineSearch;
95     QSet<OnlineSearchAbstract *> runningSearches;
96     QPushButton *searchButton;
97     QPushButton *useEntryButton;
98     OnlineSearchQueryFormGeneral *generalQueryTermsForm;
99     QTabWidget *tabWidget;
100     QSharedPointer<const Entry> currentEntry;
101     QProgressBar *progressBar;
102     QMap<OnlineSearchAbstract *, int> progressMap;
103     QMap<OnlineSearchQueryFormAbstract *, QScrollArea *> formToScrollArea;
104 
105     enum SearchFormPrivateRole {
106         /// Homepage of a search engine
107         HomepageRole = Qt::UserRole + 5,
108         /// Special widget for a search engine
109         WidgetRole = Qt::UserRole + 6,
110         /// Name of a search engine
111         NameRole = Qt::UserRole + 7
112     };
113 
114     SearchFormPrivate(SearchResults *searchResults, SearchForm *parent)
115             : p(parent), whichEnginesLabel(nullptr), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))),
116           configGroupName(QStringLiteral("Search Engines Docklet")), sr(searchResults), searchButton(nullptr), useEntryButton(nullptr), currentEntry(nullptr) {
117         createGUI();
118     }
119 
120     OnlineSearchQueryFormAbstract *currentQueryForm() {
121         QScrollArea *area = qobject_cast<QScrollArea *>(queryTermsStack->currentWidget());
122         return formToScrollArea.key(area, nullptr);
123     }
124 
125     QScrollArea *wrapInScrollArea(OnlineSearchQueryFormAbstract *form, QWidget *parent) {
126         QScrollArea *scrollArea = new QScrollArea(parent);
127         form->setParent(scrollArea);
128         scrollArea->setWidget(form);
129         scrollArea->setWidgetResizable(true);
130         scrollArea->setFrameShape(QFrame::NoFrame);
131         scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
132         scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
133         formToScrollArea.insert(form, scrollArea);
134         return scrollArea;
135     }
136 
137     QWidget *createQueryTermsStack(QWidget *parent) {
138         QWidget *container = new QWidget(parent);
139         QVBoxLayout *vLayout = new QVBoxLayout(container);
140 
141         whichEnginesLabel = new QLabel(container);
142         whichEnginesLabel->setWordWrap(true);
143         vLayout->addWidget(whichEnginesLabel);
144         vLayout->setStretchFactor(whichEnginesLabel, 0);
145         connect(whichEnginesLabel, &QLabel::linkActivated, p, &SearchForm::switchToEngines);
146 
147         vLayout->addSpacing(8);
148 
149         queryTermsStack = new QStackedWidget(container);
150         vLayout->addWidget(queryTermsStack);
151         vLayout->setStretchFactor(queryTermsStack, 5);
152 
153         QScrollArea *scrollArea = wrapInScrollArea(createGeneralQueryTermsForm(queryTermsStack), queryTermsStack);
154         queryTermsStack->addWidget(scrollArea);
155 
156         return container;
157     }
158 
159     OnlineSearchQueryFormAbstract *createGeneralQueryTermsForm(QWidget *parent = nullptr) {
160         generalQueryTermsForm = new OnlineSearchQueryFormGeneral(parent);
161         return generalQueryTermsForm;
162     }
163 
164     QWidget *createEnginesGUI(QWidget *parent) {
165         listContainer = new QWidget(parent);
166         QGridLayout *layout = new QGridLayout(listContainer);
167         layout->setRowStretch(0, 1);
168         layout->setRowStretch(1, 0);
169 
170         enginesList = new QListWidget(listContainer);
171         layout->addWidget(enginesList, 0, 0, 1, 1);
172         connect(enginesList, &QListWidget::itemChanged, p, &SearchForm::itemCheckChanged);
173         connect(enginesList, &QListWidget::currentItemChanged, p, &SearchForm::enginesListCurrentChanged);
174         enginesList->setSelectionMode(QAbstractItemView::NoSelection);
175 
176         actionOpenHomepage = new QAction(QIcon::fromTheme(QStringLiteral("internet-web-browser")), i18n("Go to Homepage"), p);
177         connect(actionOpenHomepage, &QAction::triggered, p, &SearchForm::openHomepage);
178         enginesList->addAction(actionOpenHomepage);
179         enginesList->setContextMenuPolicy(Qt::ActionsContextMenu);
180 
181         return listContainer;
182     }
183 
184     void createGUI() {
185         QGridLayout *layout = new QGridLayout(p);
186         layout->setMargin(0);
187         layout->setRowStretch(0, 1);
188         layout->setRowStretch(1, 0);
189         layout->setColumnStretch(0, 0);
190         layout->setColumnStretch(1, 1);
191         layout->setColumnStretch(2, 0);
192 
193         tabWidget = new QTabWidget(p);
194         tabWidget->setDocumentMode(true);
195         layout->addWidget(tabWidget, 0, 0, 1, 3);
196 
197         QWidget *widget = createQueryTermsStack(tabWidget);
198         tabWidget->addTab(widget, QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Query Terms"));
199 
200         QWidget *listContainer = createEnginesGUI(tabWidget);
201         tabWidget->addTab(listContainer, QIcon::fromTheme(QStringLiteral("applications-engineering")), i18n("Engines"));
202 
203         connect(tabWidget, &QTabWidget::currentChanged, p, &SearchForm::tabSwitched);
204 
205         useEntryButton = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), i18n("Use Entry"), p);
206         layout->addWidget(useEntryButton, 1, 0, 1, 1);
207         useEntryButton->setEnabled(false);
208         connect(useEntryButton, &QPushButton::clicked, p, &SearchForm::copyFromEntry);
209 
210         progressBar = new QProgressBar(p);
211         layout->addWidget(progressBar, 1, 1, 1, 1);
212         progressBar->setMaximum(1000);
213         progressBar->hide();
214 
215         searchButton = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Search"), p);
216         layout->addWidget(searchButton, 1, 2, 1, 1);
217         connect(generalQueryTermsForm, &OnlineSearchQueryFormGeneral::returnPressed, searchButton, &QPushButton::click);
218 
219         updateGUI();
220     }
221 
222     void loadEngines() {
223         enginesList->clear();
224 
225         addEngine(new OnlineSearchAcmPortal(p));
226         addEngine(new OnlineSearchArXiv(p));
227         addEngine(new OnlineSearchBioRxiv(p));
228         addEngine(new OnlineSearchBibsonomy(p));
229         addEngine(new OnlineSearchGoogleScholar(p));
230         addEngine(new OnlineSearchIEEEXplore(p));
231         addEngine(new OnlineSearchIngentaConnect(p));
232         addEngine(new OnlineSearchJStor(p));
233         addEngine(new OnlineSearchMathSciNet(p));
234         addEngine(new OnlineSearchMRLookup(p));
235         addEngine(new OnlineSearchInspireHep(p));
236         addEngine(new OnlineSearchCERNDS(p));
237         addEngine(new OnlineSearchPubMed(p));
238         addEngine(new OnlineSearchScienceDirect(p));
239         addEngine(new OnlineSearchSpringerLink(p));
240         addEngine(new OnlineSearchSOANASAADS(p));
241         /// addEngine(new OnlineSearchIsbnDB(p)); /// disabled as provider switched to a paid model on 2017-12-26
242         addEngine(new OnlineSearchIDEASRePEc(p));
243         addEngine(new OnlineSearchDOI(p));
244 
245         p->itemCheckChanged(nullptr);
246         updateGUI();
247     }
248 
249     void addEngine(OnlineSearchAbstract *engine) {
250         KConfigGroup configGroup(config, configGroupName);
251 
252         QListWidgetItem *item = new QListWidgetItem(engine->label(), enginesList);
253         item->setCheckState(configGroup.readEntry(engine->name(), false) ? Qt::Checked : Qt::Unchecked);
254         item->setIcon(engine->icon(item));
255         item->setToolTip(engine->label());
256         item->setData(HomepageRole, engine->homepage());
257         item->setData(NameRole, engine->name());
258 
259         OnlineSearchQueryFormAbstract *widget = engine->customWidget(queryTermsStack);
260         item->setData(WidgetRole, QVariant::fromValue<OnlineSearchQueryFormAbstract *>(widget));
261         if (widget != nullptr) {
262             connect(widget, &OnlineSearchQueryFormAbstract::returnPressed, searchButton, &QPushButton::click);
263             QScrollArea *scrollArea = wrapInScrollArea(widget, queryTermsStack);
264             queryTermsStack->addWidget(scrollArea);
265         }
266 
267         itemToOnlineSearch.insert(item, engine);
268         connect(engine, &OnlineSearchAbstract::foundEntry, p, &SearchForm::foundEntry);
269         connect(engine, &OnlineSearchAbstract::stoppedSearch, p, &SearchForm::stoppedSearch);
270         connect(engine, &OnlineSearchAbstract::progress, p, &SearchForm::updateProgress);
271     }
272 
273     void switchToSearch() {
274         for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = itemToOnlineSearch.constBegin(); it != itemToOnlineSearch.constEnd(); ++it)
275             disconnect(searchButton, &QPushButton::clicked, it.value(), &OnlineSearchAbstract::cancel);
276 
277         connect(searchButton, &QPushButton::clicked, p, &SearchForm::startSearch);
278         searchButton->setText(i18n("Search"));
279         searchButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start")));
280         for (int i = tabWidget->count() - 1; i >= 0; --i)
281             tabWidget->widget(i)->setEnabled(true);
282         tabWidget->unsetCursor();
283     }
284 
285     void switchToCancel() {
286         disconnect(searchButton, &QPushButton::clicked, p, &SearchForm::startSearch);
287 
288         for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = itemToOnlineSearch.constBegin(); it != itemToOnlineSearch.constEnd(); ++it)
289             connect(searchButton, &QPushButton::clicked, it.value(), &OnlineSearchAbstract::cancel);
290         searchButton->setText(i18n("Stop"));
291         searchButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-stop")));
292         for (int i = tabWidget->count() - 1; i >= 0; --i)
293             tabWidget->widget(i)->setEnabled(false);
294         tabWidget->setCursor(Qt::WaitCursor);
295     }
296 
297     void switchToEngines() {
298         tabWidget->setCurrentWidget(listContainer);
299     }
300 
301     void updateGUI() {
302         if (whichEnginesLabel == nullptr) return;
303 
304         QStringList checkedEngines;
305         QListWidgetItem *cursor = nullptr;
306         for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = itemToOnlineSearch.constBegin(); it != itemToOnlineSearch.constEnd(); ++it)
307             if (it.key()->checkState() == Qt::Checked) {
308                 checkedEngines << it.key()->text();
309                 cursor = it.key();
310             }
311 
312         switch (checkedEngines.size()) {
313         case 0: whichEnginesLabel->setText(i18n("No search engine selected (<a href=\"changeEngine\">change</a>).")); break;
314         case 1: whichEnginesLabel->setText(i18n("Search engine <b>%1</b> is selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first())); break;
315         case 2: whichEnginesLabel->setText(i18n("Search engines <b>%1</b> and <b>%2</b> are selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first(), checkedEngines.at(1))); break;
316         case 3: whichEnginesLabel->setText(i18n("Search engines <b>%1</b>, <b>%2</b>, and <b>%3</b> are selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first(), checkedEngines.at(1), checkedEngines.at(2))); break;
317         default: whichEnginesLabel->setText(i18n("Search engines <b>%1</b>, <b>%2</b>, and more are selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first(), checkedEngines.at(1))); break;
318         }
319 
320         OnlineSearchQueryFormAbstract *currentQueryWidget = nullptr;
321         if (cursor != nullptr && checkedEngines.size() == 1)
322             currentQueryWidget = cursor->data(WidgetRole).value<OnlineSearchQueryFormAbstract *>();
323         if (currentQueryWidget == nullptr)
324             currentQueryWidget = generalQueryTermsForm;
325         QScrollArea *area = formToScrollArea.value(currentQueryWidget, nullptr);
326         if (area != nullptr)
327             queryTermsStack->setCurrentWidget(area);
328 
329         if (useEntryButton != nullptr)
330             useEntryButton->setEnabled(!currentEntry.isNull() && tabWidget->currentIndex() == 0);
331     }
332 
333     void openHomepage() {
334         QListWidgetItem *item = enginesList->currentItem();
335         if (item != nullptr) {
336             QUrl url = item->data(HomepageRole).toUrl();
337             /// Guess mime type for url to open
338             QMimeType mimeType = FileInfo::mimeTypeForUrl(url);
339             const QString mimeTypeName = mimeType.name();
340             /// Ask KDE subsystem to open url in viewer matching mime type
341 #if KIO_VERSION < 0x051f00 // < 5.31.0
342             KRun::runUrl(url, mimeTypeName, p, false, false);
343 #else // KIO_VERSION < 0x051f00 // >= 5.31.0
344             KRun::runUrl(url, mimeTypeName, p, KRun::RunFlags());
345 #endif // KIO_VERSION < 0x051f00
346         }
347     }
348 
349     void enginesListCurrentChanged(QListWidgetItem *current) {
350         actionOpenHomepage->setEnabled(current != nullptr);
351     }
352 };
353 
354 SearchForm::SearchForm(SearchResults *searchResults, QWidget *parent)
355         : QWidget(parent), d(new SearchFormPrivate(searchResults, this))
356 {
357     d->loadEngines();
358     d->switchToSearch();
359 }
360 
361 SearchForm::~SearchForm()
362 {
363     delete d;
364 }
365 
366 void SearchForm::updatedConfiguration()
367 {
368     d->loadEngines();
369 }
370 
371 void SearchForm::setElement(QSharedPointer<Element> element, const File *)
372 {
373     d->currentEntry = element.dynamicCast<const Entry>();
374     d->useEntryButton->setEnabled(!d->currentEntry.isNull() && d->tabWidget->currentIndex() == 0);
375 }
376 
377 void SearchForm::switchToEngines()
378 {
379     d->switchToEngines();
380 }
381 
382 void SearchForm::startSearch()
383 {
384     OnlineSearchQueryFormAbstract *currentForm = d->currentQueryForm();
385     if (!currentForm->readyToStart()) {
386         KMessageBox::sorry(this, i18n("Could not start searching the Internet:\nThe search terms are not complete or invalid."), i18n("Searching the Internet"));
387         return;
388     }
389 
390     d->runningSearches.clear();
391     d->sr->clear();
392     d->progressBar->setValue(0);
393     d->progressMap.clear();
394     d->useEntryButton->hide();
395     d->progressBar->show();
396 
397     if (currentForm == d->generalQueryTermsForm) {
398         /// start search using the general-purpose form's values
399 
400         QMap<QString, QString> queryTerms = d->generalQueryTermsForm->getQueryTerms();
401         int numResults = d->generalQueryTermsForm->getNumResults();
402         for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = d->itemToOnlineSearch.constBegin(); it != d->itemToOnlineSearch.constEnd(); ++it)
403             if (it.key()->checkState() == Qt::Checked) {
404                 it.value()->startSearch(queryTerms, numResults);
405                 d->runningSearches.insert(it.value());
406             }
407         if (d->runningSearches.isEmpty()) {
408             /// if no search engine has been checked (selected), something went wrong
409             return;
410         }
411     } else {
412         /// use the single selected search engine's specific form
413 
414         for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = d->itemToOnlineSearch.constBegin(); it != d->itemToOnlineSearch.constEnd(); ++it)
415             if (it.key()->checkState() == Qt::Checked) {
416                 it.value()->startSearchFromForm();
417                 d->runningSearches.insert(it.value());
418             }
419         if (d->runningSearches.isEmpty()) {
420             /// if no search engine has been checked (selected), something went wrong
421             return;
422         }
423     }
424 
425     d->switchToCancel();
426 }
427 
428 void SearchForm::foundEntry(QSharedPointer<Entry> entry)
429 {
430     d->sr->insertElement(entry);
431 }
432 
433 void SearchForm::stoppedSearch(int)
434 {
435     OnlineSearchAbstract *engine = static_cast<OnlineSearchAbstract *>(sender());
436     if (d->runningSearches.remove(engine)) {
437         if (d->runningSearches.isEmpty()) {
438             /// last search engine stopped
439             d->switchToSearch();
440             emit doneSearching();
441 
442             QTimer::singleShot(1000, d->progressBar, &QProgressBar::hide);
443             QTimer::singleShot(1100, d->useEntryButton, &QPushButton::show);
444         } else {
445             QStringList remainingEngines;
446             remainingEngines.reserve(d->runningSearches.size());
447             for (OnlineSearchAbstract *running : const_cast<const QSet<OnlineSearchAbstract *> &>(d->runningSearches)) {
448                 remainingEngines.append(running->label());
449             }
450             if (!remainingEngines.isEmpty())
451                 qCDebug(LOG_KBIBTEX_PROGRAM) << "Remaining running engines:" << remainingEngines.join(QStringLiteral(", "));
452         }
453     }
454 }
455 
456 void SearchForm::tabSwitched(int newTab)
457 {
458     Q_UNUSED(newTab);
459     d->updateGUI();
460 }
461 
462 void SearchForm::itemCheckChanged(QListWidgetItem *item)
463 {
464     int numCheckedEngines = 0;
465     for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = d->itemToOnlineSearch.constBegin(); it != d->itemToOnlineSearch.constEnd(); ++it)
466         if (it.key()->checkState() == Qt::Checked)
467             ++numCheckedEngines;
468 
469     d->searchButton->setEnabled(numCheckedEngines > 0);
470 
471     if (item != nullptr) {
472         KConfigGroup configGroup(d->config, d->configGroupName);
473         QString name = item->data(SearchForm::SearchFormPrivate::NameRole).toString();
474         configGroup.writeEntry(name, item->checkState() == Qt::Checked);
475         d->config->sync();
476     }
477 }
478 
479 void SearchForm::openHomepage()
480 {
481     d->openHomepage();
482 }
483 
484 void SearchForm::enginesListCurrentChanged(QListWidgetItem *current, QListWidgetItem *)
485 {
486     d->enginesListCurrentChanged(current);
487 }
488 
489 void SearchForm::copyFromEntry()
490 {
491     Q_ASSERT_X(!d->currentEntry.isNull(), "SearchForm::copyFromEntry", "d->currentEntry is NULL");
492 
493     d->currentQueryForm()->copyFromEntry(*(d->currentEntry));
494 }
495 
496 void SearchForm::updateProgress(int cur, int total)
497 {
498     OnlineSearchAbstract *ws = static_cast<OnlineSearchAbstract *>(sender());
499     d->progressMap[ws] = total > 0 ? cur * 1000 / total : 0;
500 
501     int progress = 0, count = 0;
502     for (QMap<OnlineSearchAbstract *, int>::ConstIterator it = d->progressMap.constBegin(); it != d->progressMap.constEnd(); ++it, ++count)
503         progress += it.value();
504 
505     d->progressBar->setValue(count >= 1 ? progress / count : 0);
506 }
507