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