diff --git a/src/bibtexkeydialog.cpp b/src/bibtexkeydialog.cpp index 91da0838..2c61a7fd 100644 --- a/src/bibtexkeydialog.cpp +++ b/src/bibtexkeydialog.cpp @@ -1,133 +1,133 @@ /*************************************************************************** Copyright (C) 2011 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "bibtexkeydialog.h" #include "collections/bibtexcollection.h" #include "entry.h" #include "tellico_debug.h" #include #include #include #include #include #include #include #include #include using Tellico::BibtexKeyDialog; // default button is going to be used as a print button, so it's separated BibtexKeyDialog::BibtexKeyDialog(Data::CollPtr coll_, QWidget* parent_) : QDialog(parent_), m_coll(coll_) { Q_ASSERT(m_coll); setModal(false); setWindowTitle(i18n("Citation Key Manager")); QVBoxLayout* topLayout = new QVBoxLayout; setLayout(topLayout); QWidget* mainWidget = new QWidget(this); topLayout->addWidget(mainWidget); m_dupeLabel = new KTitleWidget(this); m_dupeLabel->setText(m_coll->title(), KTitleWidget::PlainMessage); m_dupeLabel->setComment(i18n("Checking for entries with duplicate citation keys...")); m_dupeLabel->setPixmap(QIcon::fromTheme(QStringLiteral("tools-wizard")).pixmap(64, 64), KTitleWidget::ImageLeft); topLayout->addWidget(m_dupeLabel); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); buttonBox->button(QDialogButtonBox::Close)->setDefault(true); - connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QPushButton* checkDuplicates = new QPushButton(buttonBox); KGuiItem::assign(checkDuplicates, KGuiItem(i18n("Check for duplicates"), QStringLiteral("system-search"))); buttonBox->addButton(checkDuplicates, QDialogButtonBox::ActionRole); m_filterButton = new QPushButton(buttonBox); KGuiItem::assign(m_filterButton, KGuiItem(i18n("Filter for duplicates"), QStringLiteral("view-filter"))); buttonBox->addButton(m_filterButton, QDialogButtonBox::ActionRole); topLayout->addWidget(buttonBox); if(m_coll->type() != Data::Collection::Bibtex) { // if it's not a bibliography, no need to save a pointer m_coll = Data::CollPtr(); myWarning() << "not a bibliography"; } else { // the button is enabled when duplicates are found m_filterButton->setEnabled(false); - connect(m_filterButton, SIGNAL(clicked()), SLOT(slotFilterDuplicates())); - connect(checkDuplicates, SIGNAL(clicked()), SLOT(slotCheckDuplicates())); - QTimer::singleShot(0, this, SLOT(slotCheckDuplicatesImpl())); + connect(m_filterButton, &QAbstractButton::clicked, this, &BibtexKeyDialog::slotFilterDuplicates); + connect(checkDuplicates, &QAbstractButton::clicked, this, &BibtexKeyDialog::slotCheckDuplicates); + QTimer::singleShot(0, this, &BibtexKeyDialog::slotCheckDuplicatesImpl); } } BibtexKeyDialog::~BibtexKeyDialog() { } void BibtexKeyDialog::slotCheckDuplicates() { if(!m_coll) { return; } m_dupeLabel->setComment(i18n("Checking for entries with duplicate citation keys...")); - QTimer::singleShot(0, this, SLOT(slotCheckDuplicatesImpl())); + QTimer::singleShot(0, this, &BibtexKeyDialog::slotCheckDuplicatesImpl); } void BibtexKeyDialog::slotCheckDuplicatesImpl() { const Data::BibtexCollection* c = static_cast(m_coll.data()); m_dupes = c->duplicateBibtexKeys(); m_filterButton->setEnabled(false); if(m_dupes.isEmpty()) { m_dupeLabel->setComment(i18n("There are no duplicate citation keys.")); m_filterButton->setEnabled(false); } else { m_dupeLabel->setComment(i18np("There is %1 duplicate citation key.", "There are %1 duplicate citation keys.", m_dupes.count())); m_filterButton->setEnabled(true); } } void BibtexKeyDialog::slotFilterDuplicates() { if(!m_coll || m_dupes.isEmpty()) { return; } FilterPtr filter(new Filter(Filter::MatchAny)); QSet keys; foreach(Data::EntryPtr entry, m_dupes) { const QString key = entry->field(QStringLiteral("bibtex-key")); if(!keys.contains(key)) { filter->append(new FilterRule(QStringLiteral("bibtex-key"), key, FilterRule::FuncEquals)); keys << key; } } if(!filter->isEmpty()) { emit signalUpdateFilter(filter); } } diff --git a/src/borrowerdialog.cpp b/src/borrowerdialog.cpp index f76e1238..c1edd55f 100644 --- a/src/borrowerdialog.cpp +++ b/src/borrowerdialog.cpp @@ -1,204 +1,204 @@ /*************************************************************************** Copyright (C) 2005-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "borrowerdialog.h" #include "document.h" #include "collection.h" #include "../tellico_debug.h" #include #include #include #ifdef HAVE_KABC #ifdef QT_STRICT_ITERATORS #define WAS_STRICT #undef QT_STRICT_ITERATORS #endif #include #include #ifdef WAS_STRICT #define QT_STRICT_ITERATORS #undef WAS_STRICT #endif #endif #include #include #include using Tellico::BorrowerDialog; #ifdef HAVE_KABC BorrowerDialog::Item::Item(QTreeWidget* parent_, const KContacts::Addressee& add_) : QTreeWidgetItem(parent_), m_uid(add_.uid()) { setData(0, Qt::DisplayRole, add_.realName().trimmed()); setData(0, Qt::DecorationRole, QIcon::fromTheme(QLatin1String("kaddressbook"))); } #endif BorrowerDialog::Item::Item(QTreeWidget* parent_, const Tellico::Data::Borrower& bor_) : QTreeWidgetItem(parent_), m_uid(bor_.uid()) { setData(0, Qt::DisplayRole, bor_.name()); setData(0, Qt::DecorationRole, QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QLatin1String(":/icons/tellico")))); } // default button is going to be used as a print button, so it's separated BorrowerDialog::BorrowerDialog(QWidget* parent_) : QDialog(parent_) { setModal(true); setWindowTitle(i18n("Select Borrower")); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QVBoxLayout *mainLayout = new QVBoxLayout(this); setLayout(mainLayout); QWidget* mainWidget = new QWidget(this); mainLayout->addWidget(mainWidget); QPushButton* okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); m_treeWidget = new QTreeWidget(mainWidget); mainLayout->addWidget(m_treeWidget); m_treeWidget->setHeaderLabel(i18n("Name")); m_treeWidget->setRootIsDecorated(false); - connect(m_treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), - SLOT(accept())); - connect(m_treeWidget, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), - SLOT(updateEdit(QTreeWidgetItem*))); + connect(m_treeWidget, &QTreeWidget::itemDoubleClicked, + this, &QDialog::accept); + connect(m_treeWidget, &QTreeWidget::currentItemChanged, + this, &BorrowerDialog::updateEdit); m_lineEdit = new KLineEdit(mainWidget); //krazy:exclude=qclasses mainLayout->addWidget(m_lineEdit); - connect(m_lineEdit->completionObject(), SIGNAL(match(const QString&)), - SLOT(selectItem(const QString&))); + connect(m_lineEdit->completionObject(), &KCompletion::match, + this, &BorrowerDialog::selectItem); m_lineEdit->setFocus(); m_lineEdit->completionObject()->setIgnoreCase(true); mainLayout->addWidget(buttonBox); #ifdef HAVE_KABC // Search for all existing contacts Akonadi::ContactSearchJob* job = new Akonadi::ContactSearchJob(); connect(job, SIGNAL(result(KJob*)), this, SLOT(akonadiSearchResult(KJob*))); #endif populateBorrowerList(); setMinimumWidth(400); } void BorrowerDialog::akonadiSearchResult(KJob* job_) { if(job_->error() != 0) { myDebug() << job_->errorString(); return; } #ifdef HAVE_KABC Akonadi::ContactSearchJob* searchJob = qobject_cast(job_); Q_ASSERT(searchJob); populateBorrowerList(); foreach(const KContacts::Addressee& addressee, searchJob->contacts()) { // skip people with no name const QString name = addressee.realName().trimmed(); if(name.isEmpty()) { continue; } if(m_itemHash.contains(name)) { continue; // if an item already exists with this name } Item* item = new Item(m_treeWidget, addressee); m_itemHash.insert(name, item); m_lineEdit->completionObject()->addItem(name); } #endif m_treeWidget->sortItems(0, Qt::AscendingOrder); } void BorrowerDialog::populateBorrowerList() { m_treeWidget->clear(); m_itemHash.clear(); m_lineEdit->completionObject()->clear(); // Bug 307958 - KContacts::Addressee uids are not constant // so populate the borrower list with the existing borrowers // before adding ones from address book Data::BorrowerList borrowers = Data::Document::self()->collection()->borrowers(); foreach(Data::BorrowerPtr bor, borrowers) { if(bor->name().isEmpty()) { continue; } if(m_itemHash.contains(bor->name())) { continue; // if an item already exists with this name } Item* item = new Item(m_treeWidget, *bor); m_itemHash.insert(bor->name(), item); m_lineEdit->completionObject()->addItem(bor->name()); } m_treeWidget->sortItems(0, Qt::AscendingOrder); } void BorrowerDialog::selectItem(const QString& str_) { if(str_.isEmpty()) { return; } QTreeWidgetItem* item = m_itemHash.value(str_); if(item) { m_treeWidget->blockSignals(true); m_treeWidget->setCurrentItem(item); m_treeWidget->scrollToItem(item); m_treeWidget->blockSignals(false); } } void BorrowerDialog::updateEdit(QTreeWidgetItem* item_) { QString s = item_->data(0, Qt::DisplayRole).toString(); m_lineEdit->setText(s); m_lineEdit->setSelection(0, s.length()); m_uid = static_cast(item_)->uid(); } Tellico::Data::BorrowerPtr BorrowerDialog::borrower() { return Data::BorrowerPtr(new Data::Borrower(m_lineEdit->text(), m_uid)); } // static Tellico::Data::BorrowerPtr BorrowerDialog::getBorrower(QWidget* parent_) { BorrowerDialog dlg(parent_); if(dlg.exec() == QDialog::Accepted) { return dlg.borrower(); } return Data::BorrowerPtr(); } diff --git a/src/collectionfieldsdialog.cpp b/src/collectionfieldsdialog.cpp index 293e425c..d1f387ca 100644 --- a/src/collectionfieldsdialog.cpp +++ b/src/collectionfieldsdialog.cpp @@ -1,1091 +1,1094 @@ /*************************************************************************** Copyright (C) 2003-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "collectionfieldsdialog.h" #include "collection.h" #include "field.h" #include "fieldformat.h" #include "collectionfactory.h" #include "gui/listwidgetitem.h" #include "gui/stringmapdialog.h" #include "gui/combobox.h" #include "tellico_kernel.h" #include "translators/tellico_xml.h" #include "utils/string_utils.h" #include "tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Tellico; using Tellico::FieldListItem; using Tellico::CollectionFieldsDialog; class Tellico::FieldListItem : public Tellico::GUI::ListWidgetItem { public: FieldListItem(QListWidget* parent_, Data::FieldPtr field_) : GUI::ListWidgetItem(field_->title(), parent_), m_field(field_) {} Data::FieldPtr field() const { return m_field; } void setField(Data::FieldPtr field) { m_field = field; } private: Data::FieldPtr m_field; }; CollectionFieldsDialog::CollectionFieldsDialog(Tellico::Data::CollPtr coll_, QWidget* parent_) : QDialog(parent_), m_coll(coll_), m_defaultCollection(nullptr), m_currentField(nullptr), m_modified(false), m_updatingValues(false), m_reordered(false), m_oldIndex(-1), m_notifyMode(NotifyKernel) { setModal(false); setWindowTitle(i18n("Collection Fields")); QVBoxLayout* mainLayout = new QVBoxLayout(); setLayout(mainLayout); QWidget* page = new QWidget(this); mainLayout->addWidget(page); QBoxLayout* topLayout = new QHBoxLayout(page); page->setLayout(topLayout); QGroupBox* fieldsGroup = new QGroupBox(i18n("Current Fields"), page); QBoxLayout* fieldsLayout = new QVBoxLayout(fieldsGroup); topLayout->addWidget(fieldsGroup, 1); m_fieldsWidget = new QListWidget(fieldsGroup); m_fieldsWidget->setMinimumWidth(150); fieldsLayout->addWidget(m_fieldsWidget); Data::FieldList fields = m_coll->fields(); foreach(Data::FieldPtr field, fields) { // ignore fields which are not user-editable if(!field->hasFlag(Data::Field::NoEdit)) { (void) new FieldListItem(m_fieldsWidget, field); } } - connect(m_fieldsWidget, SIGNAL(currentRowChanged(int)), SLOT(slotHighlightedChanged(int))); + connect(m_fieldsWidget, &QListWidget::currentRowChanged, this, &CollectionFieldsDialog::slotHighlightedChanged); QWidget* hb1 = new QWidget(fieldsGroup); QHBoxLayout* hb1HBoxLayout = new QHBoxLayout(hb1); hb1HBoxLayout->setMargin(0); fieldsLayout->addWidget(hb1); m_btnNew = new QPushButton(i18nc("New Field", "&New"), hb1); hb1HBoxLayout->addWidget(m_btnNew); m_btnNew->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); m_btnNew->setWhatsThis(i18n("Add a new field to the collection")); m_btnDelete = new QPushButton(i18nc("Delete Field", "Delete"), hb1); hb1HBoxLayout->addWidget(m_btnDelete); m_btnDelete->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); m_btnDelete->setWhatsThis(i18n("Remove a field from the collection")); - connect(m_btnNew, SIGNAL(clicked()), SLOT(slotNew())); - connect(m_btnDelete, SIGNAL(clicked()), SLOT(slotDelete())); + connect(m_btnNew, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotNew); + connect(m_btnDelete, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotDelete); QWidget* hb2 = new QWidget(fieldsGroup); QHBoxLayout* hb2HBoxLayout = new QHBoxLayout(hb2); hb2HBoxLayout->setMargin(0); fieldsLayout->addWidget(hb2); m_btnUp = new QPushButton(hb2); hb2HBoxLayout->addWidget(m_btnUp); m_btnUp->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); m_btnUp->setWhatsThis(i18n("Move this field up in the list. The list order is important " "for the layout of the entry editor.")); m_btnDown = new QPushButton(hb2); hb2HBoxLayout->addWidget(m_btnDown); m_btnDown->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); m_btnDown->setWhatsThis(i18n("Move this field down in the list. The list order is important " "for the layout of the entry editor.")); - connect(m_btnUp, SIGNAL(clicked()), SLOT(slotMoveUp())); - connect(m_btnDown, SIGNAL(clicked()), SLOT(slotMoveDown())); + connect(m_btnUp, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotMoveUp); + connect(m_btnDown, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotMoveDown); QWidget* vbox = new QWidget(page); QVBoxLayout* vboxVBoxLayout = new QVBoxLayout(vbox); vboxVBoxLayout->setMargin(0); topLayout->addWidget(vbox, 2); QGroupBox* propGroup = new QGroupBox(i18n("Field Properties"), vbox); vboxVBoxLayout->addWidget(propGroup); QBoxLayout* propLayout = new QVBoxLayout(propGroup); QWidget* grid = new QWidget(propGroup); QGridLayout* layout = new QGridLayout(grid); propLayout->addWidget(grid); int row = -1; QLabel* label = new QLabel(i18n("&Title:"), grid); layout->addWidget(label, ++row, 0); m_titleEdit = new QLineEdit(grid); layout->addWidget(m_titleEdit, row, 1); label->setBuddy(m_titleEdit); QString whats = i18n("The title of the field"); label->setWhatsThis(whats); m_titleEdit->setWhatsThis(whats); - connect(m_titleEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_titleEdit, &QLineEdit::textChanged, this, &CollectionFieldsDialog::slotModified); label = new QLabel(i18n("T&ype:"), grid); layout->addWidget(label, row, 2); m_typeCombo = new KComboBox(grid); layout->addWidget(m_typeCombo, row, 3); label->setBuddy(m_typeCombo); whats = QStringLiteral(""); whats += i18n("The type of the field determines what values may be used. "); whats += i18n("Simple Text is used for most fields. "); whats += i18n("Paragraph is for large text blocks. "); whats += i18n("Choice limits the field to certain values. "); whats += i18n("Checkbox is for a simple yes/no value. "); whats += i18n("Number indicates that the field contains a numerical value. "); whats += i18n("URL is for fields which refer to URLs, including references to other files. "); whats += i18n("A Table may hold one or more columns of values. "); whats += i18n("An Image field holds a picture. "); whats += i18n("A Date field can be used for values with a day, month, and year. "); whats += i18n("A Rating field uses stars to show a rating number. "); whats += QLatin1String(""); label->setWhatsThis(whats); m_typeCombo->setWhatsThis(whats); // the typeTitles match the fieldMap().values() but in a better order m_typeCombo->addItems(Data::Field::typeTitles()); - connect(m_typeCombo, SIGNAL(activated(int)), SLOT(slotModified())); - connect(m_typeCombo, SIGNAL(activated(const QString&)), SLOT(slotTypeChanged(const QString&))); + void (QComboBox::* activatedInt)(int) = &QComboBox::activated; + connect(m_typeCombo, activatedInt, this, &CollectionFieldsDialog::slotModified); + void (QComboBox::* activatedString)(const QString&) = &QComboBox::activated; + connect(m_typeCombo, activatedString, this, &CollectionFieldsDialog::slotTypeChanged); label = new QLabel(i18n("Cate&gory:"), grid); layout->addWidget(label, ++row, 0); m_catCombo = new KComboBox(true, grid); layout->addWidget(m_catCombo, row, 1); label->setBuddy(m_catCombo); whats = i18n("The field category determines where the field is placed in the editor."); label->setWhatsThis(whats); m_catCombo->setWhatsThis(whats); // I don't want to include the categories for singleCategory fields QStringList cats; const QStringList allCats = m_coll->fieldCategories(); foreach(const QString& cat, allCats) { Data::FieldList fields = m_coll->fieldsByCategory(cat); if(!fields.isEmpty() && !fields.at(0)->isSingleCategory()) { cats.append(cat); } } m_catCombo->addItems(cats); m_catCombo->setDuplicatesEnabled(false); - connect(m_catCombo, SIGNAL(currentTextChanged(const QString&)), SLOT(slotModified())); + connect(m_catCombo, &QComboBox::currentTextChanged, this, &CollectionFieldsDialog::slotModified); m_btnExtended = new QPushButton(i18n("Set &properties..."), grid); m_btnExtended->setIcon(QIcon::fromTheme(QStringLiteral("bookmarks"))); layout->addWidget(m_btnExtended, row, 2, 1, 2); label->setBuddy(m_btnExtended); whats = i18n("Extended field properties are used to specify things such as the corresponding bibtex field."); label->setWhatsThis(whats); m_btnExtended->setWhatsThis(whats); - connect(m_btnExtended, SIGNAL(clicked()), SLOT(slotShowExtendedProperties())); + connect(m_btnExtended, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotShowExtendedProperties); label = new QLabel(i18n("Description:"), grid); layout->addWidget(label, ++row, 0); m_descEdit = new QLineEdit(grid); m_descEdit->setMinimumWidth(150); layout->addWidget(m_descEdit, row, 1, 1, 3); label->setBuddy(m_descEdit); whats = i18n("The description is a useful reminder of what information is contained in the field."); label->setWhatsThis(whats); m_descEdit->setWhatsThis(whats); - connect(m_descEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_descEdit, &QLineEdit::textChanged, this, &CollectionFieldsDialog::slotModified); QGroupBox* valueGroup = new QGroupBox(i18n("Value Options"), vbox); vboxVBoxLayout->addWidget(valueGroup); QGridLayout* valueLayout = new QGridLayout(valueGroup); int valueRow = -1; label = new QLabel(i18n("Default value:"), valueGroup); valueLayout->addWidget(label, ++valueRow, 0); m_defaultEdit = new QLineEdit(valueGroup); valueLayout->addWidget(m_defaultEdit, valueRow, 1, 1, 3); label->setBuddy(m_defaultEdit); whats = i18n("A default value can be set for new entries."); label->setWhatsThis(whats); m_defaultEdit->setWhatsThis(whats); - connect(m_defaultEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_defaultEdit, &QLineEdit::textChanged, this, &CollectionFieldsDialog::slotModified); label = new QLabel(i18n("Value template:"), valueGroup); valueLayout->addWidget(label, ++valueRow, 0); m_derivedEdit = new QLineEdit(valueGroup); m_derivedEdit->setMinimumWidth(150); valueLayout->addWidget(m_derivedEdit, valueRow, 1); label->setBuddy(m_derivedEdit); /* TRANSLATORS: Do not translate %{year} and %{title}. */ whats = i18n("Derived values are formed from the values of other fields according to the value template. " "Named fields, such as \"%{year} %{title}\", get substituted in the value."); label->setWhatsThis(whats); m_derivedEdit->setWhatsThis(whats); - connect(m_derivedEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_derivedEdit, &QLineEdit::textChanged, this, &CollectionFieldsDialog::slotModified); m_derived = new QCheckBox(i18n("Use derived value"), valueGroup); m_derived->setWhatsThis(whats); valueLayout->addWidget(m_derived, valueRow, 2, 1, 2); - connect(m_derived, SIGNAL(clicked(bool)), SLOT(slotDerivedChecked(bool))); - connect(m_derived, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_derived, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotDerivedChecked); + connect(m_derived, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotModified); label = new QLabel(i18n("A&llowed values:"), valueGroup); valueLayout->addWidget(label, ++valueRow, 0); m_allowEdit = new QLineEdit(valueGroup); valueLayout->addWidget(m_allowEdit, valueRow, 1, 1, 3); label->setBuddy(m_allowEdit); whats = i18n("For Choice-type fields, these are the only values allowed. They are " "placed in a combo box. The possible values have to be separated by a semi-colon, " "for example: \"dog; cat; mouse\""); label->setWhatsThis(whats); m_allowEdit->setWhatsThis(whats); - connect(m_allowEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_allowEdit, &QLineEdit::textChanged, this, &CollectionFieldsDialog::slotModified); label = new QLabel(i18n("Format options:"), valueGroup); valueLayout->addWidget(label, ++valueRow, 0); m_formatCombo = new GUI::ComboBox(valueGroup); valueLayout->addWidget(m_formatCombo, valueRow, 1, 1, 3); label->setBuddy(m_formatCombo); m_formatCombo->addItem(i18n("No formatting"), FieldFormat::FormatNone); m_formatCombo->addItem(i18n("Allow auto-capitalization only"), FieldFormat::FormatPlain); m_formatCombo->addItem(i18n("Format as a title"), FieldFormat::FormatTitle); m_formatCombo->addItem(i18n("Format as a name"), FieldFormat::FormatName); - connect(m_formatCombo, SIGNAL(currentIndexChanged(int)), SLOT(slotModified())); + void (QComboBox::* currentIndexChanged)(int) = &QComboBox::currentIndexChanged; + connect(m_formatCombo, currentIndexChanged, this, &CollectionFieldsDialog::slotModified); QGroupBox* optionsGroup = new QGroupBox(i18n("Field Options"), vbox); vboxVBoxLayout->addWidget(optionsGroup); QBoxLayout* optionsLayout = new QVBoxLayout(optionsGroup); m_complete = new QCheckBox(i18n("Enable auto-completion"), optionsGroup); m_complete->setWhatsThis(i18n("If checked, KDE auto-completion will be enabled in the " "text edit box for this field.")); m_multiple = new QCheckBox(i18n("Allow multiple values"), optionsGroup); m_multiple->setWhatsThis(i18n("If checked, Tellico will parse the values in the field " "for multiple values, separated by a semi-colon.")); m_grouped = new QCheckBox(i18n("Allow grouping"), optionsGroup); m_grouped->setWhatsThis(i18n("If checked, this field may be used to group the entries in " "the group view.")); optionsLayout->addWidget(m_complete); optionsLayout->addWidget(m_multiple); optionsLayout->addWidget(m_grouped); - connect(m_complete, SIGNAL(clicked()), SLOT(slotModified())); - connect(m_multiple, SIGNAL(clicked()), SLOT(slotModified())); - connect(m_grouped, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_complete, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotModified); + connect(m_multiple, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotModified); + connect(m_grouped, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotModified); // need to stretch at bottom vboxVBoxLayout->addStretch(1); // keep a default collection m_defaultCollection = CollectionFactory::collection(m_coll->type(), true); m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok| QDialogButtonBox::Cancel| QDialogButtonBox::Help| QDialogButtonBox::RestoreDefaults| QDialogButtonBox::Apply); mainLayout->addWidget(m_buttonBox); QPushButton* okButton = m_buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject())); - connect(m_buttonBox, SIGNAL(helpRequested()), this, SLOT(slotHelp())); + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(m_buttonBox, &QDialogButtonBox::helpRequested, this, &CollectionFieldsDialog::slotHelp); m_buttonBox->button(QDialogButtonBox::RestoreDefaults)->setWhatsThis(i18n("Revert the selected field's properties to the default values.")); - connect(okButton, SIGNAL(clicked()), SLOT(slotOk())); - connect(m_buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(slotApply())); - connect(m_buttonBox->button(QDialogButtonBox::RestoreDefaults), SIGNAL(clicked()), SLOT(slotDefault())); + connect(okButton, &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotOk); + connect(m_buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotApply); + connect(m_buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &CollectionFieldsDialog::slotDefault); okButton->setEnabled(false); m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); m_buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false); // initially the m_typeCombo is populated with all types, but as soon as something is // selected in the fields box, the combo box is cleared and filled with the allowable // new types. The problem is that when more types are added, the size of the combo box // doesn't change. So when everything is laid out, the combo box needs to have all the // items there. - QTimer::singleShot(0, this, SLOT(slotSelectInitial())); + QTimer::singleShot(0, this, &CollectionFieldsDialog::slotSelectInitial); } CollectionFieldsDialog::~CollectionFieldsDialog() { } void CollectionFieldsDialog::setNotifyKernel(bool notify_) { if(notify_) { m_notifyMode = NotifyKernel; } else { m_notifyMode = NoNotification; } } void CollectionFieldsDialog::slotSelectInitial() { // the accel management is here so that it doesn't cause conflicts with the // ones explicitly set in the constructor KAcceleratorManager::manage(this); m_fieldsWidget->setCurrentRow(0); } void CollectionFieldsDialog::slotHelp() { KHelpClient::invokeHelp(QStringLiteral("fields-dialog")); } void CollectionFieldsDialog::slotOk() { slotApply(); accept(); } void CollectionFieldsDialog::slotApply() { updateField(); if(!checkValues()) { return; } applyChanges(); } void CollectionFieldsDialog::applyChanges() { // start a command group, "Modify" is a generic term here since the commands could be add, modify, or delete if(m_notifyMode == NotifyKernel) { Kernel::self()->beginCommandGroup(i18n("Modify Fields")); } foreach(Data::FieldPtr field, m_copiedFields) { // check for Choice fields with removed values to warn user if(field->type() == Data::Field::Choice || field->type() == Data::Field::Rating) { QStringList oldValues = m_coll->fieldByName(field->name())->allowed(); QStringList newValues = field->allowed(); for(QStringList::ConstIterator vIt = oldValues.constBegin(); vIt != oldValues.constEnd(); ++vIt) { if(newValues.contains(*vIt)) { continue; } int ret = KMessageBox::warningContinueCancel(this, i18n("Removing allowed values from the %1 field which " "currently exist in the collection may cause data corruption. " "Do you want to keep your modified values or cancel and revert " "to the current ones?", field->title()), QString(), KGuiItem(i18n("Keep modified values"))); if(ret != KMessageBox::Continue) { if(field->type() == Data::Field::Choice) { field->setAllowed(oldValues); } else { // rating field Data::FieldPtr oldField = m_coll->fieldByName(field->name()); field->setProperty(QStringLiteral("minimum"), oldField->property(QStringLiteral("minimum"))); field->setProperty(QStringLiteral("maximum"), oldField->property(QStringLiteral("maximum"))); } } break; } } if(m_notifyMode == NotifyKernel) { Kernel::self()->modifyField(field); } else { m_coll->modifyField(field); } } foreach(Data::FieldPtr field, m_newFields) { if(m_notifyMode == NotifyKernel) { Kernel::self()->addField(field); } else { m_coll->addField(field); } } // set all text not to be colored, and get new list Data::FieldList fields; fields.reserve(m_fieldsWidget->count()); for(int i = 0; i < m_fieldsWidget->count(); ++i) { FieldListItem* item = static_cast(m_fieldsWidget->item(i)); item->setColored(false); if(m_reordered) { Data::FieldPtr field = item->field(); if(field) { fields.append(field); } } } // if reordering fields, need to add ReadOnly fields since they were not shown if(m_reordered) { foreach(Data::FieldPtr field, m_coll->fields()) { if(field->hasFlag(Data::Field::NoEdit)) { fields.append(field); } } } if(fields.count() > 0) { if(m_notifyMode == NotifyKernel) { Kernel::self()->reorderFields(fields); } else { m_coll->reorderFields(fields); } } // commit command group if(m_notifyMode == NotifyKernel) { Kernel::self()->endCommandGroup(); } // now clear copied fields m_copiedFields.clear(); // clear new ones, too m_newFields.clear(); m_currentField = static_cast(m_fieldsWidget->currentItem())->field(); // the field type might have changed, so need to update the type combo list with possible values if(m_currentField) { // set the updating flag since the values are changing and slots are firing // but we don't care about UI indications of changes bool wasUpdating = m_updatingValues; m_updatingValues = true; QString currType = m_typeCombo->currentText(); m_typeCombo->clear(); m_typeCombo->addItems(newTypesAllowed(m_currentField->type())); m_typeCombo->setCurrentItem(currType); // template might have been changed for dependent fields m_derivedEdit->setText(m_currentField->property(QStringLiteral("template"))); m_updatingValues = wasUpdating; } m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); } void CollectionFieldsDialog::slotNew() { // first update the current one with all the values from the edit widgets updateField(); // next check old values if(!checkValues()) { return; } QString name = QLatin1String("custom") + QString::number(m_newFields.count()+1); int count = m_newFields.count() + 1; QString title = i18n("New Field %1", count); while(!m_fieldsWidget->findItems(title, Qt::MatchExactly).isEmpty()) { ++count; title = i18n("New Field %1", count); } Data::FieldPtr field(new Data::Field(name, title)); m_newFields.append(field); // myDebug() << "adding new field " << title; m_currentField = field; FieldListItem* item = new FieldListItem(m_fieldsWidget, field); item->setColored(true); m_fieldsWidget->setCurrentItem(item); m_fieldsWidget->scrollToItem(item); slotModified(); m_titleEdit->setFocus(); m_titleEdit->selectAll(); } void CollectionFieldsDialog::slotDelete() { if(!m_currentField) { return; } if(m_newFields.contains(m_currentField)) { // remove field from vector before deleting item containing field m_newFields.removeAll(m_currentField); } else { if(m_notifyMode == NotifyKernel) { if(!Kernel::self()->removeField(m_currentField)) { return; } } else { m_coll->removeField(m_currentField); } emit signalCollectionModified(); m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); } int currentRow = m_fieldsWidget->currentRow(); delete m_fieldsWidget->takeItem(currentRow); m_fieldsWidget->setCurrentRow(qMin(currentRow, m_fieldsWidget->count()-1)); m_fieldsWidget->scrollToItem(m_fieldsWidget->currentItem()); m_currentField = static_cast(m_fieldsWidget->currentItem())->field(); // QSharedData gets auto-deleted } void CollectionFieldsDialog::slotTypeChanged(const QString& type_) { Data::Field::Type type = Data::Field::Undef; const Data::Field::FieldMap fieldMap = Data::Field::typeMap(); for(Data::Field::FieldMap::ConstIterator it = fieldMap.begin(); it != fieldMap.end(); ++it) { if(it.value() == type_) { type = it.key(); break; } } if(type == Data::Field::Undef) { myWarning() << "type name not recognized:" << type_; type = Data::Field::Line; } // only choice types gets allowed values m_allowEdit->setEnabled(type == Data::Field::Choice); // paragraphs, tables, and images are their own category bool isCategory = (type == Data::Field::Para || type == Data::Field::Table || type == Data::Field::Image); m_catCombo->setEnabled(!isCategory); // formatting is only applicable when the type is simple text or a table bool isText = (type == Data::Field::Line || type == Data::Field::Table); // formatNone is the default m_formatCombo->setEnabled(isText); // multiple is only applicable for simple text and number isText = (type == Data::Field::Line || type == Data::Field::Number); m_multiple->setEnabled(isText); // completion is only applicable for simple text, number, and URL isText = (isText || type == Data::Field::URL); m_complete->setEnabled(isText); // grouping is not possible with paragraphs or images m_grouped->setEnabled(type != Data::Field::Para && type != Data::Field::Image); } void CollectionFieldsDialog::slotHighlightedChanged(int index_) { if(index_ == m_oldIndex) { return; // state when checkValues() returns false } // use this instead of blocking signals everywhere m_updatingValues = true; // first update the current one with all the values from the edit widgets updateField(); // next check old values if(!checkValues()) { // Other functions get called and change selection after this one. Use a SingleShot to revert - QTimer::singleShot(0, this, SLOT(resetToCurrent())); + QTimer::singleShot(0, this, &CollectionFieldsDialog::resetToCurrent); m_updatingValues = false; return; } m_oldIndex = index_; m_btnUp->setEnabled(index_ > 0); m_btnDown->setEnabled(index_ < static_cast(m_fieldsWidget->count())-1); FieldListItem* item = dynamic_cast(m_fieldsWidget->item(index_)); if(!item) { return; } // need to get a pointer to the field with the new values to insert Data::FieldPtr field = item->field(); if(!field) { myDebug() << "no field found!"; return; } // type is limited to certain types, unless it's a new field m_typeCombo->clear(); if(m_newFields.contains(field)) { m_typeCombo->addItems(newTypesAllowed(Data::Field::Undef)); } else { m_typeCombo->addItems(newTypesAllowed(field->type())); } populate(field); // default button is enabled only if default collection contains the field if(m_defaultCollection) { const bool hasField = m_defaultCollection->hasField(field->name()); m_buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(hasField); } m_currentField = field; m_updatingValues = false; } void CollectionFieldsDialog::updateField() { Data::FieldPtr field = m_currentField; if(!field || !m_modified) { return; } // only update name if it's one of the new ones if(m_newFields.contains(field)) { // name needs to be a valid XML element name QString name = XML::elementName(m_titleEdit->text().toLower()); if(name.isEmpty()) { // might end up with empty string name = QLatin1String("custom") + QString::number(m_newFields.count()+1); } while(m_coll->hasField(name)) { // ensure name uniqueness name += QLatin1String("-new"); } field->setName(name); } const QString title = m_titleEdit->text().simplified(); updateTitle(title); const Data::Field::FieldMap& fieldMap = Data::Field::typeMap(); for(Data::Field::FieldMap::ConstIterator it = fieldMap.begin(); it != fieldMap.end(); ++it) { if(it.value() == m_typeCombo->currentText()) { field->setType(it.key()); break; } } if(field->type() == Data::Field::Choice) { const QRegExp rx(QLatin1String("\\s*;\\s*")); field->setAllowed(m_allowEdit->text().split(rx, QString::SkipEmptyParts)); field->setProperty(QStringLiteral("minimum"), QString()); field->setProperty(QStringLiteral("maximum"), QString()); } else if(field->type() == Data::Field::Rating) { QString v = field->property(QStringLiteral("minimum")); if(v.isEmpty()) { field->setProperty(QStringLiteral("minimum"), QString::number(1)); } v = field->property(QStringLiteral("maximum")); if(v.isEmpty()) { field->setProperty(QStringLiteral("maximum"), QString::number(5)); } } if(field->isSingleCategory()) { field->setCategory(field->title()); } else { QString category = m_catCombo->currentText().simplified(); field->setCategory(category); m_catCombo->setCurrentItem(category, true); // if it doesn't exist, it's added } if(m_derived->isChecked()) { field->setProperty(QStringLiteral("template"), m_derivedEdit->text()); } field->setDescription(m_descEdit->text()); field->setDefaultValue(m_defaultEdit->text()); if(m_formatCombo->isEnabled()) { field->setFormatType(static_cast(m_formatCombo->currentData().toInt())); } else { field->setFormatType(FieldFormat::FormatNone); } int flags = 0; if(m_derived->isChecked()) { flags |= Data::Field::Derived; } if(m_complete->isChecked()) { flags |= Data::Field::AllowCompletion; } if(m_grouped->isChecked()) { flags |= Data::Field::AllowGrouped; } if(m_multiple->isChecked()) { flags |= Data::Field::AllowMultiple; } field->setFlags(flags); m_modified = false; } // The purpose here is to first set the modified flag. Then, if the field being edited is one // that exists in the collection already, a deep copy needs to be made. void CollectionFieldsDialog::slotModified() { // if I'm just updating the values, I don't care if(m_updatingValues) { return; } m_modified = true; m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); if(!m_currentField) { myDebug() << "no current field!"; m_currentField = static_cast(m_fieldsWidget->currentItem())->field(); } // color the text static_cast(m_fieldsWidget->currentItem())->setColored(true); // check if copy exists already if(m_copiedFields.contains(m_currentField)) { return; } // or, check if is a new field, in which case no copy is needed // check if copy exists already if(m_newFields.contains(m_currentField)) { return; } m_currentField = new Data::Field(*m_currentField); m_copiedFields.append(m_currentField); static_cast(m_fieldsWidget->currentItem())->setField(m_currentField); } void CollectionFieldsDialog::updateTitle(const QString& title_) { if(m_currentField && m_currentField->title() != title_) { m_fieldsWidget->blockSignals(true); FieldListItem* oldItem = findItem(m_currentField); if(!oldItem) { return; } oldItem->setText(title_); // will always be colored since it's new oldItem->setColored(true); m_currentField->setTitle(title_); m_fieldsWidget->blockSignals(false); } } void CollectionFieldsDialog::slotDefault() { if(!m_currentField) { return; } Data::FieldPtr defaultField = m_defaultCollection->fieldByName(m_currentField->name()); if(!defaultField) { return; } QString caption = i18n("Revert Field Properties"); QString text = i18n("

Do you really want to revert the properties for the %1 " "field back to their default values?

", m_currentField->title()); QString dontAsk = QStringLiteral("RevertFieldProperties"); int ret = KMessageBox::warningContinueCancel(this, text, caption, KGuiItem(i18n("Revert")), KStandardGuiItem::cancel(), dontAsk); if(ret != KMessageBox::Continue) { return; } // now update all values with default m_updatingValues = true; populate(defaultField); m_updatingValues = false; slotModified(); } void CollectionFieldsDialog::slotMoveUp() { int idx = m_fieldsWidget->currentRow(); if(idx < 1) { return; } // takeItem ends up signalling that the current index changed // need to revert m_oldIndex after taking the item QListWidgetItem* item = m_fieldsWidget->takeItem(idx); m_oldIndex++; m_fieldsWidget->insertItem(idx-1, item); m_fieldsWidget->setCurrentItem(item); m_reordered = true; // don't call slotModified() since that creates a deep copy. m_modified = true; m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); } void CollectionFieldsDialog::slotMoveDown() { int idx = m_fieldsWidget->currentRow(); if(idx > m_fieldsWidget->count()-1) { return; } // takeItem ends up signalling that the current index changed // need to revert m_oldIndex after taking the item QListWidgetItem* item = m_fieldsWidget->takeItem(idx); m_oldIndex--; m_fieldsWidget->insertItem(idx+1, item); m_fieldsWidget->setCurrentItem(item); m_reordered = true; // don't call slotModified() since that creates a deep copy. m_modified = true; m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); m_buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); } Tellico::FieldListItem* CollectionFieldsDialog::findItem(Tellico::Data::FieldPtr field_) { for(int i = 0; i < m_fieldsWidget->count(); ++i) { FieldListItem* textItem = static_cast(m_fieldsWidget->item(i)); if(textItem->field() == field_) { return textItem; } } return nullptr; } bool CollectionFieldsDialog::slotShowExtendedProperties() { if(!m_currentField) { return false; } // the default value is included in properties, but it has a // separate edit box QString dv = m_currentField->defaultValue(); QString dt = m_currentField->property(QStringLiteral("template")); StringMap props = m_currentField->propertyList(); props.remove(QStringLiteral("default")); props.remove(QStringLiteral("template")); StringMapDialog dlg(props, this, true); dlg.setWindowTitle(i18n("Extended Field Properties")); dlg.setLabels(i18n("Property"), i18n("Value")); if(dlg.exec() == QDialog::Accepted) { props = dlg.stringMap(); if(!dv.isEmpty()) { props.insert(QStringLiteral("default"), dv); } if(!dt.isEmpty()) { props.insert(QStringLiteral("template"), dt); } m_currentField->setPropertyList(props); slotModified(); return true; } return false; } void CollectionFieldsDialog::slotDerivedChecked(bool checked_) { m_defaultEdit->setEnabled(!checked_); m_derivedEdit->setEnabled(checked_); } void CollectionFieldsDialog::resetToCurrent() { m_fieldsWidget->setCurrentRow(m_oldIndex); } bool CollectionFieldsDialog::checkValues() { if(!m_currentField) { return true; } const QString title = m_currentField->title(); // find total number of boxes with this title in case multiple new ones with same title were added int titleCount = m_fieldsWidget->findItems(title, Qt::MatchExactly).count(); if((m_coll->fieldByTitle(title) && m_coll->fieldNameByTitle(title) != m_currentField->name()) || titleCount > 1) { // already have a field with this title KMessageBox::sorry(this, i18n("A field with this title already exists. Please enter a different title.")); m_titleEdit->selectAll(); return false; } const QString category = m_currentField->category(); if(category.isEmpty()) { KMessageBox::sorry(this, i18n("The category may not be empty. Please enter a category.")); m_catCombo->lineEdit()->selectAll(); return false; } Data::FieldList fields = m_coll->fieldsByCategory(category); if(!fields.isEmpty() && fields[0]->isSingleCategory() && fields[0]->name() != m_currentField->name()) { // can't have this category, cause it conflicts with a single-category field KMessageBox::sorry(this, i18n("A field may not be in the same category as a Paragraph, " "Table or Image field. Please enter a different category.")); m_catCombo->lineEdit()->selectAll(); return false; } // the combobox is disabled for single-category fields if(!m_catCombo->isEnabled() && m_coll->fieldByTitle(title) && m_coll->fieldNameByTitle(title) != m_currentField->name()) { KMessageBox::sorry(this, i18n("A field's title may not be the same as an existing category. " "Please enter a different title.")); m_titleEdit->selectAll(); return false; } // check for rating values outside bounds if(m_currentField->type() == Data::Field::Rating) { bool ok; // ok to ignore this here int low = Tellico::toUInt(m_currentField->property(QStringLiteral("minimum")), &ok); int high = Tellico::toUInt(m_currentField->property(QStringLiteral("maximum")), &ok); while(low < 1 || low > 9 || high < 1 || high > 10 || low >= high) { KMessageBox::sorry(this, i18n("The range for a rating field must be between 1 and 10, " "and the lower bound must be less than the higher bound. " "Please enter different low and high properties.")); if(slotShowExtendedProperties()) { low = Tellico::toUInt(m_currentField->property(QStringLiteral("minimum")), &ok); high = Tellico::toUInt(m_currentField->property(QStringLiteral("maximum")), &ok); } else { return false; } } } else if(m_currentField->type() == Data::Field::Table) { bool ok; // ok to ignore this here int ncols = Tellico::toUInt(m_currentField->property(QStringLiteral("columns")), &ok); // also enforced in GUI::TableFieldWidget if(ncols > 10) { KMessageBox::sorry(this, i18n("Tables are limited to a maximum of ten columns.")); m_currentField->setProperty(QStringLiteral("columns"), QStringLiteral("10")); } } if(m_derived->isChecked() && !m_derivedEdit->text().contains(QLatin1Char('%'))) { KMessageBox::sorry(this, i18n("A field with a derived value must have a value template.")); m_derivedEdit->setFocus(); m_derivedEdit->selectAll(); return false; } return true; } void CollectionFieldsDialog::populate(Data::FieldPtr field_) { m_titleEdit->setText(field_->title()); // if the current name is not there, then this will change the list! const Data::Field::FieldMap& fieldMap = Data::Field::typeMap(); int idx = m_typeCombo->findText(fieldMap[field_->type()]); m_typeCombo->setCurrentIndex(idx); slotTypeChanged(fieldMap[field_->type()]); // just setting the text doesn't emit the activated signal if(field_->type() == Data::Field::Choice) { m_allowEdit->setText(field_->allowed().join(FieldFormat::delimiterString())); } else { m_allowEdit->clear(); } idx = m_catCombo->findText(field_->category()); if(idx > -1) { m_catCombo->setCurrentIndex(idx); // have to do this here } else { m_catCombo->lineEdit()->setText(field_->category()); } m_descEdit->setText(field_->description()); if(field_->hasFlag(Data::Field::Derived)) { m_derivedEdit->setText(field_->property(QStringLiteral("template"))); m_derived->setChecked(true); m_defaultEdit->clear(); } else { m_derivedEdit->clear(); m_derived->setChecked(false); m_defaultEdit->setText(field_->defaultValue()); } slotDerivedChecked(m_derived->isChecked()); m_formatCombo->setCurrentData(field_->formatType()); m_complete->setChecked(field_->hasFlag(Data::Field::AllowCompletion)); m_multiple->setChecked(field_->hasFlag(Data::Field::AllowMultiple)); m_grouped->setChecked(field_->hasFlag(Data::Field::AllowGrouped)); m_btnDelete->setEnabled(!field_->hasFlag(Data::Field::NoDelete)); } // only certain type changes are allowed QStringList CollectionFieldsDialog::newTypesAllowed(int type_ /*=0*/) { // Undef means return all if(type_ == Data::Field::Undef) { return Data::Field::typeTitles(); } const Data::Field::FieldMap& fieldMap = Data::Field::typeMap(); QStringList newTypes; switch(type_) { case Data::Field::Line: // might not work if converted to a number or URL, but ok case Data::Field::Number: case Data::Field::URL: newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Para]; newTypes += fieldMap[Data::Field::Number]; newTypes += fieldMap[Data::Field::URL]; newTypes += fieldMap[Data::Field::Table]; break; case Data::Field::Date: newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Date]; break; case Data::Field::Bool: // doesn't really make sense, but can't hurt newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Para]; newTypes += fieldMap[Data::Field::Bool]; newTypes += fieldMap[Data::Field::Number]; newTypes += fieldMap[Data::Field::URL]; newTypes += fieldMap[Data::Field::Table]; break; case Data::Field::Choice: newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Para]; newTypes += fieldMap[Data::Field::Choice]; newTypes += fieldMap[Data::Field::Number]; newTypes += fieldMap[Data::Field::URL]; newTypes += fieldMap[Data::Field::Table]; newTypes += fieldMap[Data::Field::Rating]; break; case Data::Field::Table: // not really a good idea since the row delimiter will be exposed, but allow it case Data::Field::Table2: newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Number]; newTypes += fieldMap[Data::Field::Table]; break; case Data::Field::Para: newTypes += fieldMap[Data::Field::Line]; newTypes += fieldMap[Data::Field::Para]; break; case Data::Field::Rating: newTypes += fieldMap[Data::Field::Choice]; newTypes += fieldMap[Data::Field::Rating]; break; // these can never be changed case Data::Field::Image: newTypes += fieldMap[static_cast(type_)]; break; default: myDebug() << "no match for " << type_; newTypes = Data::Field::typeTitles(); break; } return newTypes; } diff --git a/src/configdialog.cpp b/src/configdialog.cpp index da9ae1a1..dbf5ebff 100644 --- a/src/configdialog.cpp +++ b/src/configdialog.cpp @@ -1,1174 +1,1173 @@ /*************************************************************************** Copyright (C) 2001-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include #include "configdialog.h" #include "field.h" #include "collection.h" #include "collectionfactory.h" #include "fetch/execexternalfetcher.h" #include "fetch/fetchmanager.h" #include "fetch/configwidget.h" #include "controller.h" #include "fetcherconfigdialog.h" #include "tellico_kernel.h" #include "utils/tellico_utils.h" #include "utils/string_utils.h" #include "config/tellico_config.h" #include "images/imagefactory.h" #include "gui/combobox.h" #include "gui/collectiontypecombo.h" #include "gui/previewdialog.h" #include "newstuff/manager.h" #include "fieldformat.h" #include "tellico_debug.h" #include #include #include #include #include #include #include #ifdef ENABLE_KNEWSTUFF3 #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int CONFIG_MIN_WIDTH = 640; static const int CONFIG_MIN_HEIGHT = 420; } using Tellico::ConfigDialog; ConfigDialog::ConfigDialog(QWidget* parent_) : KPageDialog(parent_) , m_initializedPages(0) , m_modifying(false) , m_okClicked(false) { setFaceType(List); setModal(true); setWindowTitle(i18n("Configure Tellico")); setStandardButtons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel | QDialogButtonBox::RestoreDefaults); setupGeneralPage(); setupPrintingPage(); setupTemplatePage(); setupFetchPage(); updateGeometry(); QSize s = sizeHint(); resize(qMax(s.width(), CONFIG_MIN_WIDTH), qMax(s.height(), CONFIG_MIN_HEIGHT)); // OK button is connected to buttonBox accepted() signal which is already connected to accept() slot - connect(button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(slotApply())); - connect(button(QDialogButtonBox::Help), SIGNAL(clicked()), SLOT(slotHelp())); - connect(button(QDialogButtonBox::RestoreDefaults), SIGNAL(clicked()), SLOT(slotDefault())); + connect(button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &ConfigDialog::slotApply); + connect(button(QDialogButtonBox::Help), &QAbstractButton::clicked, this, &ConfigDialog::slotHelp); + connect(button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &ConfigDialog::slotDefault); button(QDialogButtonBox::Ok)->setEnabled(false); button(QDialogButtonBox::Apply)->setEnabled(false); button(QDialogButtonBox::Ok)->setDefault(true); button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(this, SIGNAL(currentPageChanged(KPageWidgetItem*, KPageWidgetItem*)), SLOT(slotInitPage(KPageWidgetItem*))); + connect(this, &KPageDialog::currentPageChanged, this, &ConfigDialog::slotInitPage); } ConfigDialog::~ConfigDialog() { foreach(Fetch::ConfigWidget* widget, m_newStuffConfigWidgets) { widget->removed(); } } void ConfigDialog::slotInitPage(KPageWidgetItem* item_) { Q_ASSERT(item_); // every page item has a frame // if the frame has no layout, then we need to initialize the itme QFrame* frame = ::qobject_cast(item_->widget()); Q_ASSERT(frame); if(frame->layout()) { return; } const QString name = item_->name(); // these names must be kept in sync with the page names if(name == i18n("General")) { initGeneralPage(frame); } else if(name == i18n("Printing")) { initPrintingPage(frame); } else if(name == i18n("Templates")) { initTemplatePage(frame); } else if(name == i18n("Data Sources")) { initFetchPage(frame); } } void ConfigDialog::accept() { m_okClicked = true; slotApply(); KPageDialog::accept(); m_okClicked = false; } void ConfigDialog::slotApply() { emit signalConfigChanged(); button(QDialogButtonBox::Apply)->setEnabled(false); } void ConfigDialog::slotDefault() { // only change the defaults on the active page Config::self()->useDefaults(true); const QString name = currentPage()->name(); if(name == i18n("General")) { readGeneralConfig(); } else if(name == i18n("Printing")) { readPrintingConfig(); } else if(name == i18n("Templates")) { readTemplateConfig(); } Config::self()->useDefaults(false); slotModified(); } void ConfigDialog::slotHelp() { const QString name = currentPage()->name(); // these names must be kept in sync with the page names if(name == i18n("General")) { KHelpClient::invokeHelp(QStringLiteral("general-options")); } else if(name == i18n("Printing")) { KHelpClient::invokeHelp(QStringLiteral("printing-options")); } else if(name == i18n("Templates")) { KHelpClient::invokeHelp(QStringLiteral("template-options")); } else if(name == i18n("Data Sources")) { KHelpClient::invokeHelp(QStringLiteral("internet-sources-options")); } } bool ConfigDialog::isPageInitialized(Page page_) const { return m_initializedPages & page_; } void ConfigDialog::setupGeneralPage() { QFrame* frame = new QFrame(this); KPageWidgetItem* page = new KPageWidgetItem(frame, i18n("General")); page->setHeader(i18n("General Options")); page->setIcon(QIcon::fromTheme(QStringLiteral("tellico"), QIcon(QLatin1String(":/icons/tellico")))); addPage(page); // since this is the first page, go ahead and lay it out initGeneralPage(frame); } void ConfigDialog::initGeneralPage(QFrame* frame) { QVBoxLayout* l = new QVBoxLayout(frame); m_cbOpenLastFile = new QCheckBox(i18n("&Reopen file at startup"), frame); m_cbOpenLastFile->setWhatsThis(i18n("If checked, the file that was last open " "will be re-opened at program start-up.")); l->addWidget(m_cbOpenLastFile); - connect(m_cbOpenLastFile, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbOpenLastFile, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); m_cbShowTipDay = new QCheckBox(i18n("&Show \"Tip of the Day\" at startup"), frame); m_cbShowTipDay->setWhatsThis(i18n("If checked, the \"Tip of the Day\" will be " "shown at program start-up.")); l->addWidget(m_cbShowTipDay); - connect(m_cbShowTipDay, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbShowTipDay, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); m_cbEnableWebcam = new QCheckBox(i18n("&Enable webcam for barcode scanning"), frame); m_cbEnableWebcam->setWhatsThis(i18n("If checked, the input from a webcam will be used " "to scan barcodes for searching.")); l->addWidget(m_cbEnableWebcam); - connect(m_cbEnableWebcam, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbEnableWebcam, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); QGroupBox* imageGroupBox = new QGroupBox(i18n("Image Storage Options"), frame); l->addWidget(imageGroupBox); m_rbImageInFile = new QRadioButton(i18n("Store images in data file"), imageGroupBox); m_rbImageInAppDir = new QRadioButton(i18n("Store images in common application directory"), imageGroupBox); m_rbImageInLocalDir = new QRadioButton(i18n("Store images in directory relative to data file"), imageGroupBox); imageGroupBox->setWhatsThis(i18n("Images may be saved in the data file itself, which can " "cause Tellico to run slowly, stored in the Tellico " "application directory, or stored in a directory in the " "same location as the data file.")); QVBoxLayout* imageGroupLayout = new QVBoxLayout(imageGroupBox); imageGroupLayout->addWidget(m_rbImageInFile); imageGroupLayout->addWidget(m_rbImageInAppDir); imageGroupLayout->addWidget(m_rbImageInLocalDir); imageGroupBox->setLayout(imageGroupLayout); QButtonGroup* imageGroup = new QButtonGroup(frame); imageGroup->addButton(m_rbImageInFile); imageGroup->addButton(m_rbImageInAppDir); imageGroup->addButton(m_rbImageInLocalDir); - connect(imageGroup, SIGNAL(buttonClicked(int)), SLOT(slotModified())); + void (QButtonGroup::* buttonClicked)(int) = &QButtonGroup::buttonClicked; + connect(imageGroup, buttonClicked, this, &ConfigDialog::slotModified); QGroupBox* formatGroup = new QGroupBox(i18n("Formatting Options"), frame); l->addWidget(formatGroup); QVBoxLayout* formatGroupLayout = new QVBoxLayout(formatGroup); formatGroup->setLayout(formatGroupLayout); m_cbCapitalize = new QCheckBox(i18n("Auto capitalize &titles and names"), formatGroup); m_cbCapitalize->setWhatsThis(i18n("If checked, titles and names will " "be automatically capitalized.")); - connect(m_cbCapitalize, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbCapitalize, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); formatGroupLayout->addWidget(m_cbCapitalize); m_cbFormat = new QCheckBox(i18n("Auto &format titles and names"), formatGroup); m_cbFormat->setWhatsThis(i18n("If checked, titles and names will " "be automatically formatted.")); - connect(m_cbFormat, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbFormat, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); formatGroupLayout->addWidget(m_cbFormat); QWidget* g1 = new QWidget(formatGroup); QGridLayout* g1Layout = new QGridLayout(g1); g1->setLayout(g1Layout); formatGroupLayout->addWidget(g1); QLabel* lab = new QLabel(i18n("No capitali&zation:"), g1); g1Layout->addWidget(lab, 0, 0); m_leCapitals = new QLineEdit(g1); g1Layout->addWidget(m_leCapitals, 0, 1); lab->setBuddy(m_leCapitals); QString whats = i18n("A list of words which should not be capitalized. Multiple values " "should be separated by a semi-colon."); lab->setWhatsThis(whats); m_leCapitals->setWhatsThis(whats); - connect(m_leCapitals, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_leCapitals, &QLineEdit::textChanged, this, &ConfigDialog::slotModified); lab = new QLabel(i18n("Artic&les:"), g1); g1Layout->addWidget(lab, 1, 0); m_leArticles = new QLineEdit(g1); g1Layout->addWidget(m_leArticles, 1, 1); lab->setBuddy(m_leArticles); whats = i18n("A list of words which should be considered as articles " "if they are the first word in a title. Multiple values " "should be separated by a semi-colon."); lab->setWhatsThis(whats); m_leArticles->setWhatsThis(whats); - connect(m_leArticles, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_leArticles, &QLineEdit::textChanged, this, &ConfigDialog::slotModified); lab = new QLabel(i18n("Personal suffi&xes:"), g1); g1Layout->addWidget(lab, 2, 0); m_leSuffixes = new QLineEdit(g1); g1Layout->addWidget(m_leSuffixes, 2, 1); lab->setBuddy(m_leSuffixes); whats = i18n("A list of suffixes which might be used in personal names. Multiple values " "should be separated by a semi-colon."); lab->setWhatsThis(whats); m_leSuffixes->setWhatsThis(whats); - connect(m_leSuffixes, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_leSuffixes, &QLineEdit::textChanged, this, &ConfigDialog::slotModified); lab = new QLabel(i18n("Surname &prefixes:"), g1); g1Layout->addWidget(lab, 3, 0); m_lePrefixes = new QLineEdit(g1); g1Layout->addWidget(m_lePrefixes, 3, 1); lab->setBuddy(m_lePrefixes); whats = i18n("A list of prefixes which might be used in surnames. Multiple values " "should be separated by a semi-colon."); lab->setWhatsThis(whats); m_lePrefixes->setWhatsThis(whats); - connect(m_lePrefixes, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_lePrefixes, &QLineEdit::textChanged, this, &ConfigDialog::slotModified); // stretch to fill lower area l->addStretch(1); m_initializedPages |= General; readGeneralConfig(); } void ConfigDialog::setupPrintingPage() { QFrame* frame = new QFrame(this); KPageWidgetItem* page = new KPageWidgetItem(frame, i18n("Printing")); page->setHeader(i18n("Printing Options")); page->setIcon(QIcon::fromTheme(QStringLiteral("printer"))); addPage(page); } void ConfigDialog::initPrintingPage(QFrame* frame) { QVBoxLayout* l = new QVBoxLayout(frame); QGroupBox* formatOptions = new QGroupBox(i18n("Formatting Options"), frame); l->addWidget(formatOptions); QVBoxLayout* formatLayout = new QVBoxLayout(formatOptions); formatOptions->setLayout(formatLayout); m_cbPrintFormatted = new QCheckBox(i18n("&Format titles and names"), formatOptions); m_cbPrintFormatted->setWhatsThis(i18n("If checked, titles and names will be automatically formatted.")); - connect(m_cbPrintFormatted, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbPrintFormatted, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); formatLayout->addWidget(m_cbPrintFormatted); m_cbPrintHeaders = new QCheckBox(i18n("&Print field headers"), formatOptions); m_cbPrintHeaders->setWhatsThis(i18n("If checked, the field names will be printed as table headers.")); - connect(m_cbPrintHeaders, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbPrintHeaders, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); formatLayout->addWidget(m_cbPrintHeaders); QGroupBox* groupOptions = new QGroupBox(i18n("Grouping Options"), frame); l->addWidget(groupOptions); QVBoxLayout* groupLayout = new QVBoxLayout(groupOptions); groupOptions->setLayout(groupLayout); m_cbPrintGrouped = new QCheckBox(i18n("&Group the entries"), groupOptions); m_cbPrintGrouped->setWhatsThis(i18n("If checked, the entries will be grouped by the selected field.")); - connect(m_cbPrintGrouped, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_cbPrintGrouped, &QAbstractButton::clicked, this, &ConfigDialog::slotModified); groupLayout->addWidget(m_cbPrintGrouped); QGroupBox* imageOptions = new QGroupBox(i18n("Image Options"), frame); l->addWidget(imageOptions); QGridLayout* gridLayout = new QGridLayout(imageOptions); imageOptions->setLayout(gridLayout); QLabel* lab = new QLabel(i18n("Maximum image &width:"), imageOptions); gridLayout->addWidget(lab, 0, 0); m_imageWidthBox = new QSpinBox(imageOptions); m_imageWidthBox->setMaximum(999); m_imageWidthBox->setMinimum(0); m_imageWidthBox->setValue(50); gridLayout->addWidget(m_imageWidthBox, 0, 1); m_imageWidthBox->setSuffix(QStringLiteral(" px")); lab->setBuddy(m_imageWidthBox); QString whats = i18n("The maximum width of the images in the printout. The aspect ratio is preserved."); lab->setWhatsThis(whats); m_imageWidthBox->setWhatsThis(whats); - connect(m_imageWidthBox, SIGNAL(valueChanged(int)), SLOT(slotModified())); - // QSpinBox doesn't emit valueChanged if you edit the value with - // the lineEdit until you change the keyboard focus -// connect(m_imageWidthBox->child("qt_spinbox_edit"), SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + void (QSpinBox::* valueChanged)(int) = &QSpinBox::valueChanged; + connect(m_imageWidthBox, valueChanged, this, &ConfigDialog::slotModified); lab = new QLabel(i18n("&Maximum image height:"), imageOptions); gridLayout->addWidget(lab, 1, 0); m_imageHeightBox = new QSpinBox(imageOptions); m_imageHeightBox->setMaximum(999); m_imageHeightBox->setMinimum(0); m_imageHeightBox->setValue(50); gridLayout->addWidget(m_imageHeightBox, 1, 1); m_imageHeightBox->setSuffix(QStringLiteral(" px")); lab->setBuddy(m_imageHeightBox); whats = i18n("The maximum height of the images in the printout. The aspect ratio is preserved."); lab->setWhatsThis(whats); m_imageHeightBox->setWhatsThis(whats); - connect(m_imageHeightBox, SIGNAL(valueChanged(int)), SLOT(slotModified())); - // QSpinBox doesn't emit valueChanged if you edit the value with - // the lineEdit until you change the keyboard focus -// connect(m_imageHeightBox->child("qt_spinbox_edit"), SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + connect(m_imageHeightBox, valueChanged, this, &ConfigDialog::slotModified); // stretch to fill lower area l->addStretch(1); m_initializedPages |= Printing; readPrintingConfig(); } void ConfigDialog::setupTemplatePage() { QFrame* frame = new QFrame(this); KPageWidgetItem* page = new KPageWidgetItem(frame, i18n("Templates")); page->setHeader(i18n("Template Options")); // odd icon, I know, matches KMail, though... page->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-theme"))); addPage(page); } void ConfigDialog::initTemplatePage(QFrame* frame) { QVBoxLayout* l = new QVBoxLayout(frame); QGridLayout* gridLayout = new QGridLayout(); l->addLayout(gridLayout); int row = -1; // so I can reuse an i18n string, a plain label can't have an '&' QString s = KLocalizedString::removeAcceleratorMarker(i18n("Collection &type:")); QLabel* lab = new QLabel(s, frame); gridLayout->addWidget(lab, ++row, 0); const int collType = Kernel::self()->collectionType(); lab = new QLabel(CollectionFactory::nameHash().value(collType), frame); gridLayout->addWidget(lab, row, 1, 1, 2); lab = new QLabel(i18n("Template:"), frame); m_templateCombo = new GUI::ComboBox(frame); - connect(m_templateCombo, SIGNAL(activated(int)), SLOT(slotModified())); + void (QComboBox::* activatedInt)(int) = &QComboBox::activated; + connect(m_templateCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_templateCombo); QString whats = i18n("Select the template to use for the current type of collections. " "Not all templates will use the font and color settings."); lab->setWhatsThis(whats); m_templateCombo->setWhatsThis(whats); gridLayout->addWidget(lab, ++row, 0); gridLayout->addWidget(m_templateCombo, row, 1); QPushButton* btn = new QPushButton(i18n("&Preview..."), frame); btn->setWhatsThis(i18n("Show a preview of the template")); btn->setIcon(QIcon::fromTheme(QStringLiteral("zoom-original"))); gridLayout->addWidget(btn, row, 2); - connect(btn, SIGNAL(clicked()), SLOT(slotShowTemplatePreview())); + connect(btn, &QAbstractButton::clicked, this, &ConfigDialog::slotShowTemplatePreview); // so the button is squeezed small gridLayout->setColumnStretch(0, 10); gridLayout->setColumnStretch(1, 10); loadTemplateList(); // QLabel* l1 = new QLabel(i18n("The options below will be passed to the template, but not " // "all templates will use them. Some fonts and colors may be " // "specified directly in the template."), frame); // l1->setTextFormat(Qt::RichText); // l->addWidget(l1); QGroupBox* fontGroup = new QGroupBox(i18n("Font Options"), frame); l->addWidget(fontGroup); row = -1; QGridLayout* fontLayout = new QGridLayout(); fontGroup->setLayout(fontLayout); lab = new QLabel(i18n("Font:"), fontGroup); fontLayout->addWidget(lab, ++row, 0); m_fontCombo = new QFontComboBox(fontGroup); fontLayout->addWidget(m_fontCombo, row, 1); - connect(m_fontCombo, SIGNAL(activated(int)), SLOT(slotModified())); + connect(m_fontCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_fontCombo); whats = i18n("This font is passed to the template used in the Entry View."); lab->setWhatsThis(whats); m_fontCombo->setWhatsThis(whats); fontLayout->addWidget(new QLabel(i18n("Size:"), fontGroup), ++row, 0); m_fontSizeInput = new QSpinBox(fontGroup); m_fontSizeInput->setMaximum(30); // 30 is same max as konq config m_fontSizeInput->setMinimum(5); m_fontSizeInput->setSuffix(QStringLiteral("pt")); fontLayout->addWidget(m_fontSizeInput, row, 1); - connect(m_fontSizeInput, SIGNAL(valueChanged(int)), SLOT(slotModified())); + void (QSpinBox::* valueChangedInt)(int) = &QSpinBox::valueChanged; + connect(m_fontSizeInput, valueChangedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_fontSizeInput); lab->setWhatsThis(whats); m_fontSizeInput->setWhatsThis(whats); QGroupBox* colGroup = new QGroupBox(i18n("Color Options"), frame); l->addWidget(colGroup); row = -1; QGridLayout* colLayout = new QGridLayout(); colGroup->setLayout(colLayout); lab = new QLabel(i18n("Background color:"), colGroup); colLayout->addWidget(lab, ++row, 0); m_baseColorCombo = new KColorCombo(colGroup); colLayout->addWidget(m_baseColorCombo, row, 1); - connect(m_baseColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + connect(m_baseColorCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_baseColorCombo); whats = i18n("This color is passed to the template used in the Entry View."); lab->setWhatsThis(whats); m_baseColorCombo->setWhatsThis(whats); lab = new QLabel(i18n("Text color:"), colGroup); colLayout->addWidget(lab, ++row, 0); m_textColorCombo = new KColorCombo(colGroup); colLayout->addWidget(m_textColorCombo, row, 1); - connect(m_textColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + connect(m_textColorCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_textColorCombo); lab->setWhatsThis(whats); m_textColorCombo->setWhatsThis(whats); lab = new QLabel(i18n("Highlight color:"), colGroup); colLayout->addWidget(lab, ++row, 0); m_highBaseColorCombo = new KColorCombo(colGroup); colLayout->addWidget(m_highBaseColorCombo, row, 1); - connect(m_highBaseColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + connect(m_highBaseColorCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_highBaseColorCombo); lab->setWhatsThis(whats); m_highBaseColorCombo->setWhatsThis(whats); lab = new QLabel(i18n("Highlighted text color:"), colGroup); colLayout->addWidget(lab, ++row, 0); m_highTextColorCombo = new KColorCombo(colGroup); colLayout->addWidget(m_highTextColorCombo, row, 1); - connect(m_highTextColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + connect(m_highTextColorCombo, activatedInt, this, &ConfigDialog::slotModified); lab->setBuddy(m_highTextColorCombo); lab->setWhatsThis(whats); m_highTextColorCombo->setWhatsThis(whats); QGroupBox* groupBox = new QGroupBox(i18n("Manage Templates"), frame); l->addWidget(groupBox); QVBoxLayout* vlay = new QVBoxLayout(groupBox); groupBox->setLayout(vlay); QWidget* box1 = new QWidget(groupBox); QHBoxLayout* box1HBoxLayout = new QHBoxLayout(box1); box1HBoxLayout->setMargin(0); vlay->addWidget(box1); box1HBoxLayout->setSpacing(QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QPushButton* b1 = new QPushButton(i18n("Install..."), box1); box1HBoxLayout->addWidget(b1); b1->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); - connect(b1, SIGNAL(clicked()), SLOT(slotInstallTemplate())); + connect(b1, &QAbstractButton::clicked, this, &ConfigDialog::slotInstallTemplate); whats = i18n("Click to install a new template directly."); b1->setWhatsThis(whats); QPushButton* b2 = new QPushButton(i18n("Download..."), box1); box1HBoxLayout->addWidget(b2); b2->setIcon(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff"))); - connect(b2, SIGNAL(clicked()), SLOT(slotDownloadTemplate())); + connect(b2, &QAbstractButton::clicked, this, &ConfigDialog::slotDownloadTemplate); whats = i18n("Click to download additional templates."); b2->setWhatsThis(whats); QPushButton* b3 = new QPushButton(i18n("Delete..."), box1); box1HBoxLayout->addWidget(b3); b3->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); - connect(b3, SIGNAL(clicked()), SLOT(slotDeleteTemplate())); + connect(b3, &QAbstractButton::clicked, this, &ConfigDialog::slotDeleteTemplate); whats = i18n("Click to select and remove installed templates."); b3->setWhatsThis(whats); // stretch to fill lower area l->addStretch(1); // purely for aesthetics make all widgets line up QList widgets; widgets.append(m_fontCombo); widgets.append(m_fontSizeInput); widgets.append(m_baseColorCombo); widgets.append(m_textColorCombo); widgets.append(m_highBaseColorCombo); widgets.append(m_highTextColorCombo); int w = 0; foreach(QWidget* widget, widgets) { widget->ensurePolished(); w = qMax(w, widget->sizeHint().width()); } foreach(QWidget* widget, widgets) { widget->setMinimumWidth(w); } KAcceleratorManager::manage(frame); m_initializedPages |= Template; readTemplateConfig(); } void ConfigDialog::setupFetchPage() { QFrame* frame = new QFrame(this); KPageWidgetItem* page = new KPageWidgetItem(frame, i18n("Data Sources")); page->setHeader(i18n("Data Sources Options")); page->setIcon(QIcon::fromTheme(QStringLiteral("network-wired"))); addPage(page); } void ConfigDialog::initFetchPage(QFrame* frame) { QHBoxLayout* l = new QHBoxLayout(frame); QVBoxLayout* leftLayout = new QVBoxLayout(); l->addLayout(leftLayout); m_sourceListWidget = new QListWidget(frame); m_sourceListWidget->setSortingEnabled(false); // no sorting m_sourceListWidget->setSelectionMode(QAbstractItemView::SingleSelection); leftLayout->addWidget(m_sourceListWidget, 1); - connect(m_sourceListWidget, SIGNAL(currentItemChanged(QListWidgetItem*, QListWidgetItem*)), SLOT(slotSelectedSourceChanged(QListWidgetItem*))); - connect(m_sourceListWidget, SIGNAL(itemDoubleClicked(QListWidgetItem*)), SLOT(slotModifySourceClicked())); + connect(m_sourceListWidget, &QListWidget::currentItemChanged, this, &ConfigDialog::slotSelectedSourceChanged); + connect(m_sourceListWidget, &QListWidget::itemDoubleClicked, this, &ConfigDialog::slotModifySourceClicked); QWidget* hb = new QWidget(frame); QHBoxLayout* hbHBoxLayout = new QHBoxLayout(hb); hbHBoxLayout->setMargin(0); leftLayout->addWidget(hb); m_moveUpSourceBtn = new QPushButton(i18n("Move &Up"), hb); hbHBoxLayout->addWidget(m_moveUpSourceBtn); m_moveUpSourceBtn->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); m_moveUpSourceBtn->setWhatsThis(i18n("The order of the data sources sets the order " "that Tellico uses when entries are automatically updated.")); m_moveDownSourceBtn = new QPushButton(i18n("Move &Down"), hb); hbHBoxLayout->addWidget(m_moveDownSourceBtn); m_moveDownSourceBtn->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); m_moveDownSourceBtn->setWhatsThis(i18n("The order of the data sources sets the order " "that Tellico uses when entries are automatically updated.")); QWidget* hb2 = new QWidget(frame); QHBoxLayout* hb2HBoxLayout = new QHBoxLayout(hb2); hb2HBoxLayout->setMargin(0); leftLayout->addWidget(hb2); m_cbFilterSource = new QCheckBox(i18n("Filter by type:"), hb2); hb2HBoxLayout->addWidget(m_cbFilterSource); - connect(m_cbFilterSource, SIGNAL(clicked()), SLOT(slotSourceFilterChanged())); + connect(m_cbFilterSource, &QAbstractButton::clicked, this, &ConfigDialog::slotSourceFilterChanged); m_sourceTypeCombo = new GUI::CollectionTypeCombo(hb2); hb2HBoxLayout->addWidget(m_sourceTypeCombo); - connect(m_sourceTypeCombo, SIGNAL(currentIndexChanged(int)), SLOT(slotSourceFilterChanged())); + void (QComboBox::* currentIndexChanged)(int) = &QComboBox::currentIndexChanged; + connect(m_sourceTypeCombo, currentIndexChanged, this, &ConfigDialog::slotSourceFilterChanged); // we want to remove the item for a custom collection int index = m_sourceTypeCombo->findData(Data::Collection::Base); if(index > -1) { m_sourceTypeCombo->removeItem(index); } // disable until check box is checked m_sourceTypeCombo->setEnabled(false); // these icons are rather arbitrary, but seem to vaguely fit QVBoxLayout* vlay = new QVBoxLayout(); l->addLayout(vlay); QPushButton* newSourceBtn = new QPushButton(i18n("&New..."), frame); newSourceBtn->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); newSourceBtn->setWhatsThis(i18n("Click to add a new data source.")); m_modifySourceBtn = new QPushButton(i18n("&Modify..."), frame); m_modifySourceBtn->setIcon(QIcon::fromTheme(QStringLiteral("network-wired"))); m_modifySourceBtn->setWhatsThis(i18n("Click to modify the selected data source.")); m_removeSourceBtn = new QPushButton(i18n("&Delete"), frame); m_removeSourceBtn->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); m_removeSourceBtn->setWhatsThis(i18n("Click to delete the selected data source.")); m_newStuffBtn = new QPushButton(i18n("Download..."), frame); m_newStuffBtn->setIcon(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff"))); m_newStuffBtn->setWhatsThis(i18n("Click to download additional data sources.")); // checksum and signature checking are no longer possible with knewstuff2 // disable button for now m_newStuffBtn->setEnabled(false); vlay->addWidget(newSourceBtn); vlay->addWidget(m_modifySourceBtn); vlay->addWidget(m_removeSourceBtn); // separate newstuff button from the rest vlay->addSpacing(16); vlay->addWidget(m_newStuffBtn); vlay->addStretch(1); - connect(newSourceBtn, SIGNAL(clicked()), SLOT(slotNewSourceClicked())); - connect(m_modifySourceBtn, SIGNAL(clicked()), SLOT(slotModifySourceClicked())); - connect(m_moveUpSourceBtn, SIGNAL(clicked()), SLOT(slotMoveUpSourceClicked())); - connect(m_moveDownSourceBtn, SIGNAL(clicked()), SLOT(slotMoveDownSourceClicked())); - connect(m_removeSourceBtn, SIGNAL(clicked()), SLOT(slotRemoveSourceClicked())); - connect(m_newStuffBtn, SIGNAL(clicked()), SLOT(slotNewStuffClicked())); + connect(newSourceBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotNewSourceClicked); + connect(m_modifySourceBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotModifySourceClicked); + connect(m_moveUpSourceBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotMoveUpSourceClicked); + connect(m_moveDownSourceBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotMoveDownSourceClicked); + connect(m_removeSourceBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotRemoveSourceClicked); + connect(m_newStuffBtn, &QAbstractButton::clicked, this, &ConfigDialog::slotNewStuffClicked); KAcceleratorManager::manage(frame); m_initializedPages |= Fetch; readFetchConfig(); } void ConfigDialog::readGeneralConfig() { m_modifying = true; m_cbShowTipDay->setChecked(Config::showTipOfDay()); m_cbOpenLastFile->setChecked(Config::reopenLastFile()); #ifdef ENABLE_WEBCAM m_cbEnableWebcam->setChecked(Config::enableWebcam()); #else m_cbEnableWebcam->setChecked(false); m_cbEnableWebcam->setEnabled(false); #endif switch(Config::imageLocation()) { case Config::ImagesInFile: m_rbImageInFile->setChecked(true); break; case Config::ImagesInAppDir: m_rbImageInAppDir->setChecked(true); break; case Config::ImagesInLocalDir: m_rbImageInLocalDir->setChecked(true); break; } bool autoCapitals = Config::autoCapitalization(); m_cbCapitalize->setChecked(autoCapitals); bool autoFormat = Config::autoFormat(); m_cbFormat->setChecked(autoFormat); const QRegExp comma(QLatin1String("\\s*,\\s*")); m_leCapitals->setText(Config::noCapitalizationString().replace(comma, FieldFormat::delimiterString())); m_leArticles->setText(Config::articlesString().replace(comma, FieldFormat::delimiterString())); m_leSuffixes->setText(Config::nameSuffixesString().replace(comma, FieldFormat::delimiterString())); m_lePrefixes->setText(Config::surnamePrefixesString().replace(comma, FieldFormat::delimiterString())); m_modifying = false; } void ConfigDialog::readPrintingConfig() { m_modifying = true; m_cbPrintHeaders->setChecked(Config::printFieldHeaders()); m_cbPrintFormatted->setChecked(Config::printFormatted()); m_cbPrintGrouped->setChecked(Config::printGrouped()); m_imageWidthBox->setValue(Config::maxImageWidth()); m_imageHeightBox->setValue(Config::maxImageHeight()); m_modifying = false; } void ConfigDialog::readTemplateConfig() { m_modifying = true; // entry template selection const int collType = Kernel::self()->collectionType(); QString file = Config::templateName(collType); file.replace(QLatin1Char('_'), QLatin1Char(' ')); QString fileContext = file + QLatin1String(" XSL Template"); m_templateCombo->setCurrentItem(i18nc(fileContext.toUtf8().constData(), file.toUtf8().constData())); m_fontCombo->setCurrentFont(QFont(Config::templateFont(collType).family())); m_fontSizeInput->setValue(Config::templateFont(collType).pointSize()); m_baseColorCombo->setColor(Config::templateBaseColor(collType)); m_textColorCombo->setColor(Config::templateTextColor(collType)); m_highBaseColorCombo->setColor(Config::templateHighlightedBaseColor(collType)); m_highTextColorCombo->setColor(Config::templateHighlightedTextColor(collType)); m_modifying = false; } void ConfigDialog::readFetchConfig() { m_modifying = true; m_sourceListWidget->clear(); m_configWidgets.clear(); m_sourceListWidget->setUpdatesEnabled(false); foreach(Fetch::Fetcher::Ptr fetcher, Fetch::Manager::self()->fetchers()) { Fetch::FetcherInfo info(fetcher->type(), fetcher->source(), fetcher->updateOverwrite(), fetcher->uuid()); FetcherInfoListItem* item = new FetcherInfoListItem(m_sourceListWidget, info); item->setFetcher(fetcher); } m_sourceListWidget->setUpdatesEnabled(true); if(m_sourceListWidget->count() == 0) { m_modifySourceBtn->setEnabled(false); m_removeSourceBtn->setEnabled(false); } else { // go ahead and select the first one m_sourceListWidget->setCurrentItem(m_sourceListWidget->item(0)); } m_modifying = false; - QTimer::singleShot(500, this, SLOT(slotCreateConfigWidgets())); + QTimer::singleShot(500, this, &ConfigDialog::slotCreateConfigWidgets); } void ConfigDialog::saveConfiguration() { if(isPageInitialized(General)) saveGeneralConfig(); if(isPageInitialized(Printing)) savePrintingConfig(); if(isPageInitialized(Template)) saveTemplateConfig(); if(isPageInitialized(Fetch)) saveFetchConfig(); } void ConfigDialog::saveGeneralConfig() { Config::setShowTipOfDay(m_cbShowTipDay->isChecked()); Config::setEnableWebcam(m_cbEnableWebcam->isChecked()); int imageLocation; if(m_rbImageInFile->isChecked()) { imageLocation = Config::ImagesInFile; } else if(m_rbImageInAppDir->isChecked()) { imageLocation = Config::ImagesInAppDir; } else { imageLocation = Config::ImagesInLocalDir; } Config::setImageLocation(imageLocation); Config::setReopenLastFile(m_cbOpenLastFile->isChecked()); Config::setAutoCapitalization(m_cbCapitalize->isChecked()); Config::setAutoFormat(m_cbFormat->isChecked()); const QRegExp semicolon(QLatin1String("\\s*;\\s*")); const QChar comma = QLatin1Char(','); Config::setNoCapitalizationString(m_leCapitals->text().replace(semicolon, comma)); Config::setArticlesString(m_leArticles->text().replace(semicolon, comma)); Config::setNameSuffixesString(m_leSuffixes->text().replace(semicolon, comma)); Config::setSurnamePrefixesString(m_lePrefixes->text().replace(semicolon, comma)); } void ConfigDialog::savePrintingConfig() { Config::setPrintFieldHeaders(m_cbPrintHeaders->isChecked()); Config::setPrintFormatted(m_cbPrintFormatted->isChecked()); Config::setPrintGrouped(m_cbPrintGrouped->isChecked()); Config::setMaxImageWidth(m_imageWidthBox->value()); Config::setMaxImageHeight(m_imageHeightBox->value()); } void ConfigDialog::saveTemplateConfig() { const int collType = Kernel::self()->collectionType(); Config::setTemplateName(collType, m_templateCombo->currentData().toString()); QFont font(m_fontCombo->currentFont().family(), m_fontSizeInput->value()); Config::setTemplateFont(collType, font); Config::setTemplateBaseColor(collType, m_baseColorCombo->color()); Config::setTemplateTextColor(collType, m_textColorCombo->color()); Config::setTemplateHighlightedBaseColor(collType, m_highBaseColorCombo->color()); Config::setTemplateHighlightedTextColor(collType, m_highTextColorCombo->color()); } void ConfigDialog::saveFetchConfig() { // first, tell config widgets they got deleted foreach(Fetch::ConfigWidget* widget, m_removedConfigWidgets) { widget->removed(); } m_removedConfigWidgets.clear(); bool reloadFetchers = false; int count = 0; // start group numbering at 0 for( ; count < m_sourceListWidget->count(); ++count) { FetcherInfoListItem* item = static_cast(m_sourceListWidget->item(count)); Fetch::ConfigWidget* cw = m_configWidgets[item]; if(!cw || (!cw->shouldSave() && !item->isNewSource())) { continue; } m_newStuffConfigWidgets.removeAll(cw); QString group = QStringLiteral("Data Source %1").arg(count); // in case we later change the order, clear the group now KSharedConfig::openConfig()->deleteGroup(group); KConfigGroup configGroup(KSharedConfig::openConfig(), group); configGroup.writeEntry("Name", item->data(Qt::DisplayRole).toString()); configGroup.writeEntry("Type", int(item->fetchType())); configGroup.writeEntry("UpdateOverwrite", item->updateOverwrite()); configGroup.writeEntry("Uuid", item->uuid()); cw->saveConfig(configGroup); item->setNewSource(false); // in case the ordering changed item->setConfigGroup(group); reloadFetchers = true; } // now update total number of sources KConfigGroup sourceGroup(KSharedConfig::openConfig(), "Data Sources"); sourceGroup.writeEntry("Sources Count", count); // and purge old config groups QString group = QStringLiteral("Data Source %1").arg(count); while(KSharedConfig::openConfig()->hasGroup(group)) { KSharedConfig::openConfig()->deleteGroup(group); ++count; group = QStringLiteral("Data Source %1").arg(count); } Config::self()->save(); if(reloadFetchers) { Fetch::Manager::self()->loadFetchers(); Controller::self()->updatedFetchers(); // reload fetcher items if OK was not clicked // meaning apply was clicked if(!m_okClicked) { QString currentSource; if(m_sourceListWidget->currentItem()) { currentSource = m_sourceListWidget->currentItem()->data(Qt::DisplayRole).toString(); } readFetchConfig(); if(!currentSource.isEmpty()) { QList items = m_sourceListWidget->findItems(currentSource, Qt::MatchExactly); if(!items.isEmpty()) { m_sourceListWidget->setCurrentItem(items.first()); m_sourceListWidget->scrollToItem(items.first()); } } } } } void ConfigDialog::slotModified() { if(m_modifying) { return; } button(QDialogButtonBox::Ok)->setEnabled(true); button(QDialogButtonBox::Apply)->setEnabled(true); } void ConfigDialog::slotNewSourceClicked() { FetcherConfigDialog dlg(this); if(dlg.exec() != QDialog::Accepted) { return; } Fetch::Type type = dlg.sourceType(); if(type == Fetch::Unknown) { return; } Fetch::FetcherInfo info(type, dlg.sourceName(), dlg.updateOverwrite()); FetcherInfoListItem* item = new FetcherInfoListItem(m_sourceListWidget, info); m_sourceListWidget->scrollToItem(item); m_sourceListWidget->setCurrentItem(item); Fetch::ConfigWidget* cw = dlg.configWidget(); if(cw) { cw->setAccepted(true); cw->slotSetModified(); cw->setParent(this); // keep the config widget around m_configWidgets.insert(item, cw); } m_modifySourceBtn->setEnabled(true); m_removeSourceBtn->setEnabled(true); slotModified(); // toggle apply button } void ConfigDialog::slotModifySourceClicked() { FetcherInfoListItem* item = static_cast(m_sourceListWidget->currentItem()); if(!item) { return; } Fetch::ConfigWidget* cw = nullptr; if(m_configWidgets.contains(item)) { cw = m_configWidgets[item]; } else { // grab the config widget, taking ownership cw = item->fetcher()->configWidget(this); if(cw) { // might return 0 when no widget available for fetcher type m_configWidgets.insert(item, cw); // there's weird layout bug if it's not hidden cw->hide(); } } if(!cw) { // no config widget for this one // might be because support was compiled out myDebug() << "no config widget for source" << item->data(Qt::DisplayRole).toString(); return; } FetcherConfigDialog dlg(item->data(Qt::DisplayRole).toString(), item->fetchType(), item->updateOverwrite(), cw, this); if(dlg.exec() == QDialog::Accepted) { cw->setAccepted(true); // mark to save QString newName = dlg.sourceName(); if(newName != item->data(Qt::DisplayRole).toString()) { item->setData(Qt::DisplayRole, newName); cw->slotSetModified(); } item->setUpdateOverwrite(dlg.updateOverwrite()); slotModified(); // toggle apply button } cw->setParent(this); // keep the config widget around } void ConfigDialog::slotRemoveSourceClicked() { FetcherInfoListItem* item = static_cast(m_sourceListWidget->currentItem()); if(!item) { return; } Tellico::NewStuff::Manager::self()->removeScriptByName(item->text()); Fetch::ConfigWidget* cw = m_configWidgets[item]; if(cw) { m_removedConfigWidgets.append(cw); // it gets deleted by the parent } m_configWidgets.remove(item); delete item; // m_sourceListWidget->setCurrentItem(m_sourceListWidget->currentItem()); slotModified(); // toggle apply button } void ConfigDialog::slotMoveUpSourceClicked() { int row = m_sourceListWidget->currentRow(); if(row < 1) { return; } QListWidgetItem* item = m_sourceListWidget->takeItem(row); m_sourceListWidget->insertItem(row-1, item); m_sourceListWidget->setCurrentItem(item); slotModified(); // toggle apply button } void ConfigDialog::slotMoveDownSourceClicked() { int row = m_sourceListWidget->currentRow(); if(row > m_sourceListWidget->count()-2) { return; } QListWidgetItem* item = m_sourceListWidget->takeItem(row); m_sourceListWidget->insertItem(row+1, item); m_sourceListWidget->setCurrentItem(item); slotModified(); // toggle apply button } void ConfigDialog::slotSourceFilterChanged() { m_sourceTypeCombo->setEnabled(m_cbFilterSource->isChecked()); const bool showAll = !m_sourceTypeCombo->isEnabled(); const int type = m_sourceTypeCombo->currentType(); for(int count = 0; count < m_sourceListWidget->count(); ++count) { FetcherInfoListItem* item = static_cast(m_sourceListWidget->item(count)); item->setHidden(!showAll && item->fetcher() && !item->fetcher()->canFetch(type)); } } void ConfigDialog::slotSelectedSourceChanged(QListWidgetItem* item_) { int row = m_sourceListWidget->row(item_); m_moveUpSourceBtn->setEnabled(row > 0); m_moveDownSourceBtn->setEnabled(row < m_sourceListWidget->count()-1); } void ConfigDialog::slotNewStuffClicked() { #ifdef ENABLE_KNEWSTUFF3 KNS3::DownloadDialog dialog(QStringLiteral("tellico-script.knsrc"), this); dialog.exec(); KNS3::Entry::List entries = dialog.installedEntries(); if(!entries.isEmpty()) { Fetch::Manager::self()->loadFetchers(); readFetchConfig(); } #endif } Tellico::FetcherInfoListItem* ConfigDialog::findItem(const QString& path_) const { if(path_.isEmpty()) { myDebug() << "empty path"; return nullptr; } // this is a bit ugly, loop over all items, find the execexternal one // that matches the path for(int i = 0; i < m_sourceListWidget->count(); ++i) { FetcherInfoListItem* item = static_cast(m_sourceListWidget->item(i)); if(item->fetchType() != Fetch::ExecExternal) { continue; } Fetch::ExecExternalFetcher* f = dynamic_cast(item->fetcher().data()); if(f && f->execPath() == path_) { return item; } } myDebug() << "no matching item found"; return nullptr; } void ConfigDialog::slotShowTemplatePreview() { GUI::PreviewDialog* dlg = new GUI::PreviewDialog(this); const QString templateName = m_templateCombo->currentData().toString(); dlg->setXSLTFile(templateName + QLatin1String(".xsl")); StyleOptions options; options.fontFamily = m_fontCombo->currentFont().family(); options.fontSize = m_fontSizeInput->value(); options.baseColor = m_baseColorCombo->color(); options.textColor = m_textColorCombo->color(); options.highlightedTextColor = m_highTextColorCombo->color(); options.highlightedBaseColor = m_highBaseColorCombo->color(); dlg->setXSLTOptions(Kernel::self()->collectionType(), options); Data::CollPtr c = CollectionFactory::collection(Kernel::self()->collectionType(), true); Data::EntryPtr e(new Data::Entry(c)); foreach(Data::FieldPtr f, c->fields()) { if(f->name() == QLatin1String("title")) { e->setField(f->name(), m_templateCombo->currentText()); } else if(f->type() == Data::Field::Image) { continue; } else if(f->type() == Data::Field::Choice) { e->setField(f->name(), f->allowed().front()); } else if(f->type() == Data::Field::Number) { e->setField(f->name(), QStringLiteral("1")); } else if(f->type() == Data::Field::Bool) { e->setField(f->name(), QStringLiteral("true")); } else if(f->type() == Data::Field::Rating) { e->setField(f->name(), QStringLiteral("5")); } else { e->setField(f->name(), f->title()); } } dlg->showEntry(e); dlg->show(); // dlg gets deleted by itself // the finished() signal is connected in its constructor to delayedDestruct } void ConfigDialog::loadTemplateList() { QStringList files = Tellico::locateAllFiles(QStringLiteral("tellico/entry-templates/*.xsl")); QMap templates; // a QMap will have them values sorted by key foreach(const QString& file, files) { QFileInfo fi(file); QString lfile = fi.fileName().section(QLatin1Char('.'), 0, -2); QString name = lfile; name.replace(QLatin1Char('_'), QLatin1Char(' ')); QString title = i18nc((name + QLatin1String(" XSL Template")).toUtf8().constData(), name.toUtf8().constData()); templates.insert(title, lfile); } QString s = m_templateCombo->currentText(); m_templateCombo->clear(); for(QMap::ConstIterator it2 = templates.constBegin(); it2 != templates.constEnd(); ++it2) { m_templateCombo->addItem(it2.key(), it2.value()); } m_templateCombo->setCurrentItem(s); } void ConfigDialog::slotInstallTemplate() { QString filter = i18n("XSL Files") + QLatin1String(" (*.xsl)") + QLatin1String(";;"); filter += i18n("Template Packages") + QLatin1String(" (*.tar.gz *.tgz)") + QLatin1String(";;"); filter += i18n("All Files") + QLatin1String(" (*)"); const QString fileClass(QStringLiteral(":InstallTemplate")); const QString f = QFileDialog::getOpenFileName(this, QString(), KRecentDirs::dir(fileClass), filter); if(f.isEmpty()) { return; } KRecentDirs::add(fileClass, QFileInfo(f).dir().canonicalPath()); if(Tellico::NewStuff::Manager::self()->installTemplate(f)) { loadTemplateList(); } } void ConfigDialog::slotDownloadTemplate() { #ifdef ENABLE_KNEWSTUFF3 KNS3::DownloadDialog dialog(QStringLiteral("tellico-template.knsrc"), this); dialog.exec(); KNS3::Entry::List entries = dialog.installedEntries(); if(!entries.isEmpty()) { loadTemplateList(); } #endif } void ConfigDialog::slotDeleteTemplate() { bool ok; QString name = QInputDialog::getItem(this, i18n("Delete Template"), i18n("Select template to delete:"), Tellico::NewStuff::Manager::self()->userTemplates().keys(), 0, false, &ok); if(ok && !name.isEmpty()) { Tellico::NewStuff::Manager::self()->removeTemplateByName(name); loadTemplateList(); } } void ConfigDialog::slotCreateConfigWidgets() { for(int count = 0; count < m_sourceListWidget->count(); ++count) { FetcherInfoListItem* item = static_cast(m_sourceListWidget->item(count)); // only create a new config widget if we don't have one already if(!m_configWidgets.contains(item)) { Fetch::ConfigWidget* cw = item->fetcher()->configWidget(this); if(cw) { // might return 0 when no widget available for fetcher type m_configWidgets.insert(item, cw); // there's weird layout bug if it's not hidden cw->hide(); } } } } diff --git a/src/detailedlistview.cpp b/src/detailedlistview.cpp index 449c38cd..5f1bb106 100644 --- a/src/detailedlistview.cpp +++ b/src/detailedlistview.cpp @@ -1,639 +1,639 @@ /*************************************************************************** Copyright (C) 2001-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "detailedlistview.h" #include "collection.h" #include "collectionfactory.h" #include "controller.h" #include "field.h" #include "entry.h" #include "tellico_debug.h" #include "tellico_kernel.h" #include "config/tellico_config.h" #include "models/entrymodel.h" #include "models/entrysortmodel.h" #include "models/modelmanager.h" #include "gui/detailedentryitemdelegate.h" #include "gui/ratingdelegate.h" #include "utils/string_utils.h" #include #include #include #include #include #include using namespace Tellico; using Tellico::DetailedListView; DetailedListView::DetailedListView(QWidget* parent_) : GUI::TreeView(parent_) , m_loadingCollection(false), m_currentContextColumn(-1) { setHeaderHidden(false); setSelectionMode(QAbstractItemView::ExtendedSelection); setAlternatingRowColors(true); setRootIsDecorated(false); setUniformRowHeights(true); - connect(this, SIGNAL(doubleClicked(const QModelIndex&)), SLOT(slotDoubleClicked(const QModelIndex&))); + connect(this, &QAbstractItemView::doubleClicked, this, &DetailedListView::slotDoubleClicked); // header menu header()->installEventFilter(this); header()->setMinimumSectionSize(20); m_headerMenu = new QMenu(this); m_columnMenu = new QMenu(this); - connect(m_columnMenu, SIGNAL(triggered(QAction*)), - SLOT(slotColumnMenuActivated(QAction*))); + connect(m_columnMenu, &QMenu::triggered, + this, &DetailedListView::slotColumnMenuActivated); EntryModel* entryModel = new EntryModel(this); EntrySortModel* sortModel = new EntrySortModel(this); sortModel->setSortRole(EntryPtrRole); sortModel->setSourceModel(entryModel); setModel(sortModel); setItemDelegate(new DetailedEntryItemDelegate(this)); ModelManager::self()->setEntryModel(sortModel); - connect(model(), SIGNAL(headerDataChanged(Qt::Orientation, int, int)), SLOT(updateHeaderMenu())); - connect(model(), SIGNAL(headerDataChanged(Qt::Orientation, int, int)), SLOT(updateColumnDelegates())); - connect(model(), SIGNAL(columnsInserted(const QModelIndex&, int, int)), SLOT(hideNewColumn(const QModelIndex&, int, int))); - connect(header(), SIGNAL(sectionCountChanged(int, int)), SLOT(updateHeaderMenu())); + connect(model(), &QAbstractItemModel::headerDataChanged, this, &DetailedListView::updateHeaderMenu); + connect(model(), &QAbstractItemModel::headerDataChanged, this, &DetailedListView::updateColumnDelegates); + connect(model(), &QAbstractItemModel::columnsInserted, this, &DetailedListView::hideNewColumn); + connect(header(), &QHeaderView::sectionCountChanged, this, &DetailedListView::updateHeaderMenu); } DetailedListView::~DetailedListView() { } Tellico::EntryModel* DetailedListView::sourceModel() const { return static_cast(sortModel()->sourceModel()); } void DetailedListView::addCollection(Tellico::Data::CollPtr coll_) { if(!coll_) { return; } const QString configGroup = QStringLiteral("Options - %1").arg(CollectionFactory::typeName(coll_)); KConfigGroup config(KSharedConfig::openConfig(), configGroup); QString configN; if(coll_->type() == Data::Collection::Base) { QUrl url = Kernel::self()->URL(); for(int i = 0; i < Config::maxCustomURLSettings(); ++i) { QUrl u = config.readEntry(QStringLiteral("URL_%1").arg(i), QUrl()); if(u == url) { configN = QStringLiteral("_%1").arg(i); break; } } } // we don't want to immediately hide all these columns when adding fields - disconnect(model(), SIGNAL(columnsInserted(const QModelIndex&, int, int)), - this, SLOT(hideNewColumn(const QModelIndex&, int, int))); + disconnect(model(), &QAbstractItemModel::columnsInserted, + this, &DetailedListView::hideNewColumn); sourceModel()->setImagesAreAvailable(false); sourceModel()->setFields(coll_->fields()); - connect(model(), SIGNAL(columnsInserted(const QModelIndex&, int, int)), - this, SLOT(hideNewColumn(const QModelIndex&, int, int))); + connect(model(), &QAbstractItemModel::columnsInserted, + this, &DetailedListView::hideNewColumn); // we're not using saveState() and restoreState() since our columns are variable QStringList columnNames = config.readEntry(QLatin1String("ColumnNames") + configN, QStringList()); QList columnWidths = config.readEntry(QLatin1String("ColumnWidths") + configN, QList()); QList columnOrder = config.readEntry(QLatin1String("ColumnOrder") + configN, QList()); // just a broken-world check while(columnWidths.size() < columnNames.size()) { columnWidths << 0; } while(columnOrder.size() < columnNames.size()) { columnOrder << columnOrder.size(); } QList currentColumnOrder; // now restore widths and order for(int ncol = 0; ncol < header()->count(); ++ncol) { int idx = columnNames.indexOf(columnFieldName(ncol)); // column width of 0 means hidden if(idx < 0 || columnWidths.at(idx) <= 0) { hideColumn(ncol); if(idx > -1) { currentColumnOrder << ncol; } } else { setColumnWidth(ncol, columnWidths.at(idx)); currentColumnOrder << ncol; } } const int maxCount = qMin(currentColumnOrder.size(), columnOrder.size()); for(int i = 0; i < maxCount; ++i) { header()->moveSection(header()->visualIndex(currentColumnOrder.at(i)), columnOrder.at(i)); } // always hide tables and paragraphs for(int ncol = 0; ncol < coll_->fields().count(); ++ncol) { Data::FieldPtr field = model()->headerData(ncol, Qt::Horizontal, FieldPtrRole).value(); if(field) { if(field->type() == Data::Field::Table || field->type() == Data::Field::Para) { hideColumn(ncol); } } else { myDebug() << "no field for col" << ncol; } } // because some of the fields got hidden... updateColumnDelegates(); updateHeaderMenu(); checkHeader(); sortModel()->setSortColumn(config.readEntry(QLatin1String("SortColumn") + configN, -1)); sortModel()->setSecondarySortColumn(config.readEntry(QLatin1String("PrevSortColumn") + configN, -1)); sortModel()->setTertiarySortColumn(config.readEntry(QLatin1String("Prev2SortColumn") + configN, -1)); const int order = config.readEntry(QLatin1String("SortOrder") + configN, static_cast(Qt::AscendingOrder)); sortModel()->setSortOrder(static_cast(order)); setUpdatesEnabled(false); m_loadingCollection = true; addEntries(coll_->entries()); m_loadingCollection = false; setUpdatesEnabled(true); header()->setSortIndicator(sortModel()->sortColumn(), sortModel()->sortOrder()); } void DetailedListView::slotReset() { //clear() does not remove columns sourceModel()->clear(); } void DetailedListView::addEntries(Tellico::Data::EntryList entries_) { if(entries_.isEmpty()) { return; } sourceModel()->addEntries(entries_); if(!m_loadingCollection) { setState(entries_, NewState); } } void DetailedListView::modifyEntries(Tellico::Data::EntryList entries_) { if(entries_.isEmpty()) { return; } sourceModel()->modifyEntries(entries_); setState(entries_, ModifiedState); } void DetailedListView::removeEntries(Tellico::Data::EntryList entries_) { if(entries_.isEmpty()) { return; } sourceModel()->removeEntries(entries_); } void DetailedListView::setState(Tellico::Data::EntryList entries_, int state) { foreach(Data::EntryPtr entry, entries_) { QModelIndex index = sourceModel()->indexFromEntry(entry); if(index.isValid()) { sourceModel()->setData(index, state, SaveStateRole); } else { myWarning() << "no index found for" << entry->id() << entry->title(); } } } void DetailedListView::removeCollection(Tellico::Data::CollPtr coll_) { if(!coll_) { myWarning() << "null coll pointer!"; return; } sourceModel()->clear(); } void DetailedListView::contextMenuEvent(QContextMenuEvent* event_) { QModelIndex index = indexAt(event_->pos()); if(!index.isValid()) { return; } QMenu menu(this); Controller::self()->plugEntryActions(&menu); menu.exec(event_->globalPos()); } // don't shadow QListView::setSelected void DetailedListView::setEntriesSelected(Data::EntryList entries_) { if(entries_.isEmpty()) { // don't move this one outside the block since it calls setCurrentItem(0) clearSelection(); return; } clearSelection(); EntrySortModel* proxyModel = static_cast(model()); foreach(Data::EntryPtr entry, entries_) { QModelIndex index = sourceModel()->indexFromEntry(entry); if(!proxyModel->mapFromSource(index).isValid()) { // clear the filter if we're trying to select an entry that is currently filtered out Controller::self()->clearFilter(); break; } } blockSignals(true); foreach(Data::EntryPtr entry, entries_) { QModelIndex index = sourceModel()->indexFromEntry(entry); selectionModel()->select(proxyModel->mapFromSource(index), QItemSelectionModel::Select | QItemSelectionModel::Rows); } //setCurrentIndex(index); blockSignals(false); QModelIndex index = sourceModel()->indexFromEntry(entries_.first()); scrollTo(proxyModel->mapFromSource(index)); } bool DetailedListView::eventFilter(QObject* obj_, QEvent* event_) { if(event_->type() == QEvent::ContextMenu && obj_ == header()) { m_currentContextColumn = header()->logicalIndexAt(static_cast(event_)->pos()); m_headerMenu->exec(static_cast(event_)->globalPos()); return true; } return GUI::TreeView::eventFilter(obj_, event_); } void DetailedListView::slotDoubleClicked(const QModelIndex& index_) { Data::EntryPtr entry = index_.data(EntryPtrRole).value(); if(entry) { Controller::self()->editEntry(entry); } } void DetailedListView::slotColumnMenuActivated(QAction* action_) { const int col = action_->data().toInt(); if(col > -1) { // only column actions have data const bool isChecked = action_->isChecked(); setColumnHidden(col, !isChecked); // if we're showing a column, resize all sections if(isChecked) { resizeColumnToContents(col); adjustColumnWidths(); } } checkHeader(); } void DetailedListView::showAllColumns() { foreach(QAction* action, m_columnMenu->actions()) { if(action->isCheckable() && !action->isChecked()) { action->trigger(); } } } void DetailedListView::hideAllColumns() { for(int ncol = 0; ncol < header()->count(); ++ncol) { hideColumn(ncol); } foreach(QAction* action, m_columnMenu->actions()) { if(action->isCheckable()) { action->setChecked(false); } } checkHeader(); } void DetailedListView::hideCurrentColumn() { setColumnHidden(m_currentContextColumn, true); checkHeader(); } void DetailedListView::slotRefresh() { sortModel()->invalidate(); } void DetailedListView::setFilter(Tellico::FilterPtr filter_) { static_cast(sortModel())->setFilter(filter_); } Tellico::FilterPtr DetailedListView::filter() const { return static_cast(sortModel())->filter(); } void DetailedListView::addField(Tellico::Data::CollPtr, Tellico::Data::FieldPtr field) { sourceModel()->addFields(Data::FieldList() << field); } void DetailedListView::modifyField(Tellico::Data::CollPtr, Tellico::Data::FieldPtr oldField_, Tellico::Data::FieldPtr newField_) { Q_UNUSED(oldField_) sourceModel()->modifyField(oldField_, newField_); } void DetailedListView::removeField(Tellico::Data::CollPtr, Tellico::Data::FieldPtr field_) { sourceModel()->removeFields(Data::FieldList() << field_); } void DetailedListView::reorderFields(const Tellico::Data::FieldList& fields_) { QStringList columnNames; QList columnWidths, columnOrder; for(int ncol = 0; ncol < header()->count(); ++ncol) { // ignore hidden columns if(!isColumnHidden(ncol)) { columnNames << columnFieldName(ncol); columnWidths << columnWidth(ncol); columnOrder << header()->visualIndex(ncol); } } sourceModel()->setFields(fields_); QList currentColumnOrder; // now restore widths and order for(int ncol = 0; ncol < header()->count(); ++ncol) { int idx = columnNames.indexOf(columnFieldName(ncol)); // column width of 0 means hidden if(idx < 0 || columnWidths.at(idx) <= 0) { hideColumn(ncol); if(idx > -1) { currentColumnOrder << ncol; } } else { setColumnWidth(ncol, columnWidths.at(idx)); currentColumnOrder << ncol; } } const int maxCount = qMin(currentColumnOrder.size(), columnOrder.size()); for(int i = 0; i < maxCount; ++i) { header()->moveSection(header()->visualIndex(currentColumnOrder.at(i)), columnOrder.at(i)); } updateHeaderMenu(); } void DetailedListView::saveConfig(Tellico::Data::CollPtr coll_, int configIndex_) { const QString configGroup = QStringLiteral("Options - %1").arg(CollectionFactory::typeName(coll_)); KConfigGroup config(KSharedConfig::openConfig(), configGroup); // all of this is to have custom settings on a per file basis QString configN; if(coll_->type() == Data::Collection::Base) { QList info; for(int i = 0; i < Config::maxCustomURLSettings(); ++i) { QUrl u(config.readEntry(QStringLiteral("URL_%1").arg(i))); if(!u.isEmpty() && i != configIndex_) { configN = QStringLiteral("_%1").arg(i); ConfigInfo ci; ci.cols = config.readEntry(QLatin1String("ColumnNames") + configN, QStringList()); ci.widths = config.readEntry(QLatin1String("ColumnWidths") + configN, QList()); ci.order = config.readEntry(QLatin1String("ColumnOrder") + configN, QList()); ci.prevSort = config.readEntry(QLatin1String("PrevSortColumn") + configN, 0); ci.prev2Sort = config.readEntry(QLatin1String("Prev2SortColumn") + configN, 0); ci.sortOrder = config.readEntry(QLatin1String("SortOrder") + configN, static_cast(Qt::AscendingOrder)); info.append(ci); } } // subtract one since we're writing the current settings, too int limit = qMin(info.count(), Config::maxCustomURLSettings()-1); for(int i = 0; i < limit; ++i) { // starts at one since the current config will be written below configN = QStringLiteral("_%1").arg(i+1); config.writeEntry(QLatin1String("ColumnNames") + configN, info[i].cols); config.writeEntry(QLatin1String("ColumnWidths") + configN, info[i].widths); config.writeEntry(QLatin1String("ColumnOrder") + configN, info[i].order); config.writeEntry(QLatin1String("PrevSortColumn") + configN, info[i].prevSort); config.writeEntry(QLatin1String("Prev2SortColumn") + configN, info[i].prev2Sort); config.writeEntry(QLatin1String("SortOrder") + configN, info[i].sortOrder); // legacy entry item config.deleteEntry(QLatin1String("ColumnState") + configN); } configN = QStringLiteral("_0"); } QStringList colNames; QList widths, order; for(int ncol = 0; ncol < header()->count(); ++ncol) { // ignore hidden columns if(!isColumnHidden(ncol)) { colNames << columnFieldName(ncol); widths << columnWidth(ncol); order << header()->visualIndex(ncol); } } config.writeEntry(QLatin1String("ColumnNames") + configN, colNames); config.writeEntry(QLatin1String("ColumnWidths") + configN, widths); config.writeEntry(QLatin1String("ColumnOrder") + configN, order); const int sortCol1 = sortModel()->sortColumn(); const int sortCol2 = sortModel()->secondarySortColumn(); const int sortCol3 = sortModel()->tertiarySortColumn(); const int sortOrder = static_cast(sortModel()->sortOrder()); config.writeEntry(QLatin1String("SortColumn") + configN, sortCol1); config.writeEntry(QLatin1String("PrevSortColumn") + configN, sortCol2); config.writeEntry(QLatin1String("Prev2SortColumn") + configN, sortCol3); config.writeEntry(QLatin1String("SortOrder") + configN, sortOrder); // remove old entry item config.deleteEntry(QLatin1String("ColumnState") + configN); } QString DetailedListView::sortColumnTitle1() const { return model()->headerData(header()->sortIndicatorSection(), Qt::Horizontal).toString(); } QString DetailedListView::sortColumnTitle2() const { return model()->headerData(sortModel()->secondarySortColumn(), Qt::Horizontal).toString(); } QString DetailedListView::sortColumnTitle3() const { return model()->headerData(sortModel()->tertiarySortColumn(), Qt::Horizontal).toString(); } QStringList DetailedListView::visibleColumns() const { // we want the visual order, so use a QMap and sort by visualIndex QMap titleMap; for(int i = 0; i < header()->count(); ++i) { if(!isColumnHidden(i)) { titleMap.insert(header()->visualIndex(i), model()->headerData(i, Qt::Horizontal).toString()); } } return titleMap.values(); } // can't be const Tellico::Data::EntryList DetailedListView::visibleEntries() { // We could just return the full collection entry list if the filter is 0 // but printing depends on the sorted order Data::EntryList entries; for(int i = 0; i < model()->rowCount(); ++i) { Data::EntryPtr tmp = model()->data(model()->index(i, 0), EntryPtrRole).value(); if(tmp) { entries += tmp; } } return entries; } void DetailedListView::selectAllVisible() { QModelIndex topLeft = model()->index(0, 0); QModelIndex bottomRight = model()->index(model()->rowCount()-1, model()->columnCount()-1); QItemSelection selection(topLeft, bottomRight); selectionModel()->select(selection, QItemSelectionModel::Select); } int DetailedListView::visibleItems() const { return model()->rowCount(); } void DetailedListView::resetEntryStatus() { sourceModel()->clearSaveState(); } void DetailedListView::updateHeaderMenu() { // we only want to update the menu when the header count and model count agree if(model()->columnCount() != header()->count()) { myDebug() << "column counts disagree"; return; } m_headerMenu->clear(); m_headerMenu->addSection(i18n("View Columns")); m_columnMenu->clear(); for(int ncol = 0; ncol < header()->count(); ++ncol) { Data::FieldPtr field = model()->headerData(ncol, Qt::Horizontal, FieldPtrRole).value(); if(field && (field->type() == Data::Field::Table || field->type() == Data::Field::Para)) { continue; } QAction* act = m_columnMenu->addAction(model()->headerData(ncol, Qt::Horizontal).toString()); act->setData(ncol); act->setCheckable(true); act->setChecked(!isColumnHidden(ncol)); } QAction* columnAction = m_headerMenu->addMenu(m_columnMenu); columnAction->setText(i18nc("Noun, Menu name", "Columns")); columnAction->setIcon(QIcon::fromTheme(QStringLiteral("view-file-columns"))); QAction* actHideThis = m_headerMenu->addAction(i18n("Hide This Column")); - connect(actHideThis, SIGNAL(triggered(bool)), this, SLOT(hideCurrentColumn())); + connect(actHideThis, &QAction::triggered, this, &DetailedListView::hideCurrentColumn); QAction* actResize = m_headerMenu->addAction(QIcon::fromTheme(QStringLiteral("zoom-fit-width")), i18n("Resize to Content")); - connect(actResize, SIGNAL(triggered(bool)), this, SLOT(resizeColumnsToContents())); + connect(actResize, &QAction::triggered, this, &DetailedListView::resizeColumnsToContents); m_headerMenu->addSeparator(); QAction* actShowAll = m_headerMenu->addAction(i18n("Show All Columns")); - connect(actShowAll, SIGNAL(triggered(bool)), this, SLOT(showAllColumns())); + connect(actShowAll, &QAction::triggered, this, &DetailedListView::showAllColumns); QAction* actHideAll = m_headerMenu->addAction(i18n("Hide All Columns")); - connect(actHideAll, SIGNAL(triggered(bool)), this, SLOT(hideAllColumns())); + connect(actHideAll, &QAction::triggered, this, &DetailedListView::hideAllColumns); } void DetailedListView::updateColumnDelegates() { for(int ncol = 0; ncol < header()->count(); ++ncol) { Data::FieldPtr field = model()->headerData(ncol, Qt::Horizontal, FieldPtrRole).value(); if(field && field->type() == Data::Field::Rating) { /// if we're not using the overall delegate, delete the delegate since we're setting a new on if(itemDelegateForColumn(ncol) != itemDelegate()) { delete itemDelegateForColumn(ncol); } RatingDelegate* delegate = new RatingDelegate(this); bool ok; // not used delegate->setMaxRating(Tellico::toUInt(field->property(QStringLiteral("maximum")), &ok)); setItemDelegateForColumn(ncol, delegate); } else { // reset column delegate to overall delegate setItemDelegateForColumn(ncol, itemDelegate()); } } } void DetailedListView::slotRefreshImages() { sourceModel()->setImagesAreAvailable(true); } void DetailedListView::adjustColumnWidths() { // this function is called when a column is shown // reduce all visible columns to their size hint, if they are wider than that for(int ncol = 0; ncol < header()->count(); ++ncol) { if(!isColumnHidden(ncol)) { const int width = sizeHintForColumn(ncol); if(columnWidth(ncol) > width) { resizeColumnToContents(ncol); } } } } void DetailedListView::resizeColumnsToContents() { for(int ncol = 0; ncol < header()->count(); ++ncol) { if(!isColumnHidden(ncol)) { resizeColumnToContents(ncol); } } } void DetailedListView::hideNewColumn(const QModelIndex& index_, int start_, int end_) { Q_UNUSED(index_); for(int ncol = start_; ncol <= end_; ++ncol) { hideColumn(ncol); } updateHeaderMenu(); // make sure to update checkable actions } void DetailedListView::checkHeader() { // the header disappears if all columns are hidden, so if the user hides all // columns, we turn around and show the title // // normally, I would expect a check like header()->count() == header()->hiddenSectionCount() // to tell me if all sections are hidden, but it often doesn't work, with the hiddenSectionCount() // being greater than count()! From testing, if the sizeHint() width is 0, then the header is hidden if(!header()->sizeHint().isEmpty()) { return; } // find title action in menu and activate it QAction* action = nullptr; QAction* fallbackAction = nullptr; foreach(QAction* tryAction, m_columnMenu->actions()) { const int ncol = tryAction->data().toInt(); if(ncol > -1 && columnFieldName(ncol) == QLatin1String("title")) { action = tryAction; break; } else if(ncol > -1 && !fallbackAction) { fallbackAction = tryAction; } } if(!action) { action = fallbackAction; } if(action) { action->setChecked(true); const int col = action->data().toInt(); // calling slotColumnMenuActivated() would be infinite loop setColumnHidden(col, false); resizeColumnToContents(col); } else { myDebug() << "found no action to show, still empty header!"; } } QString DetailedListView::columnFieldName(int ncol_) const { Data::FieldPtr field = model()->headerData(ncol_, Qt::Horizontal, FieldPtrRole).value(); return field ? field->name() : QString(); } diff --git a/src/document.cpp b/src/document.cpp index 266fdb51..28052ad1 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -1,855 +1,855 @@ /*************************************************************************** Copyright (C) 2001-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "document.h" #include "collectionfactory.h" #include "translators/tellicoimporter.h" #include "translators/tellicozipexporter.h" #include "translators/tellicoxmlexporter.h" #include "collection.h" #include "core/filehandler.h" #include "borrower.h" #include "fieldformat.h" #include "core/tellico_strings.h" #include "images/imagefactory.h" #include "images/imagedirectory.h" #include "images/image.h" #include "images/imageinfo.h" #include "utils/stringset.h" #include "progressmanager.h" #include "config/tellico_config.h" #include "entrycomparison.h" #include "utils/guiproxy.h" #include "tellico_debug.h" #include #include #include #include #include #include using namespace Tellico; using Tellico::Data::Document; Document* Document::s_self = nullptr; Document::Document() : QObject(), m_coll(nullptr), m_isModified(false), m_loadAllImages(false), m_validFile(false), m_importer(nullptr), m_cancelImageWriting(true), m_fileFormat(Import::TellicoImporter::Unknown) { m_allImagesOnDisk = Config::imageLocation() != Config::ImagesInFile; newDocument(Collection::Book); } Document::~Document() { delete m_importer; m_importer = nullptr; } Tellico::Data::CollPtr Document::collection() const { return m_coll; } void Document::setURL(const QUrl& url_) { m_url = url_; if(m_url.fileName() != i18n(Tellico::untitledFilename)) { ImageFactory::setLocalDirectory(m_url); EntryComparison::setDocumentUrl(m_url); } } void Document::setModified(bool modified_) { if(modified_ != m_isModified) { m_isModified = modified_; emit signalModified(m_isModified); } } void Document::slotSetModified() { setModified(true); } /** * Since QUndoStack emits cleanChanged(), the behavior is opposite * the document modified flag */ void Document::slotSetClean(bool clean_) { setModified(!clean_); } bool Document::newDocument(int type_) { if(m_importer) { m_importer->deleteLater(); m_importer = nullptr; } deleteContents(); m_coll = CollectionFactory::collection(type_, true); m_coll->setTrackGroups(true); emit signalCollectionAdded(m_coll); emit signalCollectionImagesLoaded(m_coll); setModified(false); QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename)); setURL(url); m_validFile = false; m_fileFormat = Import::TellicoImporter::Unknown; return true; } bool Document::openDocument(const QUrl& url_) { MARK; m_loadAllImages = false; // delayed image loading only works for local files if(!url_.isLocalFile()) { m_loadAllImages = true; } if(m_importer) { m_importer->deleteLater(); } m_importer = new Import::TellicoImporter(url_, m_loadAllImages); ProgressItem& item = ProgressManager::self()->newProgressItem(m_importer, m_importer->progressLabel(), true); connect(m_importer, &Import::Importer::signalTotalSteps, ProgressManager::self(), &ProgressManager::setTotalSteps); connect(m_importer, &Import::Importer::signalProgress, ProgressManager::self(), &ProgressManager::setProgress); connect(&item, &ProgressItem::signalCancelled, m_importer, &Import::Importer::slotCancel); ProgressItem::Done done(m_importer); CollPtr coll = m_importer->collection(); if(!m_importer) { myDebug() << "The importer was deleted out from under us"; return false; } // delayed image loading only works for zip files // format is only known AFTER collection() is called m_fileFormat = m_importer->format(); m_allImagesOnDisk = !m_importer->hasImages(); if(!m_importer->hasImages() || m_fileFormat != Import::TellicoImporter::Zip) { m_loadAllImages = true; } ImageFactory::setZipArchive(m_importer->takeImages()); if(!coll) { // myDebug() << "returning false"; GUI::Proxy::sorry(m_importer->statusMessage()); m_validFile = false; return false; } deleteContents(); m_coll = coll; m_coll->setTrackGroups(true); setURL(url_); m_validFile = true; emit signalCollectionAdded(m_coll); // m_importer might have been deleted? setModified(m_importer && m_importer->modifiedOriginal()); // if(pruneImages()) { // slotSetModified(true); // } if(m_importer && m_importer->hasImages()) { m_cancelImageWriting = false; - QTimer::singleShot(500, this, SLOT(slotLoadAllImages())); + QTimer::singleShot(500, this, &Document::slotLoadAllImages); } else { emit signalCollectionImagesLoaded(m_coll); if(m_importer) { m_importer->deleteLater(); m_importer = nullptr; } } return true; } bool Document::saveDocument(const QUrl& url_, bool force_) { // FileHandler::queryExists calls FileHandler::writeBackupFile // so the only reason to check queryExists() is if the url to write to is different than the current one if(url_ == m_url) { if(!FileHandler::writeBackupFile(url_)) { return false; } } else { if(!force_ && !FileHandler::queryExists(url_)) { return false; } } // in case we're still loading images, give that a chance to cancel m_cancelImageWriting = true; qApp->processEvents(); ProgressItem& item = ProgressManager::self()->newProgressItem(this, i18n("Saving file..."), false); ProgressItem::Done done(this); // will always save as zip file, no matter if has images or not int imageLocation = Config::imageLocation(); bool includeImages = imageLocation == Config::ImagesInFile; int totalSteps; // write all images to disk cache if needed // have to do this before executing exporter in case // the user changed the imageInFile setting from Yes to No, in which // case saving will overwrite the old file that has the images in it! if(includeImages) { totalSteps = 10; item.setTotalSteps(totalSteps); // since TellicoZipExporter uses 100 steps, then it will get 100/110 of the total progress } else { totalSteps = 100; item.setTotalSteps(totalSteps); m_cancelImageWriting = false; writeAllImages(imageLocation == Config::ImagesInAppDir ? ImageFactory::DataDir : ImageFactory::LocalDir, url_); } QScopedPointer exporter; if(m_fileFormat == Import::TellicoImporter::XML) { exporter.reset(new Export::TellicoXMLExporter(m_coll)); static_cast(exporter.data())->setIncludeImages(includeImages); } else { exporter.reset(new Export::TellicoZipExporter(m_coll)); static_cast(exporter.data())->setIncludeImages(includeImages); } item.setProgress(int(0.8*totalSteps)); exporter->setEntries(m_coll->entries()); exporter->setURL(url_); // since we already asked about overwriting the file, force the save long opt = exporter->options() | Export::ExportForce | Export::ExportComplete | Export::ExportProgress; // only write the image sizes if they're known already opt &= ~Export::ExportImageSize; exporter->setOptions(opt); const bool success = exporter->exec(); item.setProgress(int(0.9*totalSteps)); if(success) { setURL(url_); // if successful, doc is no longer modified setModified(false); } else { myDebug() << "Document::saveDocument() - not successful saving to" << url_.url(); } return success; } bool Document::closeDocument() { if(m_importer) { m_importer->deleteLater(); m_importer = nullptr; } deleteContents(); return true; } void Document::deleteContents() { if(m_coll) { emit signalCollectionDeleted(m_coll); } // don't delete the m_importer here, bad things will happen // since the collection holds a pointer to each entry and each entry // hold a pointer to the collection, and they're both sharedptrs, // neither will ever get deleted, unless the entries are removed from the collection if(m_coll) { m_coll->clear(); } m_coll = nullptr; // old collection gets deleted as refcount goes to 0 m_cancelImageWriting = true; } void Document::appendCollection(Tellico::Data::CollPtr coll_) { appendCollection(m_coll, coll_); } void Document::appendCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_) { if(!coll1_ || !coll2_) { return; } coll1_->blockSignals(true); foreach(FieldPtr field, coll2_->fields()) { coll1_->mergeField(field); } Data::EntryList newEntries; foreach(EntryPtr entry, coll2_->entries()) { Data::EntryPtr newEntry(new Data::Entry(*entry)); newEntry->setCollection(coll1_); newEntries << newEntry; } coll1_->addEntries(newEntries); // TODO: merge filters and loans coll1_->blockSignals(false); } Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll_) { return mergeCollection(m_coll, coll_); } Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_) { MergePair pair; if(!coll1_ || !coll2_) { return pair; } coll1_->blockSignals(true); Data::FieldList fields = coll2_->fields(); foreach(FieldPtr field, fields) { coll1_->mergeField(field); } EntryList currEntries = coll1_->entries(); EntryList newEntries = coll2_->entries(); std::sort(currEntries.begin(), currEntries.end(), Data::EntryCmp(QStringLiteral("title"))); std::sort(newEntries.begin(), newEntries.end(), Data::EntryCmp(QStringLiteral("title"))); const int currTotal = currEntries.count(); int lastMatchId = 0; bool checkSameId = false; // if the matching entries have the same id, then check that first for later comparisons foreach(EntryPtr newEntry, newEntries) { int bestMatch = 0; Data::EntryPtr matchEntry, currEntry; // first, if we're checking against same ID if(checkSameId) { currEntry = coll1_->entryById(newEntry->id()); if(currEntry && coll1_->sameEntry(currEntry, newEntry) >= EntryComparison::ENTRY_PERFECT_MATCH) { // only have to compare against perfect match matchEntry = currEntry; } } if(!matchEntry) { // alternative is to loop over them all for(int i = 0; i < currTotal; ++i) { // since we're sorted by title, track the index of the previous match and start comparison there currEntry = currEntries.at((i+lastMatchId) % currTotal); const int match = coll1_->sameEntry(currEntry, newEntry); if(match >= EntryComparison::ENTRY_PERFECT_MATCH) { matchEntry = currEntry; lastMatchId = (i+lastMatchId) % currTotal; break; } else if(match >= EntryComparison::ENTRY_GOOD_MATCH && match > bestMatch) { bestMatch = match; matchEntry = currEntry; lastMatchId = (i+lastMatchId) % currTotal; // don't break, keep looking for better one } } } if(matchEntry) { checkSameId = checkSameId || (matchEntry->id() == newEntry->id()); mergeEntry(matchEntry, newEntry); } else { Data::EntryPtr e(new Data::Entry(*newEntry)); e->setCollection(coll1_); // keep track of which entries got added pair.first.append(e); } } coll1_->addEntries(pair.first); // TODO: merge filters and loans coll1_->blockSignals(false); return pair; } void Document::replaceCollection(Tellico::Data::CollPtr coll_) { if(!coll_) { return; } QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename)); setURL(url); m_validFile = false; // the collection gets cleared by the CollectionCommand that called this function // no need to do it here m_coll = coll_; m_coll->setTrackGroups(true); m_cancelImageWriting = true; // CollectionCommand takes care of calling Controller signals } void Document::unAppendCollection(Tellico::Data::CollPtr coll_, Tellico::Data::FieldList origFields_) { if(!coll_) { return; } m_coll->blockSignals(true); StringSet origFieldNames; foreach(FieldPtr field, origFields_) { m_coll->modifyField(field); origFieldNames.add(field->name()); } EntryList entries = coll_->entries(); foreach(EntryPtr entry, entries) { // probably don't need to do this, but on the safe side... entry->setCollection(coll_); } m_coll->removeEntries(entries); // since Collection::removeField() iterates over all entries to reset the value of the field // don't removeField() until after removeEntry() is done FieldList currFields = m_coll->fields(); foreach(FieldPtr field, currFields) { if(!origFieldNames.has(field->name())) { m_coll->removeField(field); } } m_coll->blockSignals(false); } void Document::unMergeCollection(Tellico::Data::CollPtr coll_, Tellico::Data::FieldList origFields_, Tellico::Data::MergePair entryPair_) { if(!coll_) { return; } m_coll->blockSignals(true); QStringList origFieldNames; foreach(FieldPtr field, origFields_) { m_coll->modifyField(field); origFieldNames << field->name(); } // first item in pair are the entries added by the operation, remove them EntryList entries = entryPair_.first; m_coll->removeEntries(entries); // second item in pair are the entries which got modified by the original merge command const QString track = QStringLiteral("track"); PairVector trackChanges = entryPair_.second; // need to go through them in reverse since one entry may have been modified multiple times // first item in the pair is the entry pointer // second item is the old value of the track field for(int i = trackChanges.count()-1; i >= 0; --i) { trackChanges[i].first->setField(track, trackChanges[i].second); } // since Collection::removeField() iterates over all entries to reset the value of the field // don't removeField() until after removeEntry() is done FieldList currFields = m_coll->fields(); foreach(FieldPtr field, currFields) { if(origFieldNames.indexOf(field->name()) == -1) { m_coll->removeField(field); } } m_coll->blockSignals(false); } bool Document::isEmpty() const { //an empty doc may contain a collection, but no entries return (!m_coll || m_coll->entries().isEmpty()); } bool Document::loadAllImagesNow() const { // DEBUG_LINE; if(!m_coll || !m_validFile) { return false; } if(m_loadAllImages) { myDebug() << "Document::loadAllImagesNow() - all valid images should already be loaded!"; return false; } return Import::TellicoImporter::loadAllImages(m_url); } Tellico::Data::EntryList Document::filteredEntries(Tellico::FilterPtr filter_) const { Data::EntryList matches; Data::EntryList entries = m_coll->entries(); foreach(EntryPtr entry, entries) { if(filter_->matches(entry)) { matches.append(entry); } } return matches; } void Document::checkOutEntry(Tellico::Data::EntryPtr entry_) { if(!entry_) { return; } const QString loaned = QStringLiteral("loaned"); if(!m_coll->hasField(loaned)) { FieldPtr f(new Field(loaned, i18n("Loaned"), Field::Bool)); f->setFlags(Field::AllowGrouped); f->setCategory(i18n("Personal")); m_coll->addField(f); } entry_->setField(loaned, QStringLiteral("true")); EntryList vec; vec.append(entry_); m_coll->updateDicts(vec, QStringList() << loaned); } void Document::checkInEntry(Tellico::Data::EntryPtr entry_) { if(!entry_) { return; } const QString loaned = QStringLiteral("loaned"); if(!m_coll->hasField(loaned)) { return; } entry_->setField(loaned, QString()); m_coll->updateDicts(EntryList() << entry_, QStringList() << loaned); } void Document::renameCollection(const QString& newTitle_) { m_coll->setTitle(newTitle_); } // this only gets called when a zip file with images is opened // by loading every image, it gets pulled out of the zip file and // copied to disk. Then the zip file can be closed and not retained in memory void Document::slotLoadAllImages() { QString id; StringSet images; foreach(EntryPtr entry, m_coll->entries()) { foreach(FieldPtr field, m_coll->imageFields()) { id = entry->field(field); if(id.isEmpty() || images.has(id)) { continue; } // this is the early loading, so just by calling imageById() // the image gets sucked from the zip file and written to disk // by ImageFactory::imageById() // TODO:: does this need to check against images with link only? if(ImageFactory::imageById(id).isNull()) { myDebug() << "Null image for entry:" << entry->title() << id; } images.add(id); if(m_cancelImageWriting) { break; } } if(m_cancelImageWriting) { break; } // stay responsive, do this in the background qApp->processEvents(); } if(m_cancelImageWriting) { myLog() << "slotLoadAllImages() - cancel image writing"; } else { emit signalCollectionImagesLoaded(m_coll); } m_cancelImageWriting = false; if(m_importer) { m_importer->deleteLater(); m_importer = nullptr; } } // cacheDir_ is the location dir to write the images // localDir_ provide the new file location which is only needed if cacheDir == LocalDir void Document::writeAllImages(int cacheDir_, const QUrl& localDir_) { // images get 80 steps in saveDocument() const uint stepSize = 1 + qMax(1, m_coll->entryCount()/80); // add 1 since it could round off uint j = 1; ImageFactory::CacheDir cacheDir = static_cast(cacheDir_); QScopedPointer imgDir; if(cacheDir == ImageFactory::LocalDir) { imgDir.reset(new ImageDirectory(ImageFactory::localDirectory(localDir_))); } QString id; StringSet images; EntryList entries = m_coll->entries(); FieldList imageFields = m_coll->imageFields(); foreach(EntryPtr entry, entries) { foreach(FieldPtr field, imageFields) { id = entry->field(field); if(id.isEmpty() || images.has(id)) { continue; } images.add(id); if(ImageFactory::imageInfo(id).linkOnly) { continue; } // careful here, if we're writing to LocalDir, need to read from the old LocalDir and write to new bool success; if(cacheDir == ImageFactory::LocalDir) { success = ImageFactory::writeCachedImage(id, imgDir.data()); } else { success = ImageFactory::writeCachedImage(id, cacheDir); } if(!success) { myDebug() << "did not write image for entry title:" << entry->title(); } if(m_cancelImageWriting) { break; } } if(j%stepSize == 0) { ProgressManager::self()->setProgress(this, j/stepSize); qApp->processEvents(); } ++j; if(m_cancelImageWriting) { break; } } if(m_cancelImageWriting) { myDebug() << "Document::writeAllImages() - cancel image writing"; } m_cancelImageWriting = false; } bool Document::pruneImages() { bool found = false; QString id; StringSet images; Data::EntryList entries = m_coll->entries(); Data::FieldList imageFields = m_coll->imageFields(); foreach(EntryPtr entry, entries) { foreach(FieldPtr field, imageFields) { id = entry->field(field); if(id.isEmpty() || images.has(id)) { continue; } const Data::Image& img = ImageFactory::imageById(id); if(img.isNull()) { entry->setField(field, QString()); found = true; myDebug() << "removing null image for" << entry->title() << ":" << id; } else { images.add(id); } } } return found; } int Document::imageCount() const { if(!m_coll) { return 0; } StringSet images; FieldList fields = m_coll->imageFields(); EntryList entries = m_coll->entries(); foreach(FieldPtr field, fields) { foreach(EntryPtr entry, entries) { images.add(entry->field(field->name())); } } return images.count(); } void Document::removeImagesNotInCollection(Tellico::Data::EntryList entries_, Tellico::Data::EntryList entriesToKeep_) { // first get list of all images in collection StringSet images; FieldList fields = m_coll->imageFields(); EntryList allEntries = m_coll->entries(); foreach(FieldPtr field, fields) { foreach(EntryPtr entry, allEntries) { images.add(entry->field(field->name())); } foreach(EntryPtr entry, entriesToKeep_) { images.add(entry->field(field->name())); } } // now for all images not in the cache, we can clear them StringSet imagesToCheck = ImageFactory::imagesNotInCache(); // if entries_ is not empty, that means we want to limit the images removed // to those that are referenced in those entries StringSet imagesToRemove; foreach(FieldPtr field, fields) { foreach(EntryPtr entry, entries_) { QString id = entry->field(field->name()); if(!id.isEmpty() && imagesToCheck.has(id) && !images.has(id)) { imagesToRemove.add(id); } } } const QStringList realImagesToRemove = imagesToRemove.toList(); for(QStringList::ConstIterator it = realImagesToRemove.begin(); it != realImagesToRemove.end(); ++it) { ImageFactory::removeImage(*it, false); // doesn't delete, just remove link } } bool Document::mergeEntry(Data::EntryPtr e1, Data::EntryPtr e2, MergeConflictResolver* resolver_) { if(!e1 || !e2) { myDebug() << "bad entry pointer"; return false; } bool ret = true; foreach(FieldPtr field, e1->collection()->fields()) { if(e2->field(field).isEmpty()) { continue; } // never try to merge entry id, creation date or mod date. Those are unique to each entry if(field->name() == QLatin1String("id") || field->name() == QLatin1String("cdate") || field->name() == QLatin1String("mdate")) { continue; } // myLog() << "reading field: " << field->name(); if(e1->field(field) == e2->field(field)) { continue; } else if(e1->field(field).isEmpty()) { // myLog() << e1->title() << ": updating field(" << field->name() << ") to " << e2->field(field); e1->setField(field, e2->field(field)); ret = true; } else if(field->type() == Data::Field::Table) { // if field F is a table-type field (album tracks, files, etc.), merge rows (keep their position) // if e1's F val in [row i, column j] empty, replace with e2's val at same position // if different (non-empty) vals at same position, CONFLICT! QStringList vals1 = FieldFormat::splitTable(e1->field(field)); QStringList vals2 = FieldFormat::splitTable(e2->field(field)); while(vals1.count() < vals2.count()) { vals1 += QString(); } for(int i = 0; i < vals2.count(); ++i) { if(vals2[i].isEmpty()) { continue; } if(vals1[i].isEmpty()) { vals1[i] = vals2[i]; ret = true; } else { QStringList parts1 = FieldFormat::splitRow(vals1[i]); QStringList parts2 = FieldFormat::splitRow(vals2[i]); bool changedPart = false; while(parts1.count() < parts2.count()) { parts1 += QString(); } for(int j = 0; j < parts2.count(); ++j) { if(parts2[j].isEmpty()) { continue; } if(parts1[j].isEmpty()) { parts1[j] = parts2[j]; changedPart = true; } else if(resolver_ && parts1[j] != parts2[j]) { int resolverResponse = resolver_->resolve(e1, e2, field, parts1[j], parts2[j]); if(resolverResponse == MergeConflictResolver::CancelMerge) { ret = false; return false; // cancel all the merge right now } else if(resolverResponse == MergeConflictResolver::KeepSecond) { parts1[j] = parts2[j]; changedPart = true; } } } if(changedPart) { vals1[i] = parts1.join(FieldFormat::columnDelimiterString()); ret = true; } } } if(ret) { e1->setField(field, vals1.join(FieldFormat::rowDelimiterString())); } // remove the merging due to user comments // maybe in the future have a more intelligent way #if 0 } else if(field->hasFlag(Data::Field::AllowMultiple)) { // if field F allows multiple values and not a Table (see above case), // e1's F values = (e1's F values) U (e2's F values) (union) // replace e1's field with union of e1's and e2's values for this field QStringList items1 = e1->fields(field, false); QStringList items2 = e2->fields(field, false); foreach(const QString& item2, items2) { // possible to have one value formatted and the other one not... if(!items1.contains(item2) && !items1.contains(Field::format(item2, field->formatType()))) { items1.append(item2); } } // not sure if I think it should be sorted or not // items1.sort(); e1->setField(field, items1.join(FieldFormat::delimiterString())); ret = true; #endif } else if(resolver_) { const int resolverResponse = resolver_->resolve(e1, e2, field); if(resolverResponse == MergeConflictResolver::CancelMerge) { ret = false; // we got cancelled return false; // cancel all the merge right now } else if(resolverResponse == MergeConflictResolver::KeepSecond) { e1->setField(field, e2->field(field)); } } else { // myDebug() << "Keeping value of" << field->name() << "for" << e1->field(QStringLiteral("title")); } } return ret; } //static QPair Document::mergeFields(Data::CollPtr coll_, Data::FieldList fields_, Data::EntryList entries_) { Data::FieldList modified, created; foreach(Data::FieldPtr field, fields_) { // don't add a field if it's a default field and not in the current collection if(coll_->hasField(field->name()) || CollectionFactory::isDefaultField(coll_->type(), field->name())) { // special case for choice fields, since we might want to add a value if(field->type() == Data::Field::Choice && coll_->hasField(field->name())) { // a2 are the existing fields in the collection, keep them in the same order QStringList a1 = coll_->fieldByName(field->name())->allowed(); foreach(const QString& newAllowedValue, field->allowed()) { if(!a1.contains(newAllowedValue)) { // could be slow for large merges, but we do only want to add new value // IF that value is actually used by an entry foreach(Data::EntryPtr entry, entries_) { if(entry->field(field->name()) == newAllowedValue) { a1 += newAllowedValue; break; } } } } if(a1.count() != coll_->fieldByName(field->name())->allowed().count()) { Data::FieldPtr f(new Data::Field(*coll_->fieldByName(field->name()))); f->setAllowed(a1); modified.append(f); } } continue; } // add field if any values are not empty foreach(Data::EntryPtr entry, entries_) { if(!entry->field(field).isEmpty()) { created.append(Data::FieldPtr(new Data::Field(*field))); break; } } } return qMakePair(modified, created); } diff --git a/src/entryeditdialog.cpp b/src/entryeditdialog.cpp index f02238fe..b516133d 100644 --- a/src/entryeditdialog.cpp +++ b/src/entryeditdialog.cpp @@ -1,791 +1,791 @@ /*************************************************************************** Copyright (C) 2001-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "entryeditdialog.h" #include "gui/tabwidget.h" #include "collection.h" #include "controller.h" #include "field.h" #include "entry.h" #include "fieldformat.h" #include "tellico_kernel.h" #include "utils/cursorsaver.h" #include "tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { // must be an even number static const int NCOLS = 2; // number of columns of GUI::FieldWidgets static const char* dialogOptionsString = "Edit Dialog Options"; } using Tellico::EntryEditDialog; EntryEditDialog::EntryEditDialog(QWidget* parent_) : QDialog(parent_), m_tabs(new GUI::TabWidget(this)), m_modified(false), m_isOrphan(false), m_isWorking(false), m_needReset(false) { setWindowTitle(i18n("Edit Entry")); QVBoxLayout* mainLayout = new QVBoxLayout(); setLayout(mainLayout); mainLayout->addWidget(m_tabs); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Help| QDialogButtonBox::Close| QDialogButtonBox::Apply); - connect(buttonBox, SIGNAL(helpRequested()), this, SLOT(slotHelp())); + connect(buttonBox, &QDialogButtonBox::helpRequested, this, &EntryEditDialog::slotHelp); mainLayout->addWidget(buttonBox); m_newButton = new QPushButton(); buttonBox->addButton(m_newButton, QDialogButtonBox::ActionRole); m_newButton->setDefault(true); KGuiItem::assign(m_newButton, KGuiItem(i18n("&New Entry"))); m_saveButton = buttonBox->button(QDialogButtonBox::Apply); m_saveButton->setEnabled(false); KGuiItem save = KStandardGuiItem::save(); save.setText(i18n("Sa&ve Entry")); KGuiItem::assign(m_saveButton, save); - connect(buttonBox->button(QDialogButtonBox::Close), SIGNAL(clicked()), SLOT(slotClose())); - connect(m_saveButton, SIGNAL(clicked()), SLOT(slotHandleSave())); - connect(m_newButton, SIGNAL(clicked()), SLOT(slotHandleNew())); + connect(buttonBox->button(QDialogButtonBox::Close), &QAbstractButton::clicked, this, &EntryEditDialog::slotClose); + connect(m_saveButton, &QAbstractButton::clicked, this, &EntryEditDialog::slotHandleSave); + connect(m_newButton, &QAbstractButton::clicked, this, &EntryEditDialog::slotHandleNew); } EntryEditDialog::~EntryEditDialog() { } void EntryEditDialog::reject() { slotClose(); } void EntryEditDialog::slotHelp() { KHelpClient::invokeHelp(QStringLiteral("entry-editor")); } void EntryEditDialog::slotClose() { // check to see if an entry should be saved before hiding // block signals so the entry view and selection isn't cleared if(m_modified && queryModified()) { accept(); // make sure to reset values in the dialog m_needReset = true; setContents(m_currEntries); slotSetModified(false); } else if(!m_modified) { accept(); } } void EntryEditDialog::slotReset() { if(m_isWorking) { return; } slotSetModified(false); m_saveButton->setEnabled(false); m_saveButton->setText(i18n("Sa&ve Entry")); m_currColl = nullptr; m_currEntries.clear(); while(m_tabs->count() > 0) { QWidget* widget = m_tabs->widget(0); m_tabs->removeTab(0); delete widget; } m_widgetDict.clear(); } void EntryEditDialog::resetLayout(Tellico::Data::CollPtr coll_) { if(!coll_ || m_isWorking) { return; } m_newButton->setIcon(QIcon(QLatin1String(":/icons/") + Kernel::self()->collectionTypeName())); setUpdatesEnabled(false); if(m_tabs->count() > 0) { // myDebug() << "resetting contents."; slotReset(); } m_isWorking = true; m_currColl = coll_; int maxHeight = 0; QList gridList; bool noChoices = true; bool focusedFirst = false; QStringList catList = m_currColl->fieldCategories(); for(QStringList::ConstIterator catIt = catList.constBegin(); catIt != catList.constEnd(); ++catIt) { Data::FieldList allCategoryfields = m_currColl->fieldsByCategory(*catIt); Data::FieldList fields; // remove fields which we don't plan to show foreach(Data::FieldPtr field, allCategoryfields) { // uneditabled and fields with derived values don't get widgets if(field->hasFlag(Data::Field::NoEdit) || field->hasFlag(Data::Field::Derived)) { continue; } fields << field; } if(fields.isEmpty()) { // sanity check continue; } // if this layout model is changed, be sure to check slotUpdateField() QWidget* page = new QWidget(m_tabs); QBoxLayout* boxLayout = new QVBoxLayout(page); QWidget* grid = new QWidget(page); gridList.append(grid); // spacing gets a bit weird, if there are absolutely no Choice fields, // then spacing should be 5, which is set later QGridLayout* layout = new QGridLayout(grid); boxLayout->addWidget(grid, 0); // those with multiple, get a stretch if(fields.count() > 1 || !fields[0]->isSingleCategory()) { boxLayout->addStretch(1); } // keep track of which should expand QVector expands(NCOLS, false); QVector maxWidth(NCOLS, 0); int count = 0; foreach(Data::FieldPtr field, fields) { if(field->type() == Data::Field::Choice) { noChoices = false; } GUI::FieldWidget* widget = GUI::FieldWidget::create(field, grid); if(!widget) { continue; } widget->insertDefault(); - connect(widget, SIGNAL(valueChanged(Tellico::Data::FieldPtr)), SLOT(fieldValueChanged(Tellico::Data::FieldPtr))); + connect(widget, &GUI::FieldWidget::valueChanged, this, &EntryEditDialog::fieldValueChanged); if(!focusedFirst && widget->focusPolicy() != Qt::NoFocus) { widget->setFocus(); focusedFirst = true; } int r = count/NCOLS; int c = count%NCOLS; layout->addWidget(widget, r, c); layout->setRowStretch(r, 1); m_widgetDict.insert(QString::number(m_currColl->id()) + field->name(), widget); maxWidth[count%NCOLS] = qMax(maxWidth[count%NCOLS], widget->labelWidth()); if(widget->expands()) { expands[count%NCOLS] = true; } widget->updateGeometry(); if(!field->isSingleCategory()) { maxHeight = qMax(maxHeight, widget->minimumSizeHint().height()); } ++count; } // now, the labels in a column should all be the same width count = 0; foreach(Data::FieldPtr field, fields) { GUI::FieldWidget* widget = m_widgetDict.value(QString::number(m_currColl->id()) + field->name()); if(widget) { widget->setLabelWidth(maxWidth[count%NCOLS]); ++count; } } // update stretch factors for columns with a line edit for(int col = 0; col < NCOLS; ++col) { if(expands[col]) { layout->setColumnStretch(col, 1); } } m_tabs->addTab(page, *catIt); } // Now, go through and set all the field widgets to the same height foreach(QWidget* grid, gridList) { QGridLayout* l = static_cast(grid->layout()); if(noChoices) { l->setSpacing(5); } for(int row = 0; row < l->rowCount() && grid->children().count() > 1; ++row) { l->setRowMinimumHeight(row, maxHeight); } // I don't want anything to be hidden, Keramik has a bug if I don't do this grid->setMinimumHeight(grid->sizeHint().height()); // the parent of the grid is the page that got added to the tabs grid->parentWidget()->layout()->invalidate(); grid->parentWidget()->setMinimumHeight(grid->parentWidget()->sizeHint().height()); // also, no accels for the field widgets KAcceleratorManager::setNoAccel(grid); } setUpdatesEnabled(true); // this doesn't seem to work // setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); // so do this instead // layout()->invalidate(); // needed so the sizeHint() gets recalculated m_tabs->setMinimumHeight(m_tabs->minimumSizeHint().height()); m_tabs->setMinimumWidth(m_tabs->sizeHint().width()); m_tabs->setCurrentIndex(0); m_isWorking = false; slotHandleNew(); } void EntryEditDialog::slotHandleNew() { if(!m_currColl || !queryModified()) { return; } m_tabs->setCurrentIndex(0); m_tabs->setFocusToFirstChild(); clear(); m_isWorking = true; // clear() will get called again if(!signalsBlocked()) { Controller::self()->slotClearSelection(); } m_isWorking = false; Data::EntryPtr entry(new Data::Entry(m_currColl)); m_currEntries.append(entry); m_isOrphan = true; } void EntryEditDialog::slotHandleSave() { if(!m_currColl || m_isWorking) { return; } m_isWorking = true; if(m_currEntries.isEmpty()) { myDebug() << "creating new entry"; m_currEntries.append(Data::EntryPtr(new Data::Entry(m_currColl))); m_isOrphan = true; } // add a message box if multiple items are selected if(m_currEntries.count() > 1) { QStringList names; foreach(Data::EntryPtr entry, m_currEntries) { names += entry->title(); } QString str(i18n("Do you really want to modify these entries?")); QString dontAsk = QStringLiteral("SaveMultipleBooks"); // don't change 'books', invisible anyway int ret = KMessageBox::questionYesNoList(this, str, names, i18n("Modify Multiple Entries"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk); if(ret != KMessageBox::Yes) { m_isWorking = false; return; } } GUI::CursorSaver cs; Data::EntryList oldEntries; Data::FieldList fieldsRequiringValues; // boolean to keep track if any field gets changed bool modified = false; foreach(Data::EntryPtr entry, m_currEntries) { // if the entry is owned, then we're modifying an existing entry, keep a copy of the old one if(entry->isOwned()) { oldEntries.append(Data::EntryPtr(new Data::Entry(*entry))); } foreach(Data::FieldPtr field, m_modifiedFields) { QString key = QString::number(m_currColl->id()) + field->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(widget && widget->isEnabled()) { const QString temp = widget->text(); // ok to set field empty string, just not all of them if(modified == false && entry->field(field) != temp) { modified = true; } entry->setField(field, temp); if(temp.isEmpty()) { const QString prop = field->property(QStringLiteral("required")).toLower(); if(prop == QLatin1String("1") || prop == QLatin1String("true")) { fieldsRequiringValues.append(field); } } } } } if(!fieldsRequiringValues.isEmpty()) { GUI::CursorSaver cs2(Qt::ArrowCursor); QString str = i18n("A value is required for the following fields. Do you want to continue?"); QStringList titles; foreach(Data::FieldPtr it, fieldsRequiringValues) { titles << it->title(); } QString dontAsk = QStringLiteral("SaveWithoutRequired"); int ret = KMessageBox::questionYesNoList(this, str, titles, i18n("Modify Entries"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk); if(ret != KMessageBox::Yes) { m_isWorking = false; return; } } // if something was not empty, signal a save if(modified) { m_isOrphan = false; if(oldEntries.isEmpty()) { Kernel::self()->addEntries(m_currEntries, false); } else { QStringList fieldNames; foreach(Data::FieldPtr field, m_modifiedFields) { fieldNames << field->name(); } Kernel::self()->modifyEntries(oldEntries, m_currEntries, fieldNames); } if(!m_currEntries.isEmpty() && !m_currEntries[0]->title().isEmpty()) { setWindowTitle(i18n("Edit Entry") + QLatin1String(" - ") + m_currEntries[0]->title()); } } m_isWorking = false; slotSetModified(false); // slotHandleNew(); } void EntryEditDialog::clear() { if(m_isWorking) { return; } m_isWorking = true; // clear the widgets foreach(GUI::FieldWidget* widget, m_widgetDict) { widget->setEnabled(true); widget->clear(); widget->insertDefault(); } m_modifiedFields.clear(); setWindowTitle(i18n("Edit Entry")); if(m_isOrphan) { if(m_currEntries.count() > 1) { myWarning() << "is an orphan, but more than one"; } m_isOrphan = false; } m_currEntries.clear(); m_saveButton->setText(i18n("Sa&ve Entry")); m_isWorking = false; slotSetModified(false); } void EntryEditDialog::setContents(Tellico::Data::EntryList entries_) { // this slot might get called if we try to save multiple items, so just return if(m_isWorking) { return; } if(entries_.isEmpty()) { if(queryModified()) { blockSignals(true); slotHandleNew(); blockSignals(false); } return; } // if some entries get selected in one view, then in another, don't reset if(!m_needReset && entries_ == m_currEntries) { return; } m_needReset = false; // first set contents to first item setEntry(entries_.front()); // something weird...if list count can actually be 1 before the setContents call // and 0 after it. Why is that? It's const! if(entries_.count() < 2) { return; } // multiple entries, so don't set caption setWindowTitle(i18n("Edit Entries")); m_currEntries = entries_; m_isWorking = true; blockSignals(true); foreach(Data::FieldPtr fIt, m_currColl->fields()) { QString key = QString::number(m_currColl->id()) + fIt->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(!widget) { // probably read-only continue; } widget->editMultiple(true); QString value = entries_[0]->field(fIt); for(int i = 1; i < entries_.count(); ++i) { // skip checking the first one if(entries_[i]->field(fIt) != value) { widget->setEnabled(false); break; } } } // end field loop blockSignals(false); m_isWorking = false; m_saveButton->setText(i18n("Sa&ve Entries")); } void EntryEditDialog::setEntry(Tellico::Data::EntryPtr entry_) { if(m_isWorking || !queryModified()) { return; } if(!entry_) { myDebug() << "null entry pointer"; slotHandleNew(); return; } // myDebug() << entry_->title(); blockSignals(true); clear(); blockSignals(false); m_isWorking = true; m_currEntries.append(entry_); if(!entry_->title().isEmpty()) { setWindowTitle(i18n("Edit Entry") + QLatin1String(" - ") + entry_->title()); } if(m_currColl != entry_->collection()) { myDebug() << "collections don't match"; m_currColl = entry_->collection(); } foreach(Data::FieldPtr field, m_currColl->fields()) { QString key = QString::number(m_currColl->id()) + field->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(!widget) { // is probably read-only continue; } widget->setText(entry_->field(field)); widget->setEnabled(true); widget->editMultiple(false); } // end field loop if(entry_->isOwned()) { m_saveButton->setText(i18n("Sa&ve Entry")); slotSetModified(false); } else { // saving is necessary for unowned entries slotSetModified(true); } m_isWorking = false; } void EntryEditDialog::removeField(Tellico::Data::CollPtr, Tellico::Data::FieldPtr field_) { if(!field_) { return; } // myDebug() << "name = " << field_->name(); QString key = QString::number(m_currColl->id()) + field_->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(widget) { m_widgetDict.remove(key); // if this is the last field in the category, need to remove the tab page // this function is called after the field has been removed from the collection, // so the category should be gone from the category list if(m_currColl->fieldCategories().indexOf(field_->category()) == -1) { // myDebug() << "last field in the category"; // fragile, widget's parent is the grid, whose parent is the tab page QWidget* w = widget->parentWidget()->parentWidget(); m_tabs->removeTab(m_tabs->indexOf(w)); delete w; // automatically deletes child widget } else { // much of this replicates code in resetLayout() QGridLayout* layout = static_cast(widget->parentWidget()->layout()); delete widget; // automatically removes from layout QVector expands(NCOLS, false); QVector maxWidth(NCOLS, 0); Data::FieldList vec = m_currColl->fieldsByCategory(field_->category()); int count = 0; foreach(Data::FieldPtr field, vec) { GUI::FieldWidget* widget = m_widgetDict.value(QString::number(m_currColl->id()) + field->name()); if(widget) { layout->removeWidget(widget); layout->addWidget(widget, count/NCOLS, count%NCOLS); maxWidth[count%NCOLS] = qMax(maxWidth[count%NCOLS], widget->labelWidth()); if(widget->expands()) { expands[count%NCOLS] = true; } widget->updateGeometry(); ++count; } } // now, the labels in a column should all be the same width count = 0; foreach(Data::FieldPtr field, vec) { GUI::FieldWidget* widget = m_widgetDict.value(QString::number(m_currColl->id()) + field->name()); if(widget) { widget->setLabelWidth(maxWidth[count%NCOLS]); ++count; } } // update stretch factors for columns with a line edit for(int col = 0; col < NCOLS; ++col) { if(expands[col]) { layout->setColumnStretch(col, 1); } } } } } void EntryEditDialog::updateCompletions(Tellico::Data::EntryPtr entry_) { #ifndef NDEBUG if(m_currColl != entry_->collection()) { myDebug() << "inconsistent collection pointers!"; // m_currColl = entry_->collection(); } #endif foreach(Data::FieldPtr f, m_currColl->fields()) { if(f->type() != Data::Field::Line || !f->hasFlag(Data::Field::AllowCompletion)) { continue; } QString key = QString::number(m_currColl->id()) + f->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(!widget) { continue; } if(f->hasFlag(Data::Field::AllowMultiple)) { QStringList items = FieldFormat::splitValue(entry_->field(f)); for(QStringList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) { widget->addCompletionObjectItem(*it); } } else { widget->addCompletionObjectItem(entry_->field(f)); } } } void EntryEditDialog::slotSetModified(bool mod_/*=true*/) { m_modified = mod_; m_saveButton->setEnabled(mod_); } bool EntryEditDialog::queryModified() { bool ok = true; // assume that if the dialog is hidden, we shouldn't ask the user to modify changes if(!isVisible()) { m_modified = false; } if(m_modified) { QString str(i18n("The current entry has been modified.\n" "Do you want to enter the changes?")); KGuiItem item = KStandardGuiItem::save(); item.setText(i18n("Save Entry")); int want_save = KMessageBox::warningYesNoCancel(this, str, i18n("Unsaved Changes"), item, KStandardGuiItem::discard()); switch(want_save) { case KMessageBox::Yes: slotHandleSave(); ok = true; break; case KMessageBox::No: m_modified = false; ok = true; break; case KMessageBox::Cancel: ok = false; break; } } return ok; } void EntryEditDialog::addField(Tellico::Data::CollPtr coll_, Tellico::Data::FieldPtr field_) { Q_ASSERT(coll_ == m_currColl); Q_UNUSED(field_); resetLayout(coll_); } // modified fields will always have the same name void EntryEditDialog::modifyField(Tellico::Data::CollPtr coll_, Tellico::Data::FieldPtr oldField_, Tellico::Data::FieldPtr newField_) { // myDebug() << newField_->name(); if(coll_ != m_currColl) { myDebug() << "wrong collection pointer!"; m_currColl = coll_; } // if the field type changed, go ahead and redo the whole layout // also if the category changed for a non-single field, since a new tab must be created if(oldField_->type() != newField_->type() || (oldField_->category() != newField_->category() && !newField_->isSingleCategory())) { bool modified = m_modified; resetLayout(coll_); setContents(m_currEntries); m_modified = modified; return; } QString key = QString::number(coll_->id()) + oldField_->name(); GUI::FieldWidget* widget = m_widgetDict.value(key); if(widget) { widget->updateField(oldField_, newField_); // need to update label widths if(newField_->title() != oldField_->title()) { int maxWidth = 0; QList childList = widget->parentWidget()->findChildren(); foreach(GUI::FieldWidget* obj, childList) { maxWidth = qMax(maxWidth, obj->labelWidth()); } foreach(GUI::FieldWidget* obj, childList) { obj->setLabelWidth(maxWidth); } } // this is very fragile! // field widgets's parent is the grid, whose parent is the tab page // this is for singleCategory fields if(newField_->category() != oldField_->category()) { int idx = m_tabs->indexOf(widget->parentWidget()->parentWidget()); if(idx > -1) { m_tabs->setTabText(idx, newField_->category()); } } } } void EntryEditDialog::addEntries(Tellico::Data::EntryList entries_) { foreach(Data::EntryPtr entry, entries_) { updateCompletions(entry); } } void EntryEditDialog::modifyEntries(Tellico::Data::EntryList entries_) { bool updateContents = false; foreach(Data::EntryPtr entry, entries_) { updateCompletions(entry); if(!updateContents && m_currEntries.contains(entry)) { updateContents = true; } } if(updateContents) { m_needReset = true; setContents(m_currEntries); } } void EntryEditDialog::fieldValueChanged(Data::FieldPtr field_) { if(!m_modifiedFields.contains(field_)) { m_modifiedFields.append(field_); } slotSetModified(true); } void EntryEditDialog::showEvent(QShowEvent* event_) { QDialog::showEvent(event_); /* I attempted to read and restore window size here, but it didn't work (July 2016) I discovered that I had to put it in a timer. Somewhere, the resize event or something was overriding any size changes I did here. Calling this->resize() would work but windowHandle()->resize() would not (as KWindowConfig::restoreWindowSize uses) */ - QTimer::singleShot(0, this, SLOT(slotUpdateSize())); + QTimer::singleShot(0, this, &EntryEditDialog::slotUpdateSize); } void EntryEditDialog::slotUpdateSize() { KConfigGroup config(KSharedConfig::openConfig(), QLatin1String(dialogOptionsString)); KWindowConfig::restoreWindowSize(windowHandle(), config); } void EntryEditDialog::hideEvent(QHideEvent* event_) { KConfigGroup config(KSharedConfig::openConfig(), QLatin1String(dialogOptionsString)); KWindowConfig::saveWindowSize(windowHandle(), config); config.sync(); if(queryModified()) { QDialog::hideEvent(event_); } else { event_->ignore(); } } void EntryEditDialog::closeEvent(QCloseEvent* event_) { // check to see if an entry should be saved before hiding // block signals so the entry view and selection isn't cleared if(queryModified()) { QDialog::closeEvent(event_); } else { event_->ignore(); } } diff --git a/src/entrymatchdialog.cpp b/src/entrymatchdialog.cpp index bd622166..03045469 100644 --- a/src/entrymatchdialog.cpp +++ b/src/entrymatchdialog.cpp @@ -1,139 +1,139 @@ /*************************************************************************** Copyright (C) 2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "entrymatchdialog.h" #include "entryview.h" #include "entry.h" #include "fetch/fetchmanager.h" #include #include #include #include #include #include #include #include #include #include namespace { static const int DIALOG_MIN_WIDTH = 600; } using namespace Tellico; using Tellico::EntryMatchDialog; EntryMatchDialog::EntryMatchDialog(QWidget* parent_, Data::EntryPtr entryToUpdate_, Fetch::Fetcher::Ptr fetcher_, const EntryUpdater::ResultList& matchResults_) : QDialog(parent_) { Q_ASSERT(entryToUpdate_); Q_ASSERT(fetcher_); setModal(true); setWindowTitle(i18n("Select Match")); QVBoxLayout* mainLayout = new QVBoxLayout(this); setLayout(mainLayout); QWidget* mainWidget = new QWidget(this); mainLayout->addWidget(mainWidget); QWidget* hbox = new QWidget(mainWidget); mainLayout->addWidget(hbox); QHBoxLayout* hboxHBoxLayout = new QHBoxLayout(hbox); hboxHBoxLayout->setMargin(0); hboxHBoxLayout->setSpacing(10); QLabel* icon = new QLabel(hbox); hboxHBoxLayout->addWidget(icon); icon->setPixmap(Fetch::Manager::fetcherIcon(fetcher_, KIconLoader::Panel, 48)); icon->setAlignment(Qt::Alignment(Qt::AlignLeft) | Qt::AlignTop); QString s = i18n("%1 returned multiple results which could match %2, " "the entry currently in the collection. Please select the correct match.", fetcher_->source(), entryToUpdate_->title()); KTextEdit* l = new KTextEdit(hbox); hboxHBoxLayout->addWidget(l); l->setHtml(s); l->setReadOnly(true); l->setMaximumHeight(48); l->setFrameStyle(0); QSplitter* split = new QSplitter(Qt::Vertical, mainWidget); mainLayout->addWidget(split); split->setMinimumHeight(400); m_treeWidget = new QTreeWidget(split); m_treeWidget->setAllColumnsShowFocus(true); m_treeWidget->setSortingEnabled(true); m_treeWidget->setHeaderLabels(QStringList() << i18n("Title") << i18n("Description")); - connect(m_treeWidget, SIGNAL(itemSelectionChanged()), SLOT(slotShowEntry())); + connect(m_treeWidget, &QTreeWidget::itemSelectionChanged, this, &EntryMatchDialog::slotShowEntry); foreach(const EntryUpdater::UpdateResult& res, matchResults_) { Data::EntryPtr matchingEntry = res.first->fetchEntry(); QTreeWidgetItem* item = new QTreeWidgetItem(m_treeWidget, QStringList() << matchingEntry->title() << res.first->desc); m_itemResults.insert(item, res); m_itemEntries.insert(item, matchingEntry); } m_entryView = new EntryView(split); // don't bother creating funky gradient images for compact view m_entryView->setUseGradientImages(false); // set the xslt file AFTER setting the gradient image option m_entryView->setXSLTFile(QStringLiteral("Compact.xsl")); m_entryView->addXSLTStringParam("skip-fields", "id,mdate,cdate"); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton* okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); mainLayout->addWidget(buttonBox); setMinimumWidth(qMax(minimumWidth(), DIALOG_MIN_WIDTH)); // have the entry view be taller than the tree widget split->setStretchFactor(1, 10); } void EntryMatchDialog::slotShowEntry() { QTreeWidgetItem* item = m_treeWidget->currentItem(); if(!item) { return; } m_entryView->showEntry(m_itemEntries[item]); } Tellico::EntryUpdater::UpdateResult EntryMatchDialog::updateResult() const { QTreeWidgetItem* item = m_treeWidget->currentItem(); if(!item) { return EntryUpdater::UpdateResult(nullptr, false); } return m_itemResults[item]; } diff --git a/src/entrymerger.cpp b/src/entrymerger.cpp index 4244a12d..28af7028 100644 --- a/src/entrymerger.cpp +++ b/src/entrymerger.cpp @@ -1,134 +1,134 @@ /*************************************************************************** Copyright (C) 2007-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "entrymerger.h" #include "entry.h" #include "entrycomparison.h" #include "collection.h" #include "tellico_kernel.h" #include "controller.h" #include "progressmanager.h" #include "gui/statusbar.h" #include "tellico_debug.h" #include #include using namespace Tellico; using Tellico::AskUserResolver; using Tellico::EntryMerger; Tellico::MergeConflictResolver::Result AskUserResolver::resolve(Data::EntryPtr entry1, Data::EntryPtr entry2, Data::FieldPtr field, const QString& value1, const QString& value2) { return static_cast(Kernel::self()->askAndMerge(entry1, entry2, field, value1, value2)); } EntryMerger::EntryMerger(Tellico::Data::EntryList entries_, QObject* parent_) : QObject(parent_), m_entriesToCheck(entries_), m_origCount(entries_.count()), m_cancelled(false) , m_resolver(new AskUserResolver) { m_entriesLeft = m_entriesToCheck; Kernel::self()->beginCommandGroup(i18n("Merge Entries")); QString label = i18n("Merging entries..."); ProgressItem& item = ProgressManager::self()->newProgressItem(this, label, true /*canCancel*/); item.setTotalSteps(m_origCount); connect(&item, &Tellico::ProgressItem::signalCancelled, this, &Tellico::EntryMerger::slotCancel); // done if no entries to merge if(m_origCount < 2) { - QTimer::singleShot(500, this, SLOT(slotCleanup())); + QTimer::singleShot(500, this, &EntryMerger::slotCleanup); } else { slotStartNext(); // starts fetching } } EntryMerger::~EntryMerger() { delete m_resolver; } void EntryMerger::slotStartNext() { QString statusMsg = i18n("Total merged/scanned entries: %1/%2", m_entriesToRemove.count(), m_origCount - m_entriesToCheck.count()); StatusBar::self()->setStatus(statusMsg); ProgressManager::self()->setProgress(this, m_origCount - m_entriesToCheck.count()); Data::EntryPtr baseEntry = m_entriesToCheck[0]; for(int i = 1; i < m_entriesToCheck.count(); ++i) { // skip checking against first Data::EntryPtr it = m_entriesToCheck[i]; bool match = cleanMerge(baseEntry, it); if(!match) { int score = baseEntry->collection()->sameEntry(baseEntry, it); match = score >= EntryComparison::ENTRY_GOOD_MATCH; } if(match) { bool merge_ok = Data::Document::mergeEntry(baseEntry, it, m_resolver); if(merge_ok) { m_entriesToRemove.append(it); m_entriesLeft.removeAll(it); } } } m_entriesToCheck.removeAll(baseEntry); if(m_cancelled || m_entriesToCheck.count() < 2) { - QTimer::singleShot(0, this, SLOT(slotCleanup())); + QTimer::singleShot(0, this, &EntryMerger::slotCleanup); } else { - QTimer::singleShot(0, this, SLOT(slotStartNext())); + QTimer::singleShot(0, this, &EntryMerger::slotStartNext); } } void EntryMerger::slotCancel() { m_cancelled = true; } void EntryMerger::slotCleanup() { Kernel::self()->removeEntries(m_entriesToRemove); Controller::self()->slotUpdateSelection(m_entriesLeft); StatusBar::self()->clearStatus(); ProgressManager::self()->setDone(this); Kernel::self()->endCommandGroup(); deleteLater(); } bool EntryMerger::cleanMerge(Tellico::Data::EntryPtr e1, Tellico::Data::EntryPtr e2) const { // figure out if there's a clean merge possible foreach(Data::FieldPtr field, e1->collection()->fields()) { // do not care about id and dates if(field->name() == QLatin1String("id") || field->name() == QLatin1String("cdate") || field->name() == QLatin1String("mdate")) { continue; } QString val1 = e1->field(field); QString val2 = e2->field(field); if(val1 != val2 && !val1.isEmpty() && !val2.isEmpty()) { return false; } } return true; } diff --git a/src/entryupdatejob.cpp b/src/entryupdatejob.cpp index 81f3015e..d6403e84 100644 --- a/src/entryupdatejob.cpp +++ b/src/entryupdatejob.cpp @@ -1,88 +1,88 @@ /*************************************************************************** Copyright (C) 2005-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "entryupdatejob.h" #include "entrycomparison.h" #include "document.h" #include "entry.h" #include "collection.h" #include "tellico_debug.h" #include using namespace Tellico; using Tellico::EntryUpdateJob; EntryUpdateJob::EntryUpdateJob(QObject* parent_, Data::EntryPtr entry_, Fetch::Fetcher::Ptr fetcher_, Mode mode_) : KJob(parent_), m_entry(entry_), m_fetcher(fetcher_), m_mode(mode_), m_bestMatchScore(-1) { setCapabilities(KJob::Killable); - connect(m_fetcher.data(), SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*)), - SLOT(slotResult(Tellico::Fetch::FetchResult*))); - connect(m_fetcher.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher*)), - SLOT(slotDone())); + connect(m_fetcher.data(), &Fetch::Fetcher::signalResultFound, + this, &EntryUpdateJob::slotResult); + connect(m_fetcher.data(), &Fetch::Fetcher::signalDone, + this, &EntryUpdateJob::slotDone); } void EntryUpdateJob::start() { - QTimer::singleShot(0, this, SLOT(startUpdate())); + QTimer::singleShot(0, this, &EntryUpdateJob::startUpdate); } void EntryUpdateJob::startUpdate() { m_fetcher->startUpdate(m_entry); } void EntryUpdateJob::slotResult(Tellico::Fetch::FetchResult* result_) { if(!result_) { myDebug() << "null result"; return; } Data::EntryPtr entry = result_->fetchEntry(); Q_ASSERT(entry); const int match = m_entry->collection()->sameEntry(m_entry, entry); if(match > m_bestMatchScore) { m_bestMatchScore = match; m_bestMatchEntry = entry; } // if perfect match, go ahead and top if(match > EntryComparison::ENTRY_PERFECT_MATCH) { doKill(); } } void EntryUpdateJob::slotDone() { if(m_bestMatchEntry) { const int matchToBeat = (m_mode == PerfectMatchOnly ? EntryComparison::ENTRY_PERFECT_MATCH : EntryComparison::ENTRY_GOOD_MATCH); if(m_bestMatchScore > matchToBeat) { Data::Document::mergeEntry(m_entry, m_bestMatchEntry); } } emitResult(); } bool EntryUpdateJob::doKill() { m_fetcher->stop(); return true; } diff --git a/src/entryview.cpp b/src/entryview.cpp index f798daf4..198afa97 100644 --- a/src/entryview.cpp +++ b/src/entryview.cpp @@ -1,419 +1,421 @@ /*************************************************************************** Copyright (C) 2003-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "entryview.h" #include "entry.h" #include "field.h" #include "translators/xslthandler.h" #include "translators/tellicoxmlexporter.h" #include "collection.h" #include "images/imagefactory.h" #include "images/imageinfo.h" #include "tellico_kernel.h" #include "utils/tellico_utils.h" #include "utils/datafileregistry.h" #include "core/filehandler.h" #include "config/tellico_config.h" #include "gui/drophandler.h" #include "document.h" #include "tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include using Tellico::EntryView; using Tellico::EntryViewWidget; EntryViewWidget::EntryViewWidget(EntryView* part, QWidget* parent) : KHTMLView(part, parent) {} // for the life of me, I could not figure out how to call the actual // KHTMLPartBrowserExtension::copy() slot, so this will have to do void EntryViewWidget::copy() { QApplication::clipboard()->setText(part()->selectedText(), QClipboard::Clipboard); } void EntryViewWidget::changeEvent(QEvent* event_) { // this will delete and reread the default colors, assuming they changed if(event_->type() == QEvent::PaletteChange || event_->type() == QEvent::FontChange || event_->type() == QEvent::ApplicationFontChange) { static_cast(part())->resetView(); } KHTMLView::changeEvent(event_); } EntryView::EntryView(QWidget* parent_) : KHTMLPart(new EntryViewWidget(this, parent_), parent_), m_handler(nullptr), m_tempFile(nullptr), m_useGradientImages(true), m_checkCommonFile(true) { setJScriptEnabled(false); setJavaEnabled(false); setMetaRefreshEnabled(false); setPluginsEnabled(false); clear(); // needed for initial layout view()->setAcceptDrops(true); DropHandler* drophandler = new DropHandler(this); view()->installEventFilter(drophandler); - connect(browserExtension(), SIGNAL(openUrlRequestDelayed(const QUrl&, const KParts::OpenUrlArguments&, const KParts::BrowserArguments&)), - SLOT(slotOpenURL(const QUrl&))); + connect(browserExtension(), &KParts::BrowserExtension::openUrlRequestDelayed, + this, &EntryView::slotOpenURL); } EntryView::~EntryView() { delete m_handler; m_handler = nullptr; delete m_tempFile; m_tempFile = nullptr; } void EntryView::clear() { m_entry = nullptr; // just clear the view begin(); if(!m_textToShow.isEmpty()) { write(m_textToShow); } end(); view()->layout(); // I need this because some of the margins and widths may get messed up } void EntryView::showEntries(Tellico::Data::EntryList entries_) { if(!entries_.isEmpty()) { showEntry(entries_.first()); } } void EntryView::showEntry(Tellico::Data::EntryPtr entry_) { if(!entry_) { clear(); return; } m_textToShow.clear(); #if 0 myWarning() << "turn me off!"; m_entry = 0; setXSLTFile(m_xsltFile); #endif if(!m_handler || !m_handler->isValid()) { setXSLTFile(m_xsltFile); } if(!m_handler || !m_handler->isValid()) { myWarning() << "no xslt handler"; return; } m_entry = entry_; // by setting the xslt file as the URL, any images referenced in the xslt "theme" can be found // by simply using a relative path in the xslt file QUrl u = QUrl::fromLocalFile(m_xsltFile); begin(u); Export::TellicoXMLExporter exporter(entry_->collection()); exporter.setEntries(Data::EntryList() << entry_); long opt = exporter.options(); // verify images for the view opt |= Export::ExportVerifyImages; // on second thought, don't auto-format everything, just clean it // if(Data::Field::autoFormat()) { // opt = Export::ExportFormatted; // } if(entry_->collection()->type() == Data::Collection::Bibtex) { opt |= Export::ExportClean; } exporter.setOptions(opt); QDomDocument dom = exporter.exportXML(); // myDebug() << dom.toString(); #if 0 myWarning() << "turn me off!"; QFile f1(QLatin1String("/tmp/test.xml")); if(f1.open(QIODevice::WriteOnly)) { QTextStream t(&f1); t << dom.toString(); } f1.close(); #endif QString html = m_handler->applyStylesheet(dom.toString()); // write out image files Data::FieldList fields = entry_->collection()->imageFields(); foreach(Data::FieldPtr field, fields) { QString id = entry_->field(field); if(id.isEmpty()) { continue; } // only write out image if it's not linked only if(!ImageFactory::imageInfo(id).linkOnly) { if(Data::Document::self()->allImagesOnDisk()) { ImageFactory::writeCachedImage(id, ImageFactory::cacheDir()); } else { ImageFactory::writeCachedImage(id, ImageFactory::TempDir); } } } #if 0 myWarning() << "turn me off!"; QFile f2(QLatin1String("/tmp/test.html")); if(f2.open(QIODevice::WriteOnly)) { QTextStream t(&f2); t << html; } f2.close(); #endif // myDebug() << html; write(html); end(); // not need anymore? view()->layout(); // I need this because some of the margins and widths may get messed up } void EntryView::showText(const QString& text_) { m_textToShow = text_; begin(); write(text_); end(); } void EntryView::setXSLTFile(const QString& file_) { if(file_.isEmpty()) { myWarning() << "empty xslt file"; return; } QString oldFile = m_xsltFile; // if starts with slash, then absolute path if(file_.at(0) == QLatin1Char('/')) { m_xsltFile = file_; } else { const QString templateDir = QStringLiteral("entry-templates/"); m_xsltFile = DataFileRegistry::self()->locate(templateDir + file_); if(m_xsltFile.isEmpty()) { if(!file_.isEmpty()) { myWarning() << "can't locate" << file_; } m_xsltFile = DataFileRegistry::self()->locate(templateDir + QLatin1String("Fancy.xsl")); if(m_xsltFile.isEmpty()) { QString str = QStringLiteral(""); str += i18n("Tellico is unable to locate the default entry stylesheet."); str += QLatin1Char(' '); str += i18n("Please check your installation."); str += QLatin1String(""); KMessageBox::error(view(), str); clear(); return; } } } const int type = m_entry ? m_entry->collection()->type() : Kernel::self()->collectionType(); // we need to know if the colors changed from last time, in case // we need to do that ugly hack to reload the cache bool reloadImages = m_useGradientImages; // if m_useGradientImages is false, then we don't even need to check // if there's no handler, there there's _no way_ to check if(m_handler && reloadImages) { // the only two colors that matter for the gradients are the base color // and highlight base color QByteArray oldBase = m_handler->param("bgcolor"); QByteArray oldHigh = m_handler->param("color2"); // remember the string params have apostrophes on either side, so we can start search at pos == 1 reloadImages = oldBase.indexOf(Config::templateBaseColor(type).name().toLatin1(), 1) == -1 || oldHigh.indexOf(Config::templateHighlightedBaseColor(type).name().toLatin1(), 1) == -1; } if(!m_handler || m_xsltFile != oldFile) { delete m_handler; // must read the file name to get proper context m_handler = new XSLTHandler(QFile::encodeName(m_xsltFile)); if(m_checkCommonFile && !m_handler->isValid()) { Tellico::checkCommonXSLFile(); m_checkCommonFile = false; delete m_handler; m_handler = new XSLTHandler(QFile::encodeName(m_xsltFile)); } if(!m_handler->isValid()) { myWarning() << "invalid xslt handler"; clear(); delete m_handler; m_handler = nullptr; return; } } m_handler->addStringParam("font", Config::templateFont(type).family().toLatin1()); m_handler->addStringParam("fontsize", QByteArray().setNum(Config::templateFont(type).pointSize())); m_handler->addStringParam("bgcolor", Config::templateBaseColor(type).name().toLatin1()); m_handler->addStringParam("fgcolor", Config::templateTextColor(type).name().toLatin1()); m_handler->addStringParam("color1", Config::templateHighlightedTextColor(type).name().toLatin1()); m_handler->addStringParam("color2", Config::templateHighlightedBaseColor(type).name().toLatin1()); if(Data::Document::self()->allImagesOnDisk()) { m_handler->addStringParam("imgdir", QFile::encodeName(ImageFactory::imageDir())); } else { m_handler->addStringParam("imgdir", QFile::encodeName(ImageFactory::tempDir())); } m_handler->addStringParam("datadir", QFile::encodeName(Tellico::installationDir())); // if we don't have to reload the images, then just show the entry and we're done if(reloadImages) { // now, have to recreate images and refresh khtml cache resetColors(); } else { showEntry(m_entry); } } void EntryView::slotRefresh() { setXSLTFile(m_xsltFile); showEntry(m_entry); view()->repaint(); } // do some contortions in case the url is relative // need to interpret it relative to document URL instead of xslt file // the current node under the mouse would be the text node inside // the anchor node, so iterate up the parents void EntryView::slotOpenURL(const QUrl& url_) { if(url_.scheme() == QLatin1String("tc")) { // handle this internally emit signalAction(url_); return; } QUrl u = url_; for(DOM::Node node = nodeUnderMouse(); !node.isNull(); node = node.parentNode()) { if(node.nodeType() == DOM::Node::ELEMENT_NODE && static_cast(node).tagName() == "a") { QString href = static_cast(node).getAttribute("href").string(); if(!href.isEmpty()) { // interpret url relative to document url u = Kernel::self()->URL().resolved(QUrl(href)); } break; } } // open the url QDesktopServices::openUrl(u); } void EntryView::slotReloadEntry() { // this slot should only be connected in setXSLTFile() // must disconnect the signal first, otherwise, get an infinite loop - disconnect(SIGNAL(completed())); + void (EntryView::* completed)() = &EntryView::completed; + disconnect(this, completed, this, &EntryView::slotReloadEntry); closeUrl(); // this is needed to stop everything, for some reason view()->setUpdatesEnabled(true); if(m_entry) { showEntry(m_entry); } else { // setXSLTFile() writes some html to clear the image cache // but we don't want to see that, so just clear everything clear(); } delete m_tempFile; m_tempFile = nullptr; } void EntryView::addXSLTStringParam(const QByteArray& name_, const QByteArray& value_) { if(!m_handler) { return; } m_handler->addStringParam(name_, value_); } void EntryView::setXSLTOptions(const Tellico::StyleOptions& opt_) { if(!m_handler) { return; } m_handler->addStringParam("font", opt_.fontFamily.toLatin1()); m_handler->addStringParam("fontsize", QByteArray().setNum(opt_.fontSize)); m_handler->addStringParam("bgcolor", opt_.baseColor.name().toLatin1()); m_handler->addStringParam("fgcolor", opt_.textColor.name().toLatin1()); m_handler->addStringParam("color1", opt_.highlightedTextColor.name().toLatin1()); m_handler->addStringParam("color2", opt_.highlightedBaseColor.name().toLatin1()); m_handler->addStringParam("imgdir", QFile::encodeName(opt_.imgDir)); } void EntryView::resetView() { delete m_handler; m_handler = nullptr; setXSLTFile(m_xsltFile); // this ends up calling resetColors() } void EntryView::resetColors() { // recreate gradients ImageFactory::createStyleImages(m_entry ? m_entry->collection()->type() : Data::Collection::Base); QString dir = m_handler ? QFile::decodeName(m_handler->param("imgdir")) : QString(); if(dir.isEmpty()) { dir = Data::Document::self()->allImagesOnDisk() ? ImageFactory::imageDir() : ImageFactory::tempDir(); } else { // it's a string param, so it has quotes on both sides dir = dir.mid(1); dir.truncate(dir.length()-1); } // this is a rather bad hack to get around the fact that the image cache is not reloaded when // the gradient files are changed on disk. Setting the URLArgs for write() calls doesn't seem to // work. So force a reload with a temp file, then catch the completed signal and repaint QString s = QStringLiteral("") .arg(dir + QLatin1String("gradient_bg.png"), dir + QLatin1String("gradient_header.png")); delete m_tempFile; m_tempFile = new QTemporaryFile(); if(!m_tempFile->open()) { myDebug() << "failed to open temp file"; delete m_tempFile; m_tempFile = nullptr; return; } QTextStream stream(m_tempFile); stream << s; stream.flush(); KParts::OpenUrlArguments args = arguments(); args.setReload(true); // tell the cache to reload images setArguments(args); // don't flicker view()->setUpdatesEnabled(false); openUrl(QUrl::fromLocalFile(m_tempFile->fileName())); - connect(this, SIGNAL(completed()), SLOT(slotReloadEntry())); + void (EntryView::* completed)() = &EntryView::completed; + connect(this, completed, this, &EntryView::slotReloadEntry); } diff --git a/src/exportdialog.cpp b/src/exportdialog.cpp index 287adec3..d1bf76f3 100644 --- a/src/exportdialog.cpp +++ b/src/exportdialog.cpp @@ -1,306 +1,306 @@ /*************************************************************************** Copyright (C) 2003-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "exportdialog.h" #include "collection.h" #include "core/filehandler.h" #include "controller.h" #include "tellico_debug.h" #include "translators/exporter.h" #include "translators/tellicoxmlexporter.h" #include "translators/tellicozipexporter.h" #include "translators/htmlexporter.h" #include "translators/csvexporter.h" #include "translators/bibtexexporter.h" #include "translators/bibtexmlexporter.h" #include "translators/xsltexporter.h" #include "translators/alexandriaexporter.h" #include "translators/onixexporter.h" #include "translators/gcstarexporter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Tellico; using Tellico::ExportDialog; ExportDialog::ExportDialog(Tellico::Export::Format format_, Tellico::Data::CollPtr coll_, QWidget* parent_) : QDialog(parent_), m_format(format_), m_coll(coll_), m_exporter(exporter(format_, coll_)) { setModal(true); setWindowTitle(i18n("Export Options")); QWidget* widget = new QWidget(this); QVBoxLayout* topLayout = new QVBoxLayout(widget); setLayout(topLayout); topLayout->addWidget(widget); QGroupBox* group1 = new QGroupBox(i18n("Formatting"), widget); topLayout->addWidget(group1, 0); QVBoxLayout* vlay = new QVBoxLayout(group1); m_formatFields = new QCheckBox(i18n("Format all fields"), group1); m_formatFields->setChecked(false); m_formatFields->setWhatsThis(i18n("If checked, the values of the fields will be " "automatically formatted according to their format type.")); vlay->addWidget(m_formatFields); m_exportSelected = new QCheckBox(i18n("Export selected entries only"), group1); m_exportSelected->setChecked(false); m_exportSelected->setWhatsThis(i18n("If checked, only the currently selected entries will " "be exported.")); vlay->addWidget(m_exportSelected); m_exportFields = new QCheckBox(i18n("Export visible fields only"), group1); m_exportFields->setChecked(false); m_exportFields->setWhatsThis(i18n("If checked, only the fields currently visible in the view will " "be exported.")); vlay->addWidget(m_exportFields); QGroupBox* group2 = new QGroupBox(i18n("Encoding"), widget); topLayout->addWidget(group2, 0); QVBoxLayout* vlay2 = new QVBoxLayout(group2); m_encodeUTF8 = new QRadioButton(i18n("Encode in Unicode (UTF-8)"), group2); m_encodeUTF8->setChecked(true); m_encodeUTF8->setWhatsThis(i18n("Encode the exported file in Unicode (UTF-8).")); vlay2->addWidget(m_encodeUTF8); QString localStr = i18n("Encode in user locale (%1)", QLatin1String(QTextCodec::codecForLocale()->name())); m_encodeLocale = new QRadioButton(localStr, group2); m_encodeLocale->setWhatsThis(i18n("Encode the exported file in the local encoding.")); vlay2->addWidget(m_encodeLocale); if(QTextCodec::codecForLocale()->name() == "UTF-8") { m_encodeUTF8->setEnabled(false); m_encodeLocale->setChecked(true); } QButtonGroup* bg = new QButtonGroup(widget); bg->addButton(m_encodeUTF8); bg->addButton(m_encodeLocale); QWidget* w = m_exporter->widget(widget); if(w) { w->layout()->setMargin(0); topLayout->addWidget(w, 0); } topLayout->addStretch(); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton* okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(okButton, SIGNAL(clicked()), SLOT(slotSaveOptions())); - connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + connect(okButton, &QAbstractButton::clicked, this, &ExportDialog::slotSaveOptions); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); topLayout->addWidget(buttonBox); readOptions(); if(format_ == Export::Alexandria) { // no encoding options enabled group2->setEnabled(false); } } ExportDialog::~ExportDialog() { delete m_exporter; m_exporter = nullptr; } QString ExportDialog::fileFilter() { return m_exporter ? m_exporter->fileFilter() : QString(); } void ExportDialog::readOptions() { KConfigGroup config(KSharedConfig::openConfig(), "ExportOptions"); bool format = config.readEntry("FormatFields", false); m_formatFields->setChecked(format); bool selected = config.readEntry("ExportSelectedOnly", false); m_exportSelected->setChecked(selected); bool encode = config.readEntry("EncodeUTF8", true); if(encode && m_encodeUTF8->isEnabled()) { m_encodeUTF8->setChecked(true); } else { m_encodeLocale->setChecked(true); } } void ExportDialog::slotSaveOptions() { KSharedConfigPtr config = KSharedConfig::openConfig(); // each exporter sets its own group m_exporter->saveOptions(config); KConfigGroup configGroup(config, "ExportOptions"); configGroup.writeEntry("FormatFields", m_formatFields->isChecked()); configGroup.writeEntry("ExportSelectedOnly", m_exportSelected->isChecked()); configGroup.writeEntry("EncodeUTF8", m_encodeUTF8->isChecked()); } // static Tellico::Export::Exporter* ExportDialog::exporter(Tellico::Export::Format format_, Data::CollPtr coll_) { Export::Exporter* exporter = nullptr; switch(format_) { case Export::TellicoXML: exporter = new Export::TellicoXMLExporter(coll_); break; case Export::TellicoZip: exporter = new Export::TellicoZipExporter(coll_); break; case Export::HTML: { Export::HTMLExporter* htmlExp = new Export::HTMLExporter(coll_); htmlExp->setGroupBy(Controller::self()->expandedGroupBy()); htmlExp->setSortTitles(Controller::self()->sortTitles()); htmlExp->setColumns(Controller::self()->visibleColumns()); exporter = htmlExp; } break; case Export::CSV: exporter = new Export::CSVExporter(coll_); break; case Export::Bibtex: exporter = new Export::BibtexExporter(coll_); break; case Export::Bibtexml: exporter = new Export::BibtexmlExporter(coll_); break; case Export::XSLT: exporter = new Export::XSLTExporter(coll_); break; case Export::Alexandria: exporter = new Export::AlexandriaExporter(coll_); break; case Export::ONIX: exporter = new Export::ONIXExporter(coll_); break; case Export::GCstar: exporter = new Export::GCstarExporter(coll_); break; default: myDebug() << "not implemented!"; break; } if(exporter) { exporter->readOptions(KSharedConfig::openConfig()); } return exporter; } bool ExportDialog::exportURL(const QUrl& url_/*=QUrl()*/) const { if(!m_exporter) { return false; } // exporter might need to know final URL, say for writing images or something m_exporter->setURL(url_); if(m_exportSelected->isChecked()) { m_exporter->setEntries(Controller::self()->selectedEntries()); } else { m_exporter->setEntries(m_coll->entries()); } if(m_exportFields->isChecked()) { Data::FieldList fields; foreach(const QString& title, Controller::self()->visibleColumns()) { Data::FieldPtr field = m_coll->fieldByTitle(title); if(field) { fields << field; } } m_exporter->setFields(fields); } else { m_exporter->setFields(m_coll->fields()); } long opt = Export::ExportImages | Export::ExportComplete | Export::ExportProgress; // for now, always export images if(m_formatFields->isChecked()) { opt |= Export::ExportFormatted; } if(m_encodeUTF8->isChecked()) { opt |= Export::ExportUTF8; } // since we already asked about overwriting the file, force the save opt |= Export::ExportForce; m_exporter->setOptions(opt); return m_exporter->exec(); } // static // alexandria is exported to known directory // all others are files Tellico::Export::Target ExportDialog::exportTarget(Tellico::Export::Format format_) { switch(format_) { case Export::Alexandria: return Export::None; default: return Export::File; } } // static bool ExportDialog::exportCollection(Data::CollPtr coll_, Data::EntryList entries_, Export::Format format_, const QUrl& url_) { QScopedPointer exp(exporter(format_, coll_)); exp->setURL(url_); exp->setEntries(entries_); KConfigGroup config(KSharedConfig::openConfig(), "ExportOptions"); long options = 0; if(config.readEntry("FormatFields", false)) { options |= Export::ExportFormatted; } if(config.readEntry("EncodeUTF8", true)) { options |= Export::ExportUTF8; } exp->setOptions(options | Export::ExportForce); return exp->exec(); } diff --git a/src/fetch/allocinefetcher.cpp b/src/fetch/allocinefetcher.cpp index 5762b2f8..4ddc3047 100644 --- a/src/fetch/allocinefetcher.cpp +++ b/src/fetch/allocinefetcher.cpp @@ -1,502 +1,503 @@ /*************************************************************************** Copyright (C) 2012-2013 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include // for TELLICO_VERSION #include "allocinefetcher.h" #include "../collections/videocollection.h" #include "../images/imagefactory.h" #include "../entry.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../core/filehandler.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const char* ALLOCINE_API_KEY = "100043982026"; static const char* ALLOCINE_API_URL = "http://api.allocine.fr/rest/v3/"; static const char* ALLOCINE_PARTNER_KEY = "29d185d98c984a359e6e6f26a0474269"; } using namespace Tellico; using Tellico::Fetch::AbstractAllocineFetcher; using Tellico::Fetch::AllocineFetcher; AbstractAllocineFetcher::AbstractAllocineFetcher(QObject* parent_, const QString& baseUrl_) : Fetcher(parent_) , m_started(false) , m_apiKey(QLatin1String(ALLOCINE_API_KEY)) , m_baseUrl(baseUrl_) , m_numCast(10) { Q_ASSERT(!m_baseUrl.isEmpty()); } AbstractAllocineFetcher::~AbstractAllocineFetcher() { } bool AbstractAllocineFetcher::canSearch(FetchKey k) const { return k == Keyword; } bool AbstractAllocineFetcher::canFetch(int type) const { return type == Data::Collection::Video; } void AbstractAllocineFetcher::readConfigHook(const KConfigGroup& config_) { QString k = config_.readEntry("API Key", ALLOCINE_API_KEY); if(!k.isEmpty()) { m_apiKey = k; } m_numCast = config_.readEntry("Max Cast", 10); } void AbstractAllocineFetcher::search() { m_started = true; QUrl u(m_baseUrl); u = u.adjusted(QUrl::StripTrailingSlash); u.setPath(u.path() + QLatin1Char('/') + QStringLiteral("search")); // myDebug() << u; // the order of the parameters appears to matter QList > params; params.append(qMakePair(QStringLiteral("partner"), m_apiKey)); // I can't figure out how to encode accent marks, but they don't // seem to be necessary QString q = removeAccents(request().value); // should I just remove all non alphabetical characters? // see https://bugs.kde.org/show_bug.cgi?id=337432 q.remove(QRegExp(QStringLiteral("[,:!?;\\(\\)]"))); q.replace(QLatin1Char('\''), QLatin1Char('+')); q.replace(QLatin1Char(' '), QLatin1Char('+')); switch(request().key) { case Keyword: params.append(qMakePair(QStringLiteral("q"), q)); break; default: myWarning() << "key not recognized: " << request().key; return; } params.append(qMakePair(QStringLiteral("format"), QStringLiteral("json"))); params.append(qMakePair(QStringLiteral("filter"), QStringLiteral("movie"))); const QString sed = QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd")); params.append(qMakePair(QStringLiteral("sed"), sed)); const QByteArray sig = calculateSignature(params); QUrlQuery query; query.setQueryItems(params); query.addQueryItem(QStringLiteral("sig"), QLatin1String(sig)); u.setQuery(query); // myDebug() << u; m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); // 10/8/17: UserAgent appears necessary to receive data m_job->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("Tellico/%1") .arg(QStringLiteral(TELLICO_VERSION))); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &AbstractAllocineFetcher::slotComplete); } void AbstractAllocineFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); } m_started = false; emit signalDone(this); } Tellico::Data::EntryPtr AbstractAllocineFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries.value(uid_); if(!entry) { myWarning() << "no entry in dict"; return Data::EntryPtr(); } QString code = entry->field(QStringLiteral("allocine-code")); if(code.isEmpty()) { // could mean we already updated the entry myDebug() << "no allocine release found"; return entry; } QUrl u(m_baseUrl); u = u.adjusted(QUrl::StripTrailingSlash); u.setPath(u.path() + QLatin1Char('/') + QStringLiteral("movie")); // the order of the parameters appears to matter QList > params; params.append(qMakePair(QStringLiteral("partner"), m_apiKey)); params.append(qMakePair(QStringLiteral("code"), code)); params.append(qMakePair(QStringLiteral("profile"), QStringLiteral("large"))); params.append(qMakePair(QStringLiteral("filter"), QStringLiteral("movie"))); params.append(qMakePair(QStringLiteral("format"), QStringLiteral("json"))); const QString sed = QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd")); params.append(qMakePair(QStringLiteral("sed"), sed)); const QByteArray sig = calculateSignature(params); QUrlQuery query; query.setQueryItems(params); query.addQueryItem(QStringLiteral("sig"), QLatin1String(sig)); u.setQuery(query); // myDebug() << "url: " << u; // 10/8/17: UserAgent appears necessary to receive data // QByteArray data = FileHandler::readDataFile(u, true); KIO::StoredTransferJob* dataJob = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); dataJob->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("Tellico/%1") .arg(QStringLiteral(TELLICO_VERSION))); if(!dataJob->exec()) { myDebug() << "Failed to load" << u; return entry; } const QByteArray data = dataJob->data(); #if 0 myWarning() << "Remove debug2 from allocinefetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test2.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); QVariantMap result = doc.object().toVariantMap().value(QStringLiteral("movie")).toMap(); if(error.error != QJsonParseError::NoError) { myDebug() << "Bad JSON results"; #if 0 myWarning() << "Remove debug3 from allocinefetcher.cpp"; QFile f2(QString::fromLatin1("/tmp/test3.json")); if(f2.open(QIODevice::WriteOnly)) { QTextStream t(&f2); t.setCodec("UTF-8"); t << data; } f2.close(); #endif return entry; } populateEntry(entry, result); // image might still be a URL const QString image_id = entry->field(QStringLiteral("cover")); if(image_id.contains(QLatin1Char('/'))) { const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */); if(id.isEmpty()) { message(i18n("The cover image could not be loaded."), MessageHandler::Warning); } // empty image ID is ok entry->setField(QStringLiteral("cover"), id); } // don't want to include id entry->collection()->removeField(QStringLiteral("allocine-code")); QStringList castRows = FieldFormat::splitTable(entry->field(QStringLiteral("cast"))); while(castRows.count() > m_numCast) { castRows.removeLast(); } entry->setField(QStringLiteral("cast"), castRows.join(FieldFormat::rowDelimiterString())); return entry; } void AbstractAllocineFetcher::slotComplete(KJob*) { if(m_job->error()) { myDebug() << "Error:" << m_job->errorString(); m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from allocinefetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); QVariantMap result = doc.object().toVariantMap().value(QStringLiteral("feed")).toMap(); // myDebug() << "total:" << result.value(QLatin1String("totalResults")); QVariantList resultList = result.value(QStringLiteral("movie")).toList(); if(resultList.isEmpty()) { myDebug() << "no results"; stop(); return; } foreach(const QVariant& result, resultList) { // myDebug() << "found result:" << result; //create a new collection for every result since we end up removing the allocine code field // when fetchEntryHook is called. See bug 338389 Data::EntryPtr entry(new Data::Entry(createCollection())); populateEntry(entry, result.toMap()); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } m_hasMoreResults = false; stop(); } Tellico::Data::CollPtr AbstractAllocineFetcher::createCollection() const { Data::CollPtr coll(new Data::VideoCollection(true)); // always add the allocine release code for fetchEntryHook Data::FieldPtr field(new Data::Field(QStringLiteral("allocine-code"), QStringLiteral("Allocine Code"), Data::Field::Number)); field->setCategory(i18n("General")); coll->addField(field); // add new fields if(optionalFields().contains(QStringLiteral("allocine"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("allocine"), i18n("Allocine Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); } if(optionalFields().contains(QStringLiteral("origtitle"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); f->setFormatType(FieldFormat::FormatTitle); coll->addField(f); } return coll; } void AbstractAllocineFetcher::populateEntry(Data::EntryPtr entry, const QVariantMap& resultMap) { if(entry->collection()->hasField(QStringLiteral("allocine-code"))) { entry->setField(QStringLiteral("allocine-code"), mapValue(resultMap, "code")); } entry->setField(QStringLiteral("title"), mapValue(resultMap, "title")); if(optionalFields().contains(QStringLiteral("origtitle"))) { entry->setField(QStringLiteral("origtitle"), mapValue(resultMap, "originalTitle")); } if(entry->title().isEmpty()) { entry->setField(QStringLiteral("title"), mapValue(resultMap, "originalTitle")); } entry->setField(QStringLiteral("year"), mapValue(resultMap, "productionYear")); entry->setField(QStringLiteral("plot"), mapValue(resultMap, "synopsis")); const int runTime = mapValue(resultMap, "runtime").toInt(); entry->setField(QStringLiteral("running-time"), QString::number(runTime/60)); const QVariantList castList = resultMap.value(QStringLiteral("castMember")).toList(); QStringList actors, directors, producers, composers; foreach(const QVariant& castVariant, castList) { const QVariantMap castMap = castVariant.toMap(); const int code = mapValue(castMap, "activity", "code").toInt(); switch(code) { case 8001: actors << (mapValue(castMap, "person", "name") + FieldFormat::columnDelimiterString() + mapValue(castMap, "role")); break; case 8002: directors << mapValue(castMap, "person", "name"); break; case 8029: producers << mapValue(castMap, "person", "name"); break; case 8003: composers << mapValue(castMap, "person", "name"); break; } } entry->setField(QStringLiteral("cast"), actors.join(FieldFormat::rowDelimiterString())); entry->setField(QStringLiteral("director"), directors.join(FieldFormat::delimiterString())); entry->setField(QStringLiteral("producer"), producers.join(FieldFormat::delimiterString())); entry->setField(QStringLiteral("composer"), composers.join(FieldFormat::delimiterString())); const QVariantMap releaseMap = resultMap.value(QStringLiteral("release")).toMap(); entry->setField(QStringLiteral("studio"), mapValue(releaseMap, "distributor", "name")); QStringList genres; foreach(const QVariant& variant, resultMap.value(QLatin1String("genre")).toList()) { genres << i18n(mapValue(variant.toMap(), "$").toUtf8().constData()); } entry->setField(QStringLiteral("genre"), genres.join(FieldFormat::delimiterString())); QStringList nats; foreach(const QVariant& variant, resultMap.value(QLatin1String("nationality")).toList()) { nats << mapValue(variant.toMap(), "$"); } entry->setField(QStringLiteral("nationality"), nats.join(FieldFormat::delimiterString())); QStringList langs; foreach(const QVariant& variant, resultMap.value(QLatin1String("language")).toList()) { langs << mapValue(variant.toMap(), "$"); } entry->setField(QStringLiteral("language"), langs.join(FieldFormat::delimiterString())); const QVariantMap colorMap = resultMap.value(QLatin1String("color")).toMap(); if(colorMap.value(QStringLiteral("code")) == QLatin1String("12001")) { entry->setField(QStringLiteral("color"), i18n("Color")); } entry->setField(QStringLiteral("cover"), mapValue(resultMap, "poster", "href")); if(optionalFields().contains(QStringLiteral("allocine"))) { entry->setField(QStringLiteral("allocine"), mapValue(resultMap, "link", "href")); } } Tellico::Fetch::FetchRequest AbstractAllocineFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Keyword, title); } return FetchRequest(); } AbstractAllocineFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AbstractAllocineFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* label = new QLabel(i18n("&Maximum cast: "), optionsWidget()); l->addWidget(label, ++row, 0); m_numCast = new QSpinBox(optionsWidget()); m_numCast->setMaximum(99); m_numCast->setMinimum(0); m_numCast->setValue(10); - connect(m_numCast, SIGNAL(valueChanged(QString)), SLOT(slotSetModified())); + void (QSpinBox::* valueChanged)(const QString&) = &QSpinBox::valueChanged; + connect(m_numCast, valueChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_numCast, row, 1); QString w = i18n("The list of cast members may include many people. Set the maximum number returned from the search."); label->setWhatsThis(w); m_numCast->setWhatsThis(w); label->setBuddy(m_numCast); l->setRowStretch(++row, 10); m_numCast->setValue(fetcher_ ? fetcher_->m_numCast : 10); } void AbstractAllocineFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { config_.writeEntry("Max Cast", m_numCast->value()); } QByteArray AbstractAllocineFetcher::calculateSignature(const QList >& params_) { typedef QPair StringPair; QByteArray queryString; foreach(const StringPair& pair, params_) { queryString.append(pair.first.toUtf8().toPercentEncoding("+")); queryString.append('='); queryString.append(pair.second.toUtf8().toPercentEncoding("+")); queryString.append('&'); } // remove final '&' queryString.chop(1); const QByteArray toSign = ALLOCINE_PARTNER_KEY + queryString; const QByteArray hash = QCryptographicHash::hash(toSign, QCryptographicHash::Sha1); QByteArray sig = hash.toBase64(); return sig; } /**********************************************************************************************/ AllocineFetcher::AllocineFetcher(QObject* parent_) : AbstractAllocineFetcher(parent_, QLatin1String(ALLOCINE_API_URL)) { } QString AllocineFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } Tellico::Fetch::ConfigWidget* AllocineFetcher::configWidget(QWidget* parent_) const { return new AllocineFetcher::ConfigWidget(parent_, this); } QString AllocineFetcher::defaultName() { return QStringLiteral("AlloCiné.fr"); } QString AllocineFetcher::defaultIcon() { return favIcon("http://www.allocine.fr"); } Tellico::StringHash AllocineFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("origtitle")] = i18n("Original Title"); hash[QStringLiteral("allocine")] = i18n("Allocine Link"); return hash; } AllocineFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AbstractAllocineFetcher* fetcher_) : AbstractAllocineFetcher::ConfigWidget(parent_, fetcher_) { // now add additional fields widget addFieldsWidget(AllocineFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } QString AllocineFetcher::ConfigWidget::preferredName() const { return AllocineFetcher::defaultName(); } diff --git a/src/fetch/amazonfetcher.cpp b/src/fetch/amazonfetcher.cpp index aac403bc..4995933c 100644 --- a/src/fetch/amazonfetcher.cpp +++ b/src/fetch/amazonfetcher.cpp @@ -1,1087 +1,1089 @@ /*************************************************************************** Copyright (C) 2004-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include #include "amazonfetcher.h" #include "amazonrequest.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../images/imagefactory.h" #include "../utils/guiproxy.h" #include "../collection.h" #include "../entry.h" #include "../field.h" #include "../fieldformat.h" #include "../utils/string_utils.h" #include "../utils/isbnvalidator.h" #include "../utils/datafileregistry.h" #include "../gui/combobox.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int AMAZON_RETURNS_PER_REQUEST = 10; static const int AMAZON_MAX_RETURNS_TOTAL = 20; static const char* AMAZON_ASSOC_TOKEN = "tellico-20"; // need to have these in the translation file static const char* linkText = I18N_NOOP("Amazon Link"); } using namespace Tellico; using Tellico::Fetch::AmazonFetcher; // static const AmazonFetcher::SiteData& AmazonFetcher::siteData(int site_) { Q_ASSERT(site_ >= 0); Q_ASSERT(site_ < 15); static SiteData dataVector[14] = { { i18n("Amazon (US)"), QUrl(QLatin1String("http://webservices.amazon.com/onca/xml")), QLatin1String("us"), i18n("United States") }, { i18n("Amazon (UK)"), QUrl(QLatin1String("http://webservices.amazon.co.uk/onca/xml")), QLatin1String("gb"), i18n("United Kingdom") }, { i18n("Amazon (Germany)"), QUrl(QLatin1String("http://webservices.amazon.de/onca/xml")), QLatin1String("de"), i18n("Germany") }, { i18n("Amazon (Japan)"), QUrl(QLatin1String("http://webservices.amazon.co.jp/onca/xml")), QLatin1String("jp"), i18n("Japan") }, { i18n("Amazon (France)"), QUrl(QLatin1String("http://webservices.amazon.fr/onca/xml")), QLatin1String("fr"), i18n("France") }, { i18n("Amazon (Canada)"), QUrl(QLatin1String("http://webservices.amazon.ca/onca/xml")), QLatin1String("ca"), i18n("Canada") }, { i18n("Amazon (China)"), QUrl(QLatin1String("http://webservices.amazon.cn/onca/xml")), QLatin1String("ch"), i18n("China") }, { i18n("Amazon (Spain)"), QUrl(QLatin1String("http://webservices.amazon.es/onca/xml")), QLatin1String("es"), i18n("Spain") }, { i18n("Amazon (Italy)"), QUrl(QLatin1String("http://webservices.amazon.it/onca/xml")), QLatin1String("it"), i18n("Italy") }, { i18n("Amazon (Brazil)"), QUrl(QLatin1String("http://webservices.amazon.com.br/onca/xml")), QLatin1String("br"), i18n("Brazil") }, { i18n("Amazon (Australia)"), QUrl(QLatin1String("http://webservices.amazon.com.au/onca/xml")), QLatin1String("au"), i18n("Australia") }, { i18n("Amazon (India)"), QUrl(QLatin1String("http://webservices.amazon.in/onca/xml")), QLatin1String("in"), i18n("India") }, { i18n("Amazon (Mexico)"), QUrl(QLatin1String("http://webservices.amazon.com.mx/onca/xml")), QLatin1String("mx"), i18n("Mexico") }, { i18n("Amazon (Turkey)"), QUrl(QLatin1String("http://webservices.amazon.com.tr/onca/xml")), QLatin1String("tr"), i18n("Turkey") } }; return dataVector[qBound(0, site_, static_cast(sizeof(dataVector)/sizeof(SiteData)))]; } AmazonFetcher::AmazonFetcher(QObject* parent_) : Fetcher(parent_), m_xsltHandler(nullptr), m_site(Unknown), m_imageSize(MediumImage), m_assoc(QLatin1String(AMAZON_ASSOC_TOKEN)), m_addLinkField(true), m_limit(AMAZON_MAX_RETURNS_TOTAL), m_countOffset(0), m_page(1), m_total(-1), m_numResults(0), m_job(nullptr), m_started(false) { (void)linkText; // just to shut up the compiler } AmazonFetcher::~AmazonFetcher() { delete m_xsltHandler; m_xsltHandler = nullptr; } QString AmazonFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } QString AmazonFetcher::attribution() const { return i18n("This data is licensed under specific terms.", QLatin1String("https://affiliate-program.amazon.com/gp/advertising/api/detail/agreement.html")); } bool AmazonFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::ComicBook || type == Data::Collection::Bibtex || type == Data::Collection::Album || type == Data::Collection::Video || type == Data::Collection::Game || type == Data::Collection::BoardGame; } bool AmazonFetcher::canSearch(FetchKey k) const { // no UPC in Canada return k == Title || k == Person || k == ISBN || (k == UPC && m_site != CA) || k == Keyword; } void AmazonFetcher::readConfigHook(const KConfigGroup& config_) { const int site = config_.readEntry("Site", int(Unknown)); Q_ASSERT(site != Unknown); m_site = static_cast(site); if(m_name.isEmpty()) { m_name = siteData(m_site).title; } QString s = config_.readEntry("AccessKey"); if(!s.isEmpty()) { m_access = s; } else { myWarning() << "No Amazon access key"; } s = config_.readEntry("AssocToken"); if(!s.isEmpty()) { m_assoc = s; } s = config_.readEntry("SecretKey"); if(!s.isEmpty()) { m_amazonKey = s.toUtf8(); } else { myWarning() << "No Amazon secret key"; } int imageSize = config_.readEntry("Image Size", -1); if(imageSize > -1) { m_imageSize = static_cast(imageSize); } } void AmazonFetcher::search() { m_started = true; m_page = 1; m_total = -1; m_countOffset = 0; m_numResults = 0; doSearch(); } void AmazonFetcher::continueSearch() { m_started = true; m_limit += AMAZON_MAX_RETURNS_TOTAL; doSearch(); } void AmazonFetcher::doSearch() { // calling secretKey() ensures that we try to read it first if(secretKey().isEmpty() || m_access.isEmpty()) { if(m_access.isEmpty()) { myWarning() << "No Amazon access key"; } // this message is split in two since the first half is reused later message(i18n("Access to data from Amazon.com requires an AWS Access Key ID and a Secret Key.") + QLatin1Char(' ') + i18n("Those values must be entered in the data source settings."), MessageHandler::Error); stop(); return; } // myDebug() << "value = " << request().value; // myDebug() << "getting page " << m_page; QMap params; params.insert(QStringLiteral("Service"), QStringLiteral("AWSECommerceService")); params.insert(QStringLiteral("AssociateTag"), m_assoc); params.insert(QStringLiteral("AWSAccessKeyId"), m_access); params.insert(QStringLiteral("Operation"), QStringLiteral("ItemSearch")); params.insert(QStringLiteral("ResponseGroup"), QStringLiteral("Large")); params.insert(QStringLiteral("ItemPage"), QString::number(m_page)); // this should match the namespace in amazon2tellico.xsl params.insert(QStringLiteral("Version"), QStringLiteral("2011-08-01")); const int type = collectionType(); switch(type) { case Data::Collection::Book: case Data::Collection::ComicBook: case Data::Collection::Bibtex: params.insert(QStringLiteral("SearchIndex"), QStringLiteral("Books")); params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); break; case Data::Collection::Album: params.insert(QStringLiteral("SearchIndex"), QStringLiteral("Music")); break; case Data::Collection::Video: // CA and JP appear to have a bug where Video only returns VHS or Music results // DVD will return DVD, Blu-ray, etc. so just ignore VHS for those users if(m_site == CA || m_site == JP || m_site == IT || m_site == ES) { params.insert(QStringLiteral("SearchIndex"), QStringLiteral("DVD")); } else { params.insert(QStringLiteral("SearchIndex"), QStringLiteral("Video")); } params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); break; case Data::Collection::Game: params.insert(QStringLiteral("SearchIndex"), QStringLiteral("VideoGames")); break; case Data::Collection::BoardGame: params.insert(QStringLiteral("SearchIndex"), QStringLiteral("Toys")); params.insert(QStringLiteral("SortIndex"), QStringLiteral("relevancerank")); break; case Data::Collection::Coin: case Data::Collection::Stamp: case Data::Collection::Wine: case Data::Collection::Base: case Data::Collection::Card: myDebug() << "can't fetch this type:" << collectionType(); stop(); return; } QString value = request().value; switch(request().key) { case Title: params.insert(QStringLiteral("Title"), value); break; case Person: if(type == Data::Collection::Video) { params.insert(QStringLiteral("Actor"), value); params.insert(QStringLiteral("Director"), value); } else if(type == Data::Collection::Album) { params.insert(QStringLiteral("Artist"), value); } else if(type == Data::Collection::Game) { params.insert(QStringLiteral("Manufacturer"), value); } else { // books and bibtex QString s = QStringLiteral("author:%1 or publisher:%2").arg(value, value); // params.insert(QLatin1String("Author"), value, mib); // params.insert(QLatin1String("Publisher"), value, mib); params.insert(QStringLiteral("Power"), s); } break; case ISBN: { params.insert(QStringLiteral("Operation"), QStringLiteral("ItemLookup")); QString cleanValue = value; cleanValue.remove(QLatin1Char('-')); // ISBN only get digits or 'X' QStringList isbns = FieldFormat::splitValue(cleanValue); // Amazon isbn13 search is still very flaky, so if possible, we're going to convert // all of them to isbn10. If we run into a 979 isbn13, then we're forced to do an // isbn13 search bool isbn13 = false; for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ) { if((*it).startsWith(QLatin1String("979"))) { if(m_site == JP) { // never works for JP myWarning() << "ISBN-13 searching not implemented for Japan"; it = isbns.erase(it); continue; } isbn13 = true; break; } ++it; } // if we want isbn10, then convert all if(!isbn13) { for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ++it) { if((*it).length() > 12) { (*it) = ISBNValidator::isbn10(*it); (*it).remove(QLatin1Char('-')); } } // the default search is by ASIN, which prohibits SearchIndex params.remove(QStringLiteral("SearchIndex")); } // limit to first 10 while(isbns.size() > 10) { isbns.pop_back(); } params.insert(QStringLiteral("ItemId"), isbns.join(QLatin1String(","))); if(isbn13) { params.insert(QStringLiteral("IdType"), QStringLiteral("EAN")); } } break; case UPC: { QString cleanValue = value; cleanValue.remove(QLatin1Char('-')); // for EAN values, add 0 to beginning if not 13 characters // in order to assume US country code from UPC value QStringList values; foreach(const QString& splitValue, cleanValue.split(FieldFormat::delimiterString())) { QString tmpValue = splitValue; if(m_site != US && tmpValue.length() == 12) { tmpValue.prepend(QLatin1Char('0')); } values << tmpValue; // limit to first 10 values if(values.length() >= 10) { break; } } params.insert(QStringLiteral("Operation"), QStringLiteral("ItemLookup")); // US allows UPC, all others are EAN if(m_site == US) { params.insert(QStringLiteral("IdType"), QStringLiteral("UPC")); } else { params.insert(QStringLiteral("IdType"), QStringLiteral("EAN")); } params.insert(QStringLiteral("ItemId"), values.join(QLatin1String(","))); } break; case Keyword: params.insert(QStringLiteral("Keywords"), value); break; case Raw: { QString key = value.section(QLatin1Char('='), 0, 0).trimmed(); QString str = value.section(QLatin1Char('='), 1).trimmed(); params.insert(key, str); } break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } AmazonRequest request(siteData(m_site).url, m_amazonKey); QUrl newUrl = request.signedRequest(params); // myDebug() << newUrl; m_job = KIO::storedGet(newUrl, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &AmazonFetcher::slotComplete); } void AmazonFetcher::stop() { if(!m_started) { return; } // myDebug(); if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void AmazonFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; #if 0 myWarning() << "Remove debug from amazonfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test%1.xml").arg(m_page)); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QStringList errors; if(m_total == -1) { QDomDocument dom; if(!dom.setContent(data, false)) { myWarning() << "server did not return valid XML."; stop(); return; } // check for ItemSearchErrorResponse if(dom.documentElement().tagName() == QLatin1String("ItemSearchErrorResponse")) { QDomNode n = dom.documentElement().namedItem(QStringLiteral("Error")).namedItem(QStringLiteral("Message")); if(!n.isNull()) { message(n.toElement().text(), MessageHandler::Error); stop(); return; } } // find TotalResults element // it's in the first level under the root element //ItemSearchResponse/Items/TotalResults QDomNode n = dom.documentElement().namedItem(QStringLiteral("Items")) .namedItem(QStringLiteral("TotalResults")); QDomElement e = n.toElement(); if(!e.isNull()) { m_total = e.text().toInt(); } n = dom.documentElement().namedItem(QStringLiteral("Items")) .namedItem(QStringLiteral("Request")) .namedItem(QStringLiteral("Errors")); e = n.toElement(); if(!e.isNull()) { QDomNodeList nodes = e.elementsByTagName(QStringLiteral("Error")); for(int i = 0; i < nodes.count(); ++i) { e = nodes.item(i).toElement().namedItem(QStringLiteral("Code")).toElement(); if(!e.isNull() && e.text() == QLatin1String("AWS.ECommerceService.NoExactMatches")) { // no exact match, not a real error, so skip continue; } // for some reason, Amazon will return an error simply when a valid ISBN is not found // I really want to ignore that, so check the IsValid element in the Request element QDomNode isValidNode = n.parentNode().namedItem(QStringLiteral("IsValid")); if(request().key == ISBN && isValidNode.toElement().text().toLower() == QLatin1String("true")) { continue; } e = nodes.item(i).toElement().namedItem(QStringLiteral("Message")).toElement(); if(!e.isNull()) { errors << e.text(); } } } } if(!m_xsltHandler) { initXSLTHandler(); if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading stop(); return; } } // QRegExp stripHTML(QLatin1String("<.*>"), true); // stripHTML.setMinimal(true); // assume amazon is always utf-8 QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(data.constData(), data.size())); Import::TellicoImporter imp(str); // be quiet when loading images imp.setOptions(imp.options() ^ Import::ImportShowImageErrors); Data::CollPtr coll = imp.collection(); if(!coll) { myDebug() << "no collection pointer"; stop(); return; } if(!m_addLinkField) { // remove amazon field if it's not to be added coll->removeField(QStringLiteral("amazon")); } Data::EntryList entries = coll->entries(); if(entries.isEmpty() && !errors.isEmpty()) { for(QStringList::ConstIterator it = errors.constBegin(); it != errors.constEnd(); ++it) { myDebug() << "AmazonFetcher::" << *it; } message(errors[0], MessageHandler::Error); stop(); return; } int count = -1; foreach(Data::EntryPtr entry, entries) { ++count; if(m_numResults >= m_limit) { break; } if(count < m_countOffset) { continue; } if(!m_started) { // might get aborted break; } // special case book author // amazon is really bad about not putting spaces after periods if(coll->type() == Data::Collection::Book) { QRegExp rx(QLatin1String("\\.([^\\s])")); QStringList values = FieldFormat::splitValue(entry->field(QStringLiteral("author"))); for(QStringList::Iterator it = values.begin(); it != values.end(); ++it) { (*it).replace(rx, QStringLiteral(". \\1")); } entry->setField(QStringLiteral("author"), values.join(FieldFormat::delimiterString())); } // UK puts the year in the title for some reason if(m_site == UK && coll->type() == Data::Collection::Video) { QRegExp rx(QLatin1String("\\[(\\d{4})\\]")); QString t = entry->title(); if(rx.indexIn(t) > -1) { QString y = rx.cap(1); t = t.remove(rx).simplified(); entry->setField(QStringLiteral("title"), t); if(entry->field(QStringLiteral("year")).isEmpty()) { entry->setField(QStringLiteral("year"), y); } } } // strip HTML from comments, or plot in movies // tentatively don't do this, looks like ECS 4 cleaned everything up /* if(coll->type() == Data::Collection::Video) { QString plot = entry->field(QLatin1String("plot")); plot.remove(stripHTML); entry->setField(QLatin1String("plot"), plot); } else if(coll->type() == Data::Collection::Game) { QString desc = entry->field(QLatin1String("description")); desc.remove(stripHTML); entry->setField(QLatin1String("description"), desc); } else { QString comments = entry->field(QLatin1String("comments")); comments.remove(stripHTML); entry->setField(QLatin1String("comments"), comments); } */ // myDebug() << entry->title(); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); ++m_numResults; } // we might have gotten aborted if(!m_started) { return; } // are there any additional results to get? m_hasMoreResults = m_page * AMAZON_RETURNS_PER_REQUEST < m_total; const int currentTotal = qMin(m_total, m_limit); if(m_page * AMAZON_RETURNS_PER_REQUEST < currentTotal) { int foundCount = (m_page-1) * AMAZON_RETURNS_PER_REQUEST + coll->entryCount(); message(i18n("Results from %1: %2/%3", source(), foundCount, m_total), MessageHandler::Status); ++m_page; m_countOffset = 0; doSearch(); } else if(request().value.count(QLatin1Char(';')) > 9) { // start new request after cutting off first 10 isbn values FetchRequest newRequest = request(); newRequest.value = request().value.section(QLatin1Char(';'), 10); startSearch(newRequest); } else { m_countOffset = m_entries.count() % AMAZON_RETURNS_PER_REQUEST; if(m_countOffset == 0) { ++m_page; // need to go to next page } stop(); } } Tellico::Data::EntryPtr AmazonFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries[uid_]; if(!entry) { myWarning() << "no entry in dict"; return entry; } // do what we can to remove useless keywords const int type = collectionType(); switch(type) { case Data::Collection::Book: case Data::Collection::ComicBook: case Data::Collection::Bibtex: if(optionalFields().contains(QStringLiteral("keyword"))) { StringSet newWords; const QStringList keywords = FieldFormat::splitValue(entry->field(QStringLiteral("keyword"))); foreach(const QString& keyword, keywords) { // the amazon2tellico stylesheet separates keywords with '/' const QStringList words = keyword.split(QLatin1Char('/')); foreach(const QString& word, words) { if(word == QLatin1String("General") || word == QLatin1String("Subjects") || word == QLatin1String("Par prix") || // french stuff word == QLatin1String("Divers") || // french stuff word.startsWith(QLatin1Char('(')) || word.startsWith(QLatin1String("Authors"))) { continue; } newWords.add(word); } } entry->setField(QStringLiteral("keyword"), newWords.toList().join(FieldFormat::delimiterString())); } entry->setField(QStringLiteral("comments"), Tellico::decodeHTML(entry->field(QStringLiteral("comments")))); break; case Data::Collection::Video: { const QString genres = QStringLiteral("genre"); QStringList oldWords = FieldFormat::splitValue(entry->field(genres)); StringSet words; // only care about genres that have "Genres" in the amazon response // and take the first word after that for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) { if((*it).indexOf(QLatin1String("Genres")) == -1) { continue; } // the amazon2tellico stylesheet separates words with '/' QStringList nodes = (*it).split(QLatin1Char('/')); for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) { if(*it2 != QLatin1String("Genres")) { continue; } ++it2; if(it2 != nodes.end() && *it2 != QLatin1String("General")) { words.add(*it2); } break; // we're done } } entry->setField(genres, words.toList().join(FieldFormat::delimiterString())); // language tracks get duplicated, too words.clear(); words.add(FieldFormat::splitValue(entry->field(QStringLiteral("language")))); entry->setField(QStringLiteral("language"), words.toList().join(FieldFormat::delimiterString())); } entry->setField(QStringLiteral("plot"), Tellico::decodeHTML(entry->field(QStringLiteral("plot")))); break; case Data::Collection::Album: { const QString genres = QStringLiteral("genre"); QStringList oldWords = FieldFormat::splitValue(entry->field(genres)); StringSet words; // only care about genres that have "Styles" in the amazon response // and take the first word after that for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) { if((*it).indexOf(QLatin1String("Styles")) == -1) { continue; } // the amazon2tellico stylesheet separates words with '/' QStringList nodes = (*it).split(QLatin1Char('/')); bool isStyle = false; for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) { if(!isStyle) { if(*it2 == QLatin1String("Styles")) { isStyle = true; } continue; } if(*it2 != QLatin1String("General")) { words.add(*it2); } } } entry->setField(genres, words.toList().join(FieldFormat::delimiterString())); } entry->setField(QStringLiteral("comments"), Tellico::decodeHTML(entry->field(QStringLiteral("comments")))); break; case Data::Collection::Game: entry->setField(QStringLiteral("description"), Tellico::decodeHTML(entry->field(QStringLiteral("description")))); break; } // clean up the title parseTitle(entry); // also sometimes table fields have rows but no values Data::FieldList fields = entry->collection()->fields(); QRegExp blank(QLatin1String("[\\s") + FieldFormat::columnDelimiterString() + FieldFormat::delimiterString() + QLatin1String("]+")); // only white space, column separators and value separators foreach(Data::FieldPtr fIt, fields) { if(fIt->type() != Data::Field::Table) { continue; } if(blank.exactMatch(entry->field(fIt))) { entry->setField(fIt, QString()); } } QString imageURL; switch(m_imageSize) { case SmallImage: imageURL = entry->field(QStringLiteral("small-image")); break; case MediumImage: imageURL = entry->field(QStringLiteral("medium-image")); break; case LargeImage: imageURL = entry->field(QStringLiteral("large-image")); break; case NoImage: default: break; } // myDebug() << "grabbing " << imageURL.toDisplayString(); if(!imageURL.isEmpty()) { QString id = ImageFactory::addImage(QUrl::fromUserInput(imageURL), true); if(id.isEmpty()) { message(i18n("The cover image could not be loaded."), MessageHandler::Warning); } else { // amazon serves up 1x1 gifs occasionally, but that's caught in the image constructor // all relevant collection types have cover fields entry->setField(QStringLiteral("cover"), id); } } // don't want to show image urls in the fetch dialog entry->setField(QStringLiteral("small-image"), QString()); entry->setField(QStringLiteral("medium-image"), QString()); entry->setField(QStringLiteral("large-image"), QString()); return entry; } void AmazonFetcher::initXSLTHandler() { QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("amazon2tellico.xsl")); if(xsltfile.isEmpty()) { myWarning() << "can not locate amazon2tellico.xsl."; return; } QUrl u = QUrl::fromLocalFile(xsltfile); delete m_xsltHandler; m_xsltHandler = new XSLTHandler(u); if(!m_xsltHandler->isValid()) { myWarning() << "error in amazon2tellico.xsl."; delete m_xsltHandler; m_xsltHandler = nullptr; return; } } Tellico::Fetch::FetchRequest AmazonFetcher::updateRequest(Data::EntryPtr entry_) { const int type = entry_->collection()->type(); const QString t = entry_->field(QStringLiteral("title")); if(type == Data::Collection::Book || type == Data::Collection::ComicBook || type == Data::Collection::Bibtex) { const QString isbn = entry_->field(QStringLiteral("isbn")); if(!isbn.isEmpty()) { return FetchRequest(Fetch::ISBN, isbn); } const QString a = entry_->field(QStringLiteral("author")); if(!a.isEmpty()) { return t.isEmpty() ? FetchRequest(Fetch::Person, a) : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a); } } else if(type == Data::Collection::Album) { const QString a = entry_->field(QStringLiteral("artist")); if(!a.isEmpty()) { return t.isEmpty() ? FetchRequest(Fetch::Person, a) : FetchRequest(Fetch::Keyword, t + QLatin1Char('-') + a); } } // optimistically try searching for title and rely on Collection::sameEntry() to figure things out if(!t.isEmpty()) { return FetchRequest(Fetch::Title, t); } return FetchRequest(); } void AmazonFetcher::parseTitle(Tellico::Data::EntryPtr entry_) { // assume that everything in brackets or parentheses is extra QRegExp rx(QLatin1String("[\\(\\[](.*)[\\)\\]]")); rx.setMinimal(true); QString title = entry_->field(QStringLiteral("title")); int pos = rx.indexIn(title); while(pos > -1) { if(parseTitleToken(entry_, rx.cap(1))) { title.remove(pos, rx.matchedLength()); --pos; // search again there } pos = rx.indexIn(title, pos+1); } entry_->setField(QStringLiteral("title"), title.trimmed()); } bool AmazonFetcher::parseTitleToken(Tellico::Data::EntryPtr entry_, const QString& token_) { // myDebug() << "title token:" << token_; // if res = true, then the token gets removed from the title bool res = false; if(token_.indexOf(QLatin1String("widescreen"), 0, Qt::CaseInsensitive) > -1 || token_.indexOf(i18n("Widescreen"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("widescreen"), QStringLiteral("true")); // res = true; leave it in the title } else if(token_.indexOf(QLatin1String("full screen"), 0, Qt::CaseInsensitive) > -1) { // skip, but go ahead and remove from title res = true; } else if(token_.indexOf(QLatin1String("import"), 0, Qt::CaseInsensitive) > -1) { // skip, but go ahead and remove from title res = true; } if(token_.indexOf(QLatin1String("blu-ray"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("medium"), i18n("Blu-ray")); res = true; } else if(token_.indexOf(QLatin1String("hd dvd"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("medium"), i18n("HD DVD")); res = true; } else if(token_.indexOf(QLatin1String("vhs"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("medium"), i18n("VHS")); res = true; } if(token_.indexOf(QLatin1String("director's cut"), 0, Qt::CaseInsensitive) > -1 || token_.indexOf(i18n("Director's Cut"), 0, Qt::CaseInsensitive) > -1) { entry_->setField(QStringLiteral("directors-cut"), QStringLiteral("true")); // res = true; leave it in the title } if(token_.toLower() == QLatin1String("ntsc")) { entry_->setField(QStringLiteral("format"), i18n("NTSC")); res = true; } if(token_.toLower() == QLatin1String("dvd")) { entry_->setField(QStringLiteral("medium"), i18n("DVD")); res = true; } static QRegExp regionRx(QLatin1String("Region [1-9]")); if(regionRx.indexIn(token_) > -1) { entry_->setField(QStringLiteral("region"), i18n(regionRx.cap(0).toUtf8().constData())); res = true; } if(entry_->collection()->type() == Data::Collection::Game) { Data::FieldPtr f = entry_->collection()->fieldByName(QStringLiteral("platform")); if(f && f->allowed().contains(token_)) { res = true; } } return res; } QString AmazonFetcher::secretKey() const { return QString::fromUtf8(m_amazonKey); } //static QString AmazonFetcher::defaultName() { return i18n("Amazon.com Web Services"); } QString AmazonFetcher::defaultIcon() { return favIcon("http://www.amazon.com"); } Tellico::StringHash AmazonFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("keyword")] = i18n("Keywords"); return hash; } Tellico::Fetch::ConfigWidget* AmazonFetcher::configWidget(QWidget* parent_) const { return new AmazonFetcher::ConfigWidget(parent_, this); } AmazonFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AmazonFetcher* fetcher_/*=0*/) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", AmazonFetcher::defaultName(), QLatin1String("https://affiliate-program.amazon.com/gp/flex/advertising/api/sign-in.html")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_accessEdit = new QLineEdit(optionsWidget()); - connect(m_accessEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_accessEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_accessEdit, row, 1); QString w = i18n("Access to data from Amazon.com requires an AWS Access Key ID and a Secret Key."); label->setWhatsThis(w); m_accessEdit->setWhatsThis(w); label->setBuddy(m_accessEdit); label = new QLabel(i18n("Secret key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_secretKeyEdit = new QLineEdit(optionsWidget()); // m_secretKeyEdit->setEchoMode(QLineEdit::PasswordEchoOnEdit); - connect(m_secretKeyEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_secretKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_secretKeyEdit, row, 1); label->setWhatsThis(w); m_secretKeyEdit->setWhatsThis(w); label->setBuddy(m_secretKeyEdit); label = new QLabel(i18n("Country: "), optionsWidget()); l->addWidget(label, ++row, 0); m_siteCombo = new GUI::ComboBox(optionsWidget()); for(int i = 0; i < XX; ++i) { const AmazonFetcher::SiteData& siteData = AmazonFetcher::siteData(i); QIcon icon(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kf5/locale/countries/%1/flag.png").arg(siteData.country))); m_siteCombo->addItem(icon, siteData.countryName, i); m_siteCombo->model()->sort(0); } - connect(m_siteCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); - connect(m_siteCombo, SIGNAL(activated(int)), SLOT(slotSiteChanged())); + void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; + connect(m_siteCombo, activatedInt, this, &ConfigWidget::slotSetModified); + connect(m_siteCombo, activatedInt, this, &ConfigWidget::slotSiteChanged); l->addWidget(m_siteCombo, row, 1); w = i18n("Amazon.com provides data from several different localized sites. Choose the one " "you wish to use for this data source."); label->setWhatsThis(w); m_siteCombo->setWhatsThis(w); label->setBuddy(m_siteCombo); label = new QLabel(i18n("&Image size: "), optionsWidget()); l->addWidget(label, ++row, 0); m_imageCombo = new GUI::ComboBox(optionsWidget()); m_imageCombo->addItem(i18n("Small Image"), SmallImage); m_imageCombo->addItem(i18n("Medium Image"), MediumImage); m_imageCombo->addItem(i18n("Large Image"), LargeImage); m_imageCombo->addItem(i18n("No Image"), NoImage); - connect(m_imageCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + connect(m_imageCombo, activatedInt, this, &ConfigWidget::slotSetModified); l->addWidget(m_imageCombo, row, 1); w = i18n("The cover image may be downloaded as well. However, too many large images in the " "collection may degrade performance."); label->setWhatsThis(w); m_imageCombo->setWhatsThis(w); label->setBuddy(m_imageCombo); label = new QLabel(i18n("&Associate's ID: "), optionsWidget()); l->addWidget(label, ++row, 0); m_assocEdit = new QLineEdit(optionsWidget()); - connect(m_assocEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + void (QLineEdit::* textChanged)(const QString&) = &QLineEdit::textChanged; + connect(m_assocEdit, textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_assocEdit, row, 1); w = i18n("The associate's id identifies the person accessing the Amazon.com Web Services, and is included " "in any links to the Amazon.com site."); label->setWhatsThis(w); m_assocEdit->setWhatsThis(w); label->setBuddy(m_assocEdit); l->setRowStretch(++row, 10); if(fetcher_) { m_siteCombo->setCurrentData(fetcher_->m_site); m_accessEdit->setText(fetcher_->m_access); m_secretKeyEdit->setText(fetcher_->secretKey()); m_assocEdit->setText(fetcher_->m_assoc); m_imageCombo->setCurrentData(fetcher_->m_imageSize); } else { // defaults m_assocEdit->setText(QLatin1String(AMAZON_ASSOC_TOKEN)); m_imageCombo->setCurrentData(MediumImage); } addFieldsWidget(AmazonFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); KAcceleratorManager::manage(optionsWidget()); } void AmazonFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { int n = m_siteCombo->currentData().toInt(); config_.writeEntry("Site", n); QString s = m_accessEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("AccessKey", s); } QByteArray b = m_secretKeyEdit->text().trimmed().toUtf8(); if(!b.isEmpty()) { config_.writeEntry("SecretKey", b); } s = m_assocEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("AssocToken", s); } n = m_imageCombo->currentData().toInt(); config_.writeEntry("Image Size", n); } QString AmazonFetcher::ConfigWidget::preferredName() const { return AmazonFetcher::siteData(m_siteCombo->currentData().toInt()).title; } void AmazonFetcher::ConfigWidget::slotSiteChanged() { emit signalName(preferredName()); } diff --git a/src/fetch/animenfofetcher.cpp b/src/fetch/animenfofetcher.cpp index c6b292c8..c9956341 100644 --- a/src/fetch/animenfofetcher.cpp +++ b/src/fetch/animenfofetcher.cpp @@ -1,514 +1,514 @@ /*************************************************************************** Copyright (C) 2006-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "animenfofetcher.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../collections/bookcollection.h" #include "../collections/videocollection.h" #include "../entry.h" #include "../fieldformat.h" #include "../core/filehandler.h" #include "../images/imagefactory.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include namespace { static const char* ANIMENFO_BASE_URL = "http://www.animenfo.com/search.php"; } using namespace Tellico; using Tellico::Fetch::AnimeNfoFetcher; AnimeNfoFetcher::AnimeNfoFetcher(QObject* parent_) : Fetcher(parent_), m_started(false) { } AnimeNfoFetcher::~AnimeNfoFetcher() { } QString AnimeNfoFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool AnimeNfoFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex || type == Data::Collection::Video; } void AnimeNfoFetcher::readConfigHook(const KConfigGroup& config_) { Q_UNUSED(config_); } void AnimeNfoFetcher::search() { m_started = true; m_matches.clear(); QUrl u(QString::fromLatin1(ANIMENFO_BASE_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("action"), QStringLiteral("Go")); q.addQueryItem(QStringLiteral("option"), QStringLiteral("keywords")); switch(request().collectionType) { case Data::Collection::Book: q.addQueryItem(QStringLiteral("queryin"), QStringLiteral("manga_titles")); break; case Data::Collection::Video: q.addQueryItem(QStringLiteral("queryin"), QStringLiteral("anime_titles")); break; default: myWarning() << "collection type not valid:" << request().collectionType; stop(); return; } switch(request().key) { case Keyword: q.addQueryItem(QStringLiteral("query"), request().value); break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } u.setQuery(q); // myDebug() << "url:" << u; m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &AnimeNfoFetcher::slotComplete); } void AnimeNfoFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void AnimeNfoFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } const QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; QString s = Tellico::decodeHTML(data); #if 0 myWarning() << "Remove debug from animenfofetcher.cpp"; QFile f(QLatin1String("/tmp/test.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << s; } f.close(); #endif QRegExp infoRx(QLatin1String("]*class\\s*=\\s*[\"']anime_info[\"'][^>]*>(.*)"), Qt::CaseInsensitive); infoRx.setMinimal(true); QRegExp anchorRx(QLatin1String("]*href\\s*=\\s*[\"'](.*)[\"'][^>]*>(.*)"), Qt::CaseInsensitive); anchorRx.setMinimal(true); QRegExp yearRx(QLatin1String("\\d{4}")); // search page comes in groups of threes int n = 0; QString u, t, y; for(int pos = infoRx.indexIn(s); m_started && pos > -1; pos = infoRx.indexIn(s, pos+1)) { if(n == 0 && !u.isEmpty()) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), t, y); QUrl url = QUrl(QString::fromLatin1(ANIMENFO_BASE_URL)).resolved(QUrl(u)); url.setQuery(QString()); m_matches.insert(r->uid, url); // don't emit signal until after putting url in matches hash emit signalResultFound(r); u.clear(); t.clear(); y.clear(); } switch(n) { case 0: // title and url { int pos2 = anchorRx.indexIn(infoRx.cap(1)); if(pos2 > -1) { u = anchorRx.cap(1); t = anchorRx.cap(2); } } break; case 1: // don't case break; case 2: if(yearRx.exactMatch(infoRx.cap(1))) { y = infoRx.cap(1); } break; } n = (n+1)%3; } // grab last response if(!u.isEmpty()) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), t, y, QString()); QUrl url = QUrl(QString::fromLatin1(ANIMENFO_BASE_URL)).resolved(QUrl(u)); url.setQuery(QString()); m_matches.insert(r->uid, url); // don't emit signal until after putting url in matches hash emit signalResultFound(r); } stop(); } Tellico::Data::EntryPtr AnimeNfoFetcher::fetchEntryHook(uint uid_) { // if we already grabbed this one, then just pull it out of the dict Data::EntryPtr entry = m_entries[uid_]; if(entry) { return entry; } QUrl url = m_matches[uid_]; if(url.isEmpty()) { myWarning() << "no url in map"; return Data::EntryPtr(); } QString results = Tellico::decodeHTML(FileHandler::readTextFile(url, true, true)); if(results.isEmpty()) { myDebug() << "no text results"; return Data::EntryPtr(); } #if 0 myWarning() << "Remove debug from animenfofetcher.cpp"; QFile f(QLatin1String("/tmp/test.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << results; } f.close(); #endif entry = parseEntry(results, url); if(!entry) { myDebug() << "error in processing entry"; return Data::EntryPtr(); } m_entries.insert(uid_, entry); // keep for later return entry; } Tellico::Data::EntryPtr AnimeNfoFetcher::parseEntry(const QString& str_, const QUrl& url_) { // myDebug(); // class might be anime_info_top QRegExp infoRx(QLatin1String("]*class\\s*=\\s*[\"']anime_info[^>]*>(.*)"), Qt::CaseInsensitive); infoRx.setMinimal(true); QRegExp tagRx(QLatin1String("<.*>")); tagRx.setMinimal(true); QRegExp anchorRx(QLatin1String("]*href\\s*=\\s*[\"'](.*)[\"'][^>]*>(.*)"), Qt::CaseInsensitive); anchorRx.setMinimal(true); QRegExp jsRx(QLatin1String(""), Qt::CaseInsensitive); jsRx.setMinimal(true); QString s = str_; s.remove(jsRx); Data::CollPtr coll; switch(request().collectionType) { case Data::Collection::Book: case Data::Collection::Bibtex: coll = Data::CollPtr(new Data::BookCollection(true)); break; case Data::Collection::Video: coll = Data::CollPtr(new Data::VideoCollection(true)); break; default: return Data::EntryPtr(); } // add new fields Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); coll->addField(f); f = new Data::Field(QStringLiteral("alttitle"), i18n("Alternative Titles"), Data::Field::Table); f->setFormatType(FieldFormat::FormatTitle); coll->addField(f); f = new Data::Field(QStringLiteral("distributor"), i18n("Distributor")); f->setCategory(i18n("Other People")); f->setFlags(Data::Field::AllowCompletion | Data::Field::AllowMultiple | Data::Field::AllowGrouped); f->setFormatType(FieldFormat::FormatPlain); coll->addField(f); f = new Data::Field(QStringLiteral("episodes"), i18n("Episodes"), Data::Field::Number); f->setCategory(i18n("Features")); coll->addField(f); f = new Data::Field(QStringLiteral("animenfo"), i18n("AnimeNfo Link"), Data::Field::URL); f->setCategory(i18n("General")); coll->addField(f); f = new Data::Field(QStringLiteral("animenfo-rating"), i18n("AnimeNfo Rating"), Data::Field::Rating); f->setCategory(i18n("General")); f->setProperty(QStringLiteral("maximum"), QStringLiteral("10")); coll->addField(f); // map captions in HTML to field names QHash fieldMap; fieldMap.insert(QStringLiteral("Title"), QStringLiteral("title")); fieldMap.insert(QStringLiteral("Japanese Title"), QStringLiteral("origtitle")); fieldMap.insert(QStringLiteral("Total Episodes"), QStringLiteral("episodes")); fieldMap.insert(QStringLiteral("Category"), QStringLiteral("keyword")); fieldMap.insert(QStringLiteral("Genres"), QStringLiteral("genre")); fieldMap.insert(QStringLiteral("Genre"), QStringLiteral("genre")); fieldMap.insert(QStringLiteral("Studio"), QStringLiteral("studio")); fieldMap.insert(QStringLiteral("US Distribution"), QStringLiteral("distributor")); fieldMap.insert(QStringLiteral("Author"), QStringLiteral("author")); fieldMap.insert(QStringLiteral("Publisher"), QStringLiteral("publisher")); fieldMap.insert(QStringLiteral("Director"), QStringLiteral("director")); fieldMap.insert(QStringLiteral("Script"), QStringLiteral("writer")); fieldMap.insert(QStringLiteral("Music"), QStringLiteral("composer")); fieldMap.insert(QStringLiteral("User Rating"), QStringLiteral("animenfo-rating")); switch(request().collectionType) { case Data::Collection::Book: case Data::Collection::Bibtex: fieldMap.insert(QStringLiteral("Year Published"), QStringLiteral("pub_year")); break; case Data::Collection::Video: fieldMap.insert(QStringLiteral("Year Published"), QStringLiteral("year")); break; default: break; } Data::EntryPtr entry(new Data::Entry(coll)); QString fullTitle; int n = 0; QString key, value; for(int pos = infoRx.indexIn(s); pos > -1; pos = infoRx.indexIn(s, pos+1)) { if(n == 0 && !key.isEmpty()) { if(fieldMap.contains(key)) { value = value.simplified(); if(value.endsWith(QLatin1Char(';'))) { value.chop(1); } if(!value.isEmpty() && value != QLatin1String("-")) { const QString fieldName = fieldMap.value(key); if(key == QLatin1String("Title")) { // strip possible trailing year, etc. fullTitle = value; value.remove(QRegExp(QLatin1String("\\s*\\([^)]*\\)$"))); entry->setField(fieldName, value); } else if(key == QLatin1String("Total Episodes")) { // strip possible trailing text value.remove(QRegExp(QLatin1String("[\\D].*$"))); entry->setField(fieldName, value); } else if(key == QLatin1String("User Rating")) { QRegExp rating(QLatin1String("^(.*)/10")); if(rating.indexIn(value) > -1) { const double d = rating.cap(1).toDouble(); entry->setField(fieldName, QString::number(static_cast(d+0.5))); } } else if(key == QLatin1String("Year Published")) { // strip possible trailing text value.remove(QRegExp(QLatin1String("[\\D;].*$"))); entry->setField(fieldName, value); } else { entry->setField(fieldName, value); } if(fieldName == QLatin1String("studio") || fieldName == QLatin1String("genre") || fieldName == QLatin1String("script") || fieldName == QLatin1String("distributor") || fieldName == QLatin1String("director") || fieldName == QLatin1String("writer") || fieldName == QLatin1String("author") || fieldName == QLatin1String("publisher") || fieldName == QLatin1String("composer")) { QStringList values = entry->field(fieldName).split(QRegExp(QLatin1String("\\s*,\\s*"))); entry->setField(fieldName, values.join(FieldFormat::delimiterString())); } } } key.clear(); value.clear(); } switch(n) { case 0: key = infoRx.cap(1).remove(tagRx); break; case 1: value = infoRx.cap(1).replace(QLatin1String("
"), QLatin1String("; ")).remove(tagRx); break; } n = (n+1)%2; } entry->setField(QStringLiteral("animenfo"), url_.url()); // image QRegExp imgRx(QStringLiteral("]*src\\s*=\\s*[\"']([^>]*)[\"']\\s+[^>]*alt\\s*=\\s*[\"']%1[\"']") .arg(QRegExp::escape(fullTitle)), Qt::CaseInsensitive); imgRx.setMinimal(true); int pos = imgRx.indexIn(s); if(pos > -1) { QUrl imgURL = QUrl(QLatin1String(ANIMENFO_BASE_URL)).resolved(QUrl(imgRx.cap(1))); QString id = ImageFactory::addImage(imgURL, true); if(!id.isEmpty()) { entry->setField(QStringLiteral("cover"), id); } else { myDebug() << "bad cover" << imgURL.url(); } } // now look for alternative titles and plot const QString a = QStringLiteral("Alternative titles"); pos = s.indexOf(a, 0, Qt::CaseInsensitive); if(pos > -1) { pos += a.length(); int pos2 = s.indexOf(QLatin1String(" -1) { value = s.mid(pos, pos2-pos).simplified(); value.replace(QLatin1String("
"), FieldFormat::rowDelimiterString()); value = value.remove(tagRx).trimmed(); entry->setField(QStringLiteral("alttitle"), value); } } pos = s.indexOf(QLatin1String("Description"), pos > -1 ? pos : 0); if(pos > -1) { QRegExp descRx(QLatin1String("]*class\\s*=\\s*[\"']description[\"'].*>(.*) -1) { entry->setField(QStringLiteral("plot"), descRx.cap(1).remove(tagRx).simplified()); } } pos = s.indexOf(QLatin1String("Voice Talent")); if(pos > -1) { QRegExp charRx(QLatin1String("(.*)"), Qt::CaseInsensitive); charRx.setMinimal(true); QRegExp voiceRx(QLatin1String("(.*)"), Qt::CaseInsensitive); voiceRx.setMinimal(true); QStringList castLines; for(pos = s.indexOf(charRx, pos); pos > -1; pos = s.indexOf(charRx, pos+1)) { if(voiceRx.indexIn(s, pos) > -1) { castLines << voiceRx.cap(1) + FieldFormat::columnDelimiterString() + charRx.cap(1); } } entry->setField(QStringLiteral("cast"), castLines.join(FieldFormat::rowDelimiterString())); } return entry; } Tellico::Fetch::FetchRequest AnimeNfoFetcher::updateRequest(Data::EntryPtr entry_) { QString t = entry_->field(QStringLiteral("title")); if(!t.isEmpty()) { return FetchRequest(Fetch::Keyword, t); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* AnimeNfoFetcher::configWidget(QWidget* parent_) const { return new AnimeNfoFetcher::ConfigWidget(parent_, this); } QString AnimeNfoFetcher::defaultName() { return QStringLiteral("AnimeNfo.com"); } QString AnimeNfoFetcher::defaultIcon() { return favIcon("http://animenfo.com"); } //static Tellico::StringHash AnimeNfoFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("distributor")] = i18n("Distributor"); hash[QStringLiteral("episodes")] = i18n("Episodes"); hash[QStringLiteral("origtitle")] = i18n("Original Title"); hash[QStringLiteral("alttitle")] = i18n("Alternative Titles"); hash[QStringLiteral("animenfo-rating")] = i18n("AnimeNfo Rating"); hash[QStringLiteral("animenfo")] = i18n("AnimeNfo Link"); return hash; } AnimeNfoFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AnimeNfoFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(AnimeNfoFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } QString AnimeNfoFetcher::ConfigWidget::preferredName() const { return AnimeNfoFetcher::defaultName(); } diff --git a/src/fetch/arxivfetcher.cpp b/src/fetch/arxivfetcher.cpp index 92462d8b..85e6754e 100644 --- a/src/fetch/arxivfetcher.cpp +++ b/src/fetch/arxivfetcher.cpp @@ -1,337 +1,337 @@ /*************************************************************************** Copyright (C) 2007-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "arxivfetcher.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../utils/datafileregistry.h" #include "../collection.h" #include "../entry.h" #include "../core/netaccess.h" #include "../images/imagefactory.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int ARXIV_RETURNS_PER_REQUEST = 20; static const char* ARXIV_BASE_URL = "http://export.arxiv.org/api/query"; } using namespace Tellico; using namespace Tellico::Fetch; using Tellico::Fetch::ArxivFetcher; ArxivFetcher::ArxivFetcher(QObject* parent_) : Fetcher(parent_), m_xsltHandler(nullptr), m_start(0), m_total(-1), m_job(nullptr), m_started(false) { } ArxivFetcher::~ArxivFetcher() { delete m_xsltHandler; m_xsltHandler = nullptr; } QString ArxivFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool ArxivFetcher::canFetch(int type) const { return type == Data::Collection::Bibtex; } void ArxivFetcher::readConfigHook(const KConfigGroup&) { } void ArxivFetcher::search() { m_started = true; m_start = 0; m_total = -1; doSearch(); } void ArxivFetcher::continueSearch() { m_started = true; doSearch(); } void ArxivFetcher::doSearch() { QUrl u = searchURL(request().key, request().value); if(u.isEmpty()) { stop(); return; } m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &ArxivFetcher::slotComplete); } void ArxivFetcher::stop() { if(!m_started) { return; } // myDebug(); if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void ArxivFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; #if 0 myWarning() << "Remove debug from arxivfetcher.cpp"; QFile f(QLatin1String("/tmp/test.xml")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif if(!m_xsltHandler) { initXSLTHandler(); if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading stop(); return; } } if(m_total == -1) { QDomDocument dom; if(!dom.setContent(data, true /*namespace*/)) { myWarning() << "server did not return valid XML."; stop(); return; } // total is top level element, with attribute totalResultsAvailable QDomNodeList list = dom.elementsByTagNameNS(QStringLiteral("http://a9.com/-/spec/opensearch/1.1/"), QStringLiteral("totalResults")); if(list.count() > 0) { m_total = list.item(0).toElement().text().toInt(); } } // assume result is always utf-8 QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(data.constData(), data.size())); Import::TellicoImporter imp(str); Data::CollPtr coll = imp.collection(); if(!coll) { myDebug() << "no valid result"; stop(); return; } foreach(Data::EntryPtr entry, coll->entries()) { if(!m_started) { // might get aborted break; } FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } m_start = m_entries.count(); m_hasMoreResults = m_start < m_total; stop(); // required } Tellico::Data::EntryPtr ArxivFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries[uid_]; // if URL but no cover image, fetch it if(!entry->field(QStringLiteral("url")).isEmpty()) { Data::CollPtr coll = entry->collection(); Data::FieldPtr field = coll->fieldByName(QStringLiteral("cover")); if(!field && !coll->imageFields().isEmpty()) { field = coll->imageFields().front(); } else if(!field) { field = new Data::Field(QStringLiteral("cover"), i18n("Front Cover"), Data::Field::Image); coll->addField(field); } if(entry->field(field).isEmpty()) { QPixmap pix = NetAccess::filePreview(QUrl::fromUserInput(entry->field(QStringLiteral("url")))); if(!pix.isNull()) { QString id = ImageFactory::addImage(pix, QStringLiteral("PNG")); if(!id.isEmpty()) { entry->setField(field, id); } } } } QRegExp versionRx(QLatin1String("v\\d+$")); // if the original search was not for a versioned ID, remove it if(request().key != ArxivID || !request().value.contains(versionRx)) { QString arxiv = entry->field(QStringLiteral("arxiv")); arxiv.remove(versionRx); entry->setField(QStringLiteral("arxiv"), arxiv); } return entry; } void ArxivFetcher::initXSLTHandler() { QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("arxiv2tellico.xsl")); if(xsltfile.isEmpty()) { myWarning() << "can not locate arxiv2tellico.xsl."; return; } QUrl u = QUrl::fromLocalFile(xsltfile); delete m_xsltHandler; m_xsltHandler = new XSLTHandler(u); if(!m_xsltHandler->isValid()) { myWarning() << "error in arxiv2tellico.xsl."; delete m_xsltHandler; m_xsltHandler = nullptr; return; } } QUrl ArxivFetcher::searchURL(FetchKey key_, const QString& value_) const { QUrl u(QString::fromLatin1(ARXIV_BASE_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("start"), QString::number(m_start)); q.addQueryItem(QStringLiteral("max_results"), QString::number(ARXIV_RETURNS_PER_REQUEST)); // quotes should be used if spaces are present QString value = value_; value.replace(QLatin1Char(' '), QLatin1Char('+')); // seems to have problems with dashes, too value.replace(QLatin1Char('-'), QLatin1Char('+')); QString query; switch(key_) { case Title: query = QStringLiteral("ti:%1").arg(value); break; case Person: query = QStringLiteral("au:%1").arg(value); break; case Keyword: // keyword gets to use all the words without being quoted query = QStringLiteral("all:%1").arg(value); break; case ArxivID: { // remove prefix and/or version number QString value = value_; value.remove(QRegExp(QLatin1String("^arxiv:"), Qt::CaseInsensitive)); value.remove(QRegExp(QLatin1String("v\\d+$"))); query = QStringLiteral("id:%1").arg(value); } break; default: myWarning() << "key not recognized: " << request().key; return QUrl(); } q.addQueryItem(QStringLiteral("search_query"), query); u.setQuery(q); // myDebug() << "url: " << u; return u; } Tellico::Fetch::FetchRequest ArxivFetcher::updateRequest(Data::EntryPtr entry_) { QString id = entry_->field(QStringLiteral("arxiv")); if(!id.isEmpty()) { // remove prefix and/or version number id.remove(QRegExp(QLatin1String("^arxiv:"), Qt::CaseInsensitive)); id.remove(QRegExp(QLatin1String("v\\d+$"))); return FetchRequest(Fetch::ArxivID, id); } // optimistically try searching for title and rely on Collection::sameEntry() to figure things out QString t = entry_->field(QStringLiteral("title")); if(!t.isEmpty()) { return FetchRequest(Fetch::Title, t); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* ArxivFetcher::configWidget(QWidget* parent_) const { return new ArxivFetcher::ConfigWidget(parent_, this); } QString ArxivFetcher::defaultName() { return QStringLiteral("arXiv.org"); // no translation } QString ArxivFetcher::defaultIcon() { return favIcon("http://arxiv.org"); } ArxivFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ArxivFetcher*) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); } void ArxivFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) { } QString ArxivFetcher::ConfigWidget::preferredName() const { return ArxivFetcher::defaultName(); } diff --git a/src/fetch/bedethequefetcher.cpp b/src/fetch/bedethequefetcher.cpp index 1174b2e4..f150dea2 100644 --- a/src/fetch/bedethequefetcher.cpp +++ b/src/fetch/bedethequefetcher.cpp @@ -1,474 +1,474 @@ /*************************************************************************** Copyright (C) 2016 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "bedethequefetcher.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../utils/isbnvalidator.h" #include "../collections/comicbookcollection.h" #include "../entry.h" #include "../fieldformat.h" #include "../core/filehandler.h" #include "../images/imagefactory.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include namespace { static const char* BD_BASE_URL = "https://m.bedetheque.com/album"; } using namespace Tellico; using Tellico::Fetch::BedethequeFetcher; BedethequeFetcher::BedethequeFetcher(QObject* parent_) : Fetcher(parent_), m_total(0), m_started(false) { } BedethequeFetcher::~BedethequeFetcher() { } QString BedethequeFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } Fetch::Type BedethequeFetcher::type() const { return Bedetheque; } bool BedethequeFetcher::canFetch(int type) const { return type == Data::Collection::ComicBook; } // No UPC or Raw for now. bool BedethequeFetcher::canSearch(FetchKey k) const { return k == Title || k == Keyword || k == ISBN; } void BedethequeFetcher::readConfigHook(const KConfigGroup& config_) { Q_UNUSED(config_); } void BedethequeFetcher::search() { m_started = true; m_matches.clear(); // special case for updates which include the BD link as Raw request if(request().key == Raw) { QUrl u(request().value); u.setHost(QStringLiteral("m.bedetheque.com")); // use mobile site for easier parsing m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); m_job->addMetaData(QStringLiteral("referrer"), QString::fromLatin1(BD_BASE_URL)); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); // different slot here - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotLinkComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &BedethequeFetcher::slotLinkComplete); return; } QUrl u(QString::fromLatin1(BD_BASE_URL)); /* fetchToken(); if(m_token.isEmpty()) { myDebug() << "empty token"; stop(); return; } */ QUrlQuery q; switch(request().key) { case Title: q.addQueryItem(QStringLiteral("RechTitre"), request().value); break; case Keyword: q.addQueryItem(QStringLiteral("RechSerie"), request().value); break; case ISBN: q.addQueryItem(QStringLiteral("RechISBN"), ISBNValidator::cleanValue(request().value)); break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } // q.addQueryItem(QLatin1String("csrf_token_bedetheque"), m_token); u.setQuery(q); // myDebug() << "url: " << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); m_job->addMetaData(QStringLiteral("referrer"), QString::fromLatin1(BD_BASE_URL)); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &BedethequeFetcher::slotComplete); } void BedethequeFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void BedethequeFetcher::slotComplete(KJob*) { if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; QString output = Tellico::decodeHTML(data); #if 0 myWarning() << "Remove debug from bedethequefetcher.cpp"; QFile f(QString::fromLatin1("/tmp/testbd.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t << output; } f.close(); #endif const int pos_list = output.indexOf(QLatin1String("
  • "), 0, Qt::CaseInsensitive); if(pos_list == -1) { myDebug() << "No results found"; stop(); return; } const int pos_end = output.indexOf(QLatin1String(""), pos_list+1, Qt::CaseInsensitive); output = output.mid(pos_list, pos_end-pos_list); QString pat = QStringLiteral("https://m.bedetheque.com/BD"); QRegExp anchorRx(QLatin1String("]*href\\s*=\\s*[\"'](") + QRegExp::escape(pat) + QLatin1String("[^\"']*)\"[^>]*>(.*)(.*)<")); spanRx.setMinimal(true); for(int pos = anchorRx.indexIn(output); m_started && pos > -1; pos = anchorRx.indexIn(output, pos+anchorRx.matchedLength())) { QString url = anchorRx.cap(1); if(url.isEmpty()) { continue; } const QString result = anchorRx.cap(2); if(result.isEmpty()) { continue; } QString title; QStringList desc; for(int pos2 = spanRx.indexIn(result); pos2 > -1; pos2 = spanRx.indexIn(result, pos2+spanRx.matchedLength())) { QString cname = spanRx.cap(1); QString value = spanRx.cap(2); if(cname == QLatin1String("serie")) { desc += value; } else if(cname == QLatin1String("titre")) { title = value; } else if(cname == QLatin1String("dl")) { desc += value; } } if(!title.isEmpty() && !url.isEmpty()) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), title, desc.join(QLatin1String(" "))); m_matches.insert(r->uid, QUrl(url)); emit signalResultFound(r); } } stop(); } // slot called after downloading the exact link void BedethequeFetcher::slotLinkComplete(KJob*) { if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; QString output = Tellico::decodeHTML(data); Data::EntryPtr entry = parseEntry(output); if(!entry) { myDebug() << "error in processing entry"; stop(); return; } FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_matches.insert(r->uid, QUrl(request().value)); m_entries.insert(r->uid, entry); // keep for later emit signalResultFound(r); stop(); } Tellico::Data::EntryPtr BedethequeFetcher::fetchEntryHook(uint uid_) { // if we already grabbed this one, then just pull it out of the dict Data::EntryPtr entry = m_entries[uid_]; if(entry) { return entry; } QUrl url = m_matches[uid_]; if(url.isEmpty()) { myWarning() << "no url in map"; return Data::EntryPtr(); } QString results = Tellico::decodeHTML(FileHandler::readDataFile(url, true)); if(results.isEmpty()) { myDebug() << "no text results"; return Data::EntryPtr(); } // myDebug() << url.url(); #if 0 myWarning() << "Remove debug from bedethequefetcher.cpp"; QFile f(QLatin1String("/tmp/testbditem.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << results; } f.close(); #endif entry = parseEntry(results); if(!entry) { myDebug() << "error in processing entry"; return Data::EntryPtr(); } m_entries.insert(uid_, entry); // keep for later return entry; } Tellico::Data::EntryPtr BedethequeFetcher::parseEntry(const QString& str_) { Data::CollPtr coll(new Data::ComicBookCollection(true)); // map captions in HTML to field names QHash fieldMap; fieldMap.insert(QStringLiteral("Série"), QStringLiteral("series")); fieldMap.insert(QStringLiteral("Titre"), QStringLiteral("title")); fieldMap.insert(QStringLiteral("Origine"), QStringLiteral("country")); // fieldMap.insert(QLatin1String("Format"), QLatin1String("binding")); fieldMap.insert(QStringLiteral("Scénario"), QStringLiteral("writer")); fieldMap.insert(QStringLiteral("Dessin"), QStringLiteral("artist")); fieldMap.insert(QStringLiteral("Dépot légal"), QStringLiteral("pub_year")); fieldMap.insert(QStringLiteral("Editeur"), QStringLiteral("publisher")); fieldMap.insert(QStringLiteral("Planches"), QStringLiteral("pages")); fieldMap.insert(QStringLiteral("Style"), QStringLiteral("genre")); fieldMap.insert(QStringLiteral("Tome"), QStringLiteral("issue")); fieldMap.insert(QStringLiteral("Collection"), QStringLiteral("edition")); if(optionalFields().contains(QStringLiteral("isbn"))) { Data::FieldPtr field = Data::Field::createDefaultField(Data::Field::IsbnField); coll->addField(field); fieldMap.insert(QStringLiteral("ISBN"), field->name()); } if(optionalFields().contains(QStringLiteral("colorist"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("colorist"), i18n("Colorist"))); field->setCategory(i18n("General")); field->setFlags(Data::Field::AllowCompletion | Data::Field::AllowMultiple | Data::Field::AllowGrouped); field->setFormatType(FieldFormat::FormatName); coll->addField(field); fieldMap.insert(QStringLiteral("Couleurs"), QStringLiteral("colorist")); } if(optionalFields().contains(QStringLiteral("lien-bel"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("lien-bel"), i18n("Bedetheque Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); } QRegExp tagRx(QLatin1String("<.*>")); tagRx.setMinimal(true); QRegExp yearRx(QLatin1String("\\d{4}")); // the negative lookahead with "no-border" is for multiple values QString pat = QStringLiteral("(.+)
  • (?!\\s*
  • ::Iterator it = fieldMap.begin(); it != fieldMap.end(); ++it) { QRegExp infoRx(pat.arg(it.key())); infoRx.setMinimal(true); if(infoRx.indexIn(str_) == -1) { continue; } if(it.value() == QLatin1String("pub_year")) { QString data = infoRx.cap(1).remove(tagRx).simplified(); if(yearRx.indexIn(data) > -1) { entry->setField(it.value(), yearRx.cap(0)); } } else if(it.value() == QLatin1String("writer") || it.value() == QLatin1String("artist") || it.value() == QLatin1String("publisher") || it.value() == QLatin1String("colorist")) { // catch multiple people QString value = infoRx.cap(1); // split the values with the "no-border" CSS value.replace(QLatin1String("
  • "), FieldFormat::delimiterString()); value = FieldFormat::fixupValue(value.remove(tagRx).simplified()); entry->setField(it.value(), value); } else if(it.value() == QLatin1String("genre")) { // replace comma with semi-colons to effectively split string values QString value = infoRx.cap(1).remove(tagRx).simplified(); value.replace(QLatin1String(", "), FieldFormat::delimiterString()); entry->setField(it.value(), value); } else { entry->setField(it.value(), infoRx.cap(1).remove(tagRx).simplified()); } // myDebug() << it.value() << entry->field(it.value()); } QRegExp imgRx(QLatin1String(" -1) { QUrl u(imgRx.cap(1)); QString id = ImageFactory::addImage(u, true); if(!id.isEmpty()) { entry->setField(QStringLiteral("cover"), id); } } if(optionalFields().contains(QStringLiteral("comments"))) { QRegExp chronRx(QLatin1String("La chronique\\s*
  • \\s*]*>(.*)")); chronRx.setMinimal(true); if(chronRx.indexIn(str_) > -1) { entry->setField(QStringLiteral("comments"), chronRx.cap(1).trimmed()); } } if(optionalFields().contains(QStringLiteral("lien-bel"))) { QRegExp linkRx(QLatin1String(" -1) { entry->setField(QStringLiteral("lien-bel"), linkRx.cap(1)); } } return entry; } Tellico::Fetch::FetchRequest BedethequeFetcher::updateRequest(Data::EntryPtr entry_) { QString l = entry_->field(QStringLiteral("lien-bel")); if(!l.isEmpty()) { return FetchRequest(Fetch::Raw, l); } QString i = entry_->field(QStringLiteral("isbn")); if(!i.isEmpty()) { return FetchRequest(Fetch::ISBN, i); } QString t = entry_->field(QStringLiteral("title")); if(!t.isEmpty()) { return FetchRequest(Fetch::Title, t); } return FetchRequest(); } void BedethequeFetcher::fetchToken() { QRegExp tokenRx(QLatin1String("name\\s*=\\s*\"csrf_token_bedetheque\"\\s*value\\s*=\\s*\"([^\"]+)\"")); const QUrl url(QStringLiteral("https://www.bedetheque.com/search/albums")); const QString text = FileHandler::readTextFile(url, true /*quiet*/); if(tokenRx.indexIn(text) > -1) { m_token = tokenRx.cap(1); } } Tellico::Fetch::ConfigWidget* BedethequeFetcher::configWidget(QWidget* parent_) const { return new BedethequeFetcher::ConfigWidget(parent_, this); } QString BedethequeFetcher::defaultName() { return QStringLiteral("Bedetheque"); } QString BedethequeFetcher::defaultIcon() { return favIcon("http://www.bedetheque.com"); } //static Tellico::StringHash BedethequeFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("colorist")] = i18n("Colorist"); hash[QStringLiteral("comments")] = i18n("Comments"); hash[QStringLiteral("isbn")] = i18n("ISBN#"); // use the field name that the bedetheque.py script did, to maintain backwards compatibility hash[QStringLiteral("lien-bel")] = i18n("Bedetheque Link"); return hash; } BedethequeFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const BedethequeFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(BedethequeFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } QString BedethequeFetcher::ConfigWidget::preferredName() const { return BedethequeFetcher::defaultName(); } diff --git a/src/fetch/bibliosharefetcher.cpp b/src/fetch/bibliosharefetcher.cpp index 1033c244..e6e69390 100644 --- a/src/fetch/bibliosharefetcher.cpp +++ b/src/fetch/bibliosharefetcher.cpp @@ -1,215 +1,215 @@ /*************************************************************************** Copyright (C) 2011 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "bibliosharefetcher.h" #include "../utils/isbnvalidator.h" #include "../entry.h" #include "../images/imagefactory.h" #include "../images/imageinfo.h" #include "../tellico_debug.h" #include #include #include #include #include #include namespace { static const char* BIBLIOSHARE_BASE_URL = "http://www.biblioshare.org/BNCServices/BNCServices.asmx/"; static const char* BIBLIOSHARE_TOKEN = "nsnqwebh87kstlty"; } using namespace Tellico; using Tellico::Fetch::BiblioShareFetcher; BiblioShareFetcher::BiblioShareFetcher(QObject* parent_) : XMLFetcher(parent_) , m_token(QLatin1String(BIBLIOSHARE_TOKEN)) { setLimit(1); setXSLTFilename(QStringLiteral("biblioshare2tellico.xsl")); } BiblioShareFetcher::~BiblioShareFetcher() { } QString BiblioShareFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } // https://www.booknetcanada.ca/get-a-token QString BiblioShareFetcher::attribution() const { return i18n("Data provided by BNC BiblioShare."); } bool BiblioShareFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex; } void BiblioShareFetcher::readConfigHook(const KConfigGroup& config_) { QString k = config_.readEntry("Token", BIBLIOSHARE_TOKEN); if(!k.isEmpty()) { m_token = k; } } QUrl BiblioShareFetcher::searchUrl() { QUrl u(QString::fromLatin1(BIBLIOSHARE_BASE_URL)); u.setPath(u.path() + QStringLiteral("BiblioSimple")); QUrlQuery q; q.addQueryItem(QStringLiteral("Token"), m_token); switch(request().key) { case ISBN: { // only grab first value QString v = request().value.section(QLatin1Char(';'), 0); v = ISBNValidator::isbn13(v); v.remove(QLatin1Char('-')); q.addQueryItem(QStringLiteral("EAN"), v); } break; default: return QUrl(); } u.setQuery(q); // myDebug() << "url:" << u.url(); return u; } Tellico::Data::EntryPtr BiblioShareFetcher::fetchEntryHookData(Data::EntryPtr entry_) { Q_ASSERT(entry_); if(!entry_) { myWarning() << "no entry"; return entry_; } // if the entry cover is not set, go ahead and try to fetch it if(entry_->field(QStringLiteral("cover")).isEmpty()) { QString isbn = ISBNValidator::cleanValue(entry_->field(QStringLiteral("isbn"))); if(!isbn.isEmpty()) { isbn = ISBNValidator::isbn13(isbn); isbn.remove(QLatin1Char('-')); QUrl imageUrl(QString::fromLatin1(BIBLIOSHARE_BASE_URL)); imageUrl.setPath(imageUrl.path() + QStringLiteral("Images")); QUrlQuery q; q.addQueryItem(QStringLiteral("Token"), m_token); q.addQueryItem(QStringLiteral("EAN"), isbn); // the actual values for SAN Thumbnail don't seem to matter, they just can't be empty q.addQueryItem(QStringLiteral("SAN"), QStringLiteral("string")); q.addQueryItem(QStringLiteral("Thumbnail"), QStringLiteral("cover")); imageUrl.setQuery(q); const QString id = ImageFactory::addImage(imageUrl, true); if(!id.isEmpty()) { // placeholder images are 120x120 or 1x1 Data::ImageInfo info = ImageFactory::imageInfo(id); if((info.width() != 120 || info.height() != 120) && (info.width() != 1 || info.height() != 1)) { entry_->setField(QStringLiteral("cover"), id); } } } } return entry_; } Tellico::Fetch::FetchRequest BiblioShareFetcher::updateRequest(Data::EntryPtr entry_) { const QString isbn = entry_->field(QStringLiteral("isbn")); if(!isbn.isEmpty()) { return FetchRequest(Fetch::ISBN, isbn); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* BiblioShareFetcher::configWidget(QWidget* parent_) const { return new BiblioShareFetcher::ConfigWidget(parent_, this); } QString BiblioShareFetcher::defaultName() { return QStringLiteral("BiblioShare"); } QString BiblioShareFetcher::defaultIcon() { return favIcon("https://www.booknetcanada.ca/biblioshare"); } BiblioShareFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const BiblioShareFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QStringLiteral("https://www.booknetcanada.ca/get-a-token")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_tokenEdit = new QLineEdit(optionsWidget()); - connect(m_tokenEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_tokenEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_tokenEdit, row, 1); QString w = i18n("The default Tellico key may be used, but searching may fail due to reaching access limits."); label->setWhatsThis(w); m_tokenEdit->setWhatsThis(w); label->setBuddy(m_tokenEdit); l->setRowStretch(++row, 10); if(fetcher_) { // only show the key if it is not the default Tellico one... // that way the user is prompted to apply for their own if(fetcher_->m_token != QLatin1String(BIBLIOSHARE_TOKEN)) { m_tokenEdit->setText(fetcher_->m_token); } } } void BiblioShareFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString token = m_tokenEdit->text().trimmed(); if(!token.isEmpty()) { config_.writeEntry("Token", token); } } QString BiblioShareFetcher::ConfigWidget::preferredName() const { return BiblioShareFetcher::defaultName(); } diff --git a/src/fetch/bibsonomyfetcher.cpp b/src/fetch/bibsonomyfetcher.cpp index 01390f77..139058e5 100644 --- a/src/fetch/bibsonomyfetcher.cpp +++ b/src/fetch/bibsonomyfetcher.cpp @@ -1,195 +1,195 @@ /*************************************************************************** Copyright (C) 2007-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "bibsonomyfetcher.h" #include "../translators/bibteximporter.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../collection.h" #include "../entry.h" #include "../core/netaccess.h" #include "../core/filehandler.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include namespace { // always bibtex static const char* BIBSONOMY_BASE_URL = "http://bibsonomy.org/"; static const int BIBSONOMY_MAX_RESULTS = 20; } using namespace Tellico; using Tellico::Fetch::BibsonomyFetcher; BibsonomyFetcher::BibsonomyFetcher(QObject* parent_) : Fetcher(parent_), m_job(nullptr), m_started(false) { } BibsonomyFetcher::~BibsonomyFetcher() { } QString BibsonomyFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool BibsonomyFetcher::canFetch(int type) const { return type == Data::Collection::Bibtex; } void BibsonomyFetcher::readConfigHook(const KConfigGroup&) { } void BibsonomyFetcher::search() { m_started = true; // myDebug() << "value = " << value_; QUrl u(QString::fromLatin1(BIBSONOMY_BASE_URL)); u.setPath(QStringLiteral("/bib/")); switch(request().key) { case Person: u.setPath(u.path() + QStringLiteral("author/%1").arg(request().value)); break; case Keyword: u.setPath(u.path() + QStringLiteral("search/%1").arg(request().value)); break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } QUrlQuery q; q.addQueryItem(QStringLiteral("items"), QString::number(BIBSONOMY_MAX_RESULTS)); u.setQuery(q); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &BibsonomyFetcher::slotComplete); } void BibsonomyFetcher::stop() { if(!m_started) { return; } // myDebug(); if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void BibsonomyFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; Import::BibtexImporter imp(QString::fromUtf8(data.constData(), data.size())); Data::CollPtr coll = imp.collection(); if(!coll) { myDebug() << "no valid result"; stop(); return; } Data::EntryList entries = coll->entries(); foreach(Data::EntryPtr entry, entries) { if(!m_started) { // might get aborted break; } FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, Data::EntryPtr(entry)); emit signalResultFound(r); } stop(); // required } Tellico::Data::EntryPtr BibsonomyFetcher::fetchEntryHook(uint uid_) { return m_entries[uid_]; } Tellico::Fetch::FetchRequest BibsonomyFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Fetch::Keyword, title); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* BibsonomyFetcher::configWidget(QWidget* parent_) const { return new BibsonomyFetcher::ConfigWidget(parent_, this); } QString BibsonomyFetcher::defaultName() { return QStringLiteral("Bibsonomy"); } QString BibsonomyFetcher::defaultIcon() { return favIcon("https://www.bibsonomy.org"); } BibsonomyFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const BibsonomyFetcher*) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); } void BibsonomyFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) { } QString BibsonomyFetcher::ConfigWidget::preferredName() const { return BibsonomyFetcher::defaultName(); } diff --git a/src/fetch/comicvinefetcher.cpp b/src/fetch/comicvinefetcher.cpp index 2ba3b24e..6bd8df18 100644 --- a/src/fetch/comicvinefetcher.cpp +++ b/src/fetch/comicvinefetcher.cpp @@ -1,278 +1,278 @@ /*************************************************************************** Copyright (C) 2019 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "comicvinefetcher.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include namespace { static const int COMICVINE_MAX_RETURNS_TOTAL = 20; static const char* COMICVINE_API_URL = "https://www.comicvine.com/api"; static const char* COMICVINE_API_KEY = "6e4b19eeb8ccec8e2f026169d19adf57850d378e"; } using namespace Tellico; using Tellico::Fetch::ComicVineFetcher; ComicVineFetcher::ComicVineFetcher(QObject* parent_) : XMLFetcher(parent_) , m_total(-1) , m_apiKey(QLatin1String(COMICVINE_API_KEY)) { setLimit(COMICVINE_MAX_RETURNS_TOTAL); setXSLTFilename(QStringLiteral("comicvine2tellico.xsl")); } ComicVineFetcher::~ComicVineFetcher() { } QString ComicVineFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } QString ComicVineFetcher::attribution() const { return i18n("This information was freely provided by Comic Vine."); } bool ComicVineFetcher::canFetch(int type) const { return type == Data::Collection::ComicBook; } void ComicVineFetcher::readConfigHook(const KConfigGroup& config_) { QString k = config_.readEntry("API Key", COMICVINE_API_KEY); if(!k.isEmpty()) { m_apiKey = k; } } void ComicVineFetcher::resetSearch() { m_total = -1; } QUrl ComicVineFetcher::searchUrl() { QUrl u(QString::fromLatin1(COMICVINE_API_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("format"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("api_key"), m_apiKey); switch(request().key) { case Keyword: u.setPath(u.path() + QStringLiteral("/search")); q.addQueryItem(QStringLiteral("query"), request().value); q.addQueryItem(QStringLiteral("resources"), QStringLiteral("issue")); break; default: myWarning() << "key not recognized: " << request().key; return QUrl(); } u.setQuery(q); // myDebug() << "url: " << u.url(); return u; } void ComicVineFetcher::parseData(QByteArray& data_) { Q_UNUSED(data_); } Tellico::Data::EntryPtr ComicVineFetcher::fetchEntryHookData(Data::EntryPtr entry_) { Q_ASSERT(entry_); const QString url = entry_->field(QStringLiteral("comicvine-api")); if(url.isEmpty()) { myDebug() << "no comicvine api url found"; return entry_; } QUrl u(url); QUrlQuery q; q.addQueryItem(QStringLiteral("format"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("api_key"), m_apiKey); u.setQuery(q); // myDebug() << "url: " << u; // quiet QString output = FileHandler::readXMLFile(u, true); #if 0 myWarning() << "Remove output debug from comicvinefetcher.cpp"; QFile f(QStringLiteral("/tmp/test2.xml")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << output; } f.close(); #endif Import::TellicoImporter imp(xsltHandler()->applyStylesheet(output)); // be quiet when loading images imp.setOptions(imp.options() ^ Import::ImportShowImageErrors); Data::CollPtr coll = imp.collection(); if(!coll || coll->entryCount() == 0) { myWarning() << "no collection pointer"; return entry_; } if(coll->entryCount() > 1) { myDebug() << "weird, more than one entry found"; } // grab the publisher from the volume link const QString volUrl = entry_->field(QStringLiteral("comicvine-volume-api")); if(!volUrl.isEmpty()) { QUrl vu(volUrl); // easier to use JSON here QUrlQuery q; q.addQueryItem(QStringLiteral("format"), QStringLiteral("json")); q.addQueryItem(QStringLiteral("api_key"), m_apiKey); vu.setQuery(q); // myDebug() << "volume url: " << vu; QByteArray data = FileHandler::readDataFile(vu, true /* quiet */); #if 0 myWarning() << "Remove JSON output debug from comicvinefetcher.cpp"; QFile f2(QStringLiteral("/tmp/test2.json")); if(f2.open(QIODevice::WriteOnly)) { QTextStream t(&f2); t << data; } f2.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); QVariantMap map = doc.object().toVariantMap().value(QLatin1String("results")).toMap(); const QString pub = mapValue(map, "publisher", "name"); if(!pub.isEmpty()) { Data::EntryPtr e = coll->entries().front(); if(e) { e->setField(QStringLiteral("publisher"), pub); } } } // don't want to include api link coll->removeField(QStringLiteral("comicvine-api")); coll->removeField(QStringLiteral("comicvine-volume-api")); return coll->entries().front(); } Tellico::Fetch::FetchRequest ComicVineFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Keyword, title); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* ComicVineFetcher::configWidget(QWidget* parent_) const { return new ComicVineFetcher::ConfigWidget(parent_, this); } QString ComicVineFetcher::defaultName() { return QStringLiteral("Comic Vine"); } QString ComicVineFetcher::defaultIcon() { return favIcon("https://comicvine.gamespot.com"); } Tellico::StringHash ComicVineFetcher::allOptionalFields() { StringHash hash; // hash[QStringLiteral("colorist")] = i18n("Colorist"); hash[QStringLiteral("comicvine")] = i18n("Comic Vine Link"); return hash; } ComicVineFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ComicVineFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QLatin1String("http://api.comicvine.com")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_apiKeyEdit = new QLineEdit(optionsWidget()); - connect(m_apiKeyEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_apiKeyEdit, row, 1); QString w = i18n("The default Tellico key may be used, but searching may fail due to reaching access limits."); label->setWhatsThis(w); m_apiKeyEdit->setWhatsThis(w); label->setBuddy(m_apiKeyEdit); l->setRowStretch(++row, 10); // now add additional fields widget addFieldsWidget(ComicVineFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); if(fetcher_) { // only show the key if it is not the default Tellico one... // that way the user is prompted to apply for their own if(fetcher_->m_apiKey != QLatin1String(COMICVINE_API_KEY)) { m_apiKeyEdit->setText(fetcher_->m_apiKey); } } } void ComicVineFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString apiKey = m_apiKeyEdit->text().trimmed(); if(!apiKey.isEmpty()) { config_.writeEntry("API Key", apiKey); } } QString ComicVineFetcher::ConfigWidget::preferredName() const { return ComicVineFetcher::defaultName(); } diff --git a/src/fetch/configwidget.cpp b/src/fetch/configwidget.cpp index cae95b3d..0631521a 100644 --- a/src/fetch/configwidget.cpp +++ b/src/fetch/configwidget.cpp @@ -1,102 +1,102 @@ /*************************************************************************** Copyright (C) 2003-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "configwidget.h" #include #include #include #include #include using Tellico::Fetch::ConfigWidget; ConfigWidget::ConfigWidget(QWidget* parent_) : QWidget(parent_), m_modified(false), m_accepted(false) { QHBoxLayout* boxLayout = new QHBoxLayout(this); boxLayout->setSpacing(10); boxLayout->setMargin(0); QGroupBox* gvbox = new QGroupBox(i18n("Source Options"), this); boxLayout->addWidget(gvbox, 10 /*stretch*/); QVBoxLayout* vbox = new QVBoxLayout(); m_optionsWidget = new QWidget(gvbox); vbox->addWidget(m_optionsWidget); vbox->addStretch(1); gvbox->setLayout(vbox); } bool ConfigWidget::shouldSave() const { return m_modified && m_accepted; } void ConfigWidget::setAccepted(bool accepted_) { m_accepted = accepted_; } -void ConfigWidget::slotSetModified(bool modified_) { - m_modified = modified_; +void ConfigWidget::slotSetModified() { + m_modified = true;; } void ConfigWidget::addFieldsWidget(const Tellico::StringHash& customFields_, const QStringList& fieldsToAdd_) { if(customFields_.isEmpty()) { return; } QGroupBox* gbox = new QGroupBox(i18n("Available Fields"), this); static_cast(layout())->addWidget(gbox); QVBoxLayout* vbox = new QVBoxLayout(); for(StringHash::ConstIterator it = customFields_.begin(); it != customFields_.end(); ++it) { QCheckBox* cb = new QCheckBox(it.value(), gbox); m_fields.insert(it.key(), cb); if(fieldsToAdd_.contains(it.key())) { cb->setChecked(true); } - connect(cb, SIGNAL(clicked()), SLOT(slotSetModified())); + connect(cb, &QAbstractButton::clicked, this, &ConfigWidget::slotSetModified); vbox->addWidget(cb); } vbox->addStretch(1); gbox->setLayout(vbox); KAcceleratorManager::manage(this); } void ConfigWidget::saveConfig(KConfigGroup& config_) { QStringList fields; QHash::const_iterator it = m_fields.constBegin(); for( ; it != m_fields.constEnd(); ++it) { if(it.value()->isChecked()) { fields << it.key(); } } config_.writeEntry(QStringLiteral("Custom Fields"), fields); saveConfigHook(config_); - slotSetModified(false); + m_modified = false; } QWidget* ConfigWidget::optionsWidget() { return m_optionsWidget; } diff --git a/src/fetch/configwidget.h b/src/fetch/configwidget.h index dc4352e4..9c5fa215 100644 --- a/src/fetch/configwidget.h +++ b/src/fetch/configwidget.h @@ -1,90 +1,90 @@ /*************************************************************************** Copyright (C) 2003-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #ifndef TELLICO_FETCHCONFIGWIDGET_H #define TELLICO_FETCHCONFIGWIDGET_H #include "../datavectors.h" #include #include #include class KConfigGroup; class QStringList; namespace Tellico { namespace Fetch { /** * @author Robby Stephenson */ class ConfigWidget : public QWidget { Q_OBJECT public: ConfigWidget(QWidget* parent); virtual ~ConfigWidget() {} bool shouldSave() const; void setAccepted(bool accepted); virtual void readConfig(const KConfigGroup&) {} /** * Saves any configuration options. The config group must be * set before calling this function. * * @param config_ The KConfig pointer */ void saveConfig(KConfigGroup& config); /** * Called when a fetcher data source is removed. Useful for any cleanup work necessary. * The ExecExternalFetcher might need to remove the script, for example. * Because of the way the ConfigDialog is setup, easier to have that in the ConfigWidget * class than in the Fetcher class itself */ virtual void removed() {} virtual QString preferredName() const = 0; Q_SIGNALS: void signalName(const QString& name); public Q_SLOTS: - void slotSetModified(bool modified = true); + void slotSetModified(); protected: QWidget* optionsWidget(); void addFieldsWidget(const StringHash& customFields, const QStringList& fieldsToAdd); virtual void saveConfigHook(KConfigGroup&) {} private: bool m_modified; bool m_accepted; QWidget* m_optionsWidget; QHash m_fields; }; } } #endif diff --git a/src/fetch/crossreffetcher.cpp b/src/fetch/crossreffetcher.cpp index 8e5b7986..24fabb04 100644 --- a/src/fetch/crossreffetcher.cpp +++ b/src/fetch/crossreffetcher.cpp @@ -1,382 +1,380 @@ /*************************************************************************** Copyright (C) 2007-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "crossreffetcher.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../collection.h" #include "../entry.h" #include "../core/netaccess.h" #include "../images/imagefactory.h" #include "../utils/wallet.h" #include "../utils/datafileregistry.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #define CROSSREF_USE_UNIXREF namespace { static const char* CROSSREF_BASE_URL = "http://www.crossref.org/openurl/"; } using namespace Tellico; using namespace Tellico::Fetch; using Tellico::Fetch::CrossRefFetcher; CrossRefFetcher::CrossRefFetcher(QObject* parent_) : Fetcher(parent_), m_xsltHandler(nullptr), m_job(nullptr), m_started(false) { } CrossRefFetcher::~CrossRefFetcher() { delete m_xsltHandler; m_xsltHandler = nullptr; } QString CrossRefFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool CrossRefFetcher::canFetch(int type) const { return type == Data::Collection::Bibtex; } void CrossRefFetcher::readConfigHook(const KConfigGroup& config_) { m_user = config_.readEntry("User"); m_password = config_.readEntry("Password"); m_email = config_.readEntry("Email"); } void CrossRefFetcher::search() { m_started = true; readWallet(); if(m_email.isEmpty() && (m_user.isEmpty() || m_password.isEmpty())) { myDebug() << i18n("%1 requires a username and password.", source()); message(i18n("%1 requires a username and password.", source()), MessageHandler::Error); stop(); return; } // myDebug() << "value = " << value_; QUrl u = searchURL(request().key, request().value); if(u.isEmpty()) { stop(); return; } m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &CrossRefFetcher::slotComplete); } void CrossRefFetcher::stop() { if(!m_started) { return; } // myDebug(); if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void CrossRefFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; #if 0 myWarning() << "Remove debug from crossreffetcher.cpp"; QFile f(QLatin1String("/tmp/test.xml")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif if(!m_xsltHandler) { initXSLTHandler(); if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading stop(); return; } } // assume result is always utf-8 QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(data.constData(), data.size())); Import::TellicoImporter imp(str); Data::CollPtr coll = imp.collection(); if(!coll) { myDebug() << "no valid result"; stop(); return; } Data::EntryList entries = coll->entries(); foreach(Data::EntryPtr entry, entries) { if(!m_started) { // might get aborted break; } FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, Data::EntryPtr(entry)); emit signalResultFound(r); } stop(); // required } Tellico::Data::EntryPtr CrossRefFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries[uid_]; // if URL but no cover image, fetch it if(!entry->field(QStringLiteral("url")).isEmpty()) { Data::CollPtr coll = entry->collection(); Data::FieldPtr field = coll->fieldByName(QStringLiteral("cover")); if(!field && !coll->imageFields().isEmpty()) { field = coll->imageFields().front(); } else if(!field) { field = new Data::Field(QStringLiteral("cover"), i18n("Front Cover"), Data::Field::Image); coll->addField(field); } if(entry->field(field).isEmpty()) { QPixmap pix = NetAccess::filePreview(QUrl::fromUserInput(entry->field(QStringLiteral("url")))); if(!pix.isNull()) { QString id = ImageFactory::addImage(pix, QStringLiteral("PNG")); if(!id.isEmpty()) { entry->setField(field, id); } } } } return entry; } void CrossRefFetcher::initXSLTHandler() { #ifdef CROSSREF_USE_UNIXREF QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("unixref2tellico.xsl")); #else QString xsltfile = DataFileRegistry::self()->locate(QLatin1String("crossref2tellico.xsl")); #endif if(xsltfile.isEmpty()) { #ifdef CROSSREF_USE_UNIXREF myWarning() << "can not locate xslt file: unixref2tellico.xsl"; #else myWarning() << "can not locate xslt file: crossref2tellico.xsl"; #endif return; } QUrl u = QUrl::fromLocalFile(xsltfile); delete m_xsltHandler; m_xsltHandler = new XSLTHandler(u); if(!m_xsltHandler->isValid()) { myWarning() << "error in crossref2tellico.xsl."; delete m_xsltHandler; m_xsltHandler = nullptr; return; } } QUrl CrossRefFetcher::searchURL(FetchKey key_, const QString& value_) const { QUrl u(QString::fromLatin1(CROSSREF_BASE_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("noredirect"), QStringLiteral("true")); q.addQueryItem(QStringLiteral("multihit"), QStringLiteral("true")); #ifdef CROSSREF_USE_UNIXREF q.addQueryItem(QStringLiteral("format"), QStringLiteral("unixref")); #endif if(m_email.isEmpty()) { q.addQueryItem(QStringLiteral("pid"), QStringLiteral("%1:%2").arg(m_user, m_password)); } else { q.addQueryItem(QStringLiteral("pid"), m_email); } switch(key_) { case DOI: q.addQueryItem(QStringLiteral("rft_id"), QStringLiteral("info:doi/%1").arg(value_)); break; default: myWarning() << "key not recognized: " << key_; return QUrl(); } u.setQuery(q); // myDebug() << "url: " << u.url(); return u; } Tellico::Fetch::FetchRequest CrossRefFetcher::updateRequest(Data::EntryPtr entry_) { QString doi = entry_->field(QStringLiteral("doi")); if(!doi.isEmpty()) { return FetchRequest(Fetch::DOI, doi); } #if 0 // optimistically try searching for title and rely on Collection::sameEntry() to figure things out QString t = entry_->field(QLatin1String("title")); if(!t.isEmpty()) { return FetchRequest(Fetch::Title, t); } #endif return FetchRequest(); } void CrossRefFetcher::readWallet() const { if(m_user.isEmpty() || m_password.isEmpty()) { QMap map = Wallet::self()->readWalletMap(QStringLiteral("crossref.org")); if(!map.isEmpty()) { m_user = map.value(QStringLiteral("username")); m_password = map.value(QStringLiteral("password")); } } } Tellico::Fetch::ConfigWidget* CrossRefFetcher::configWidget(QWidget* parent_) const { return new CrossRefFetcher::ConfigWidget(parent_, this); } QString CrossRefFetcher::defaultName() { return QStringLiteral("CrossRef"); // no translation } QString CrossRefFetcher::defaultIcon() { return favIcon("http://crossref.org"); } CrossRefFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const CrossRefFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = 0; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QLatin1String("http://www.crossref.org/requestaccount/")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("&Username: "), optionsWidget()); l->addWidget(label, ++row, 0); m_userEdit = new QLineEdit(optionsWidget()); - connect(m_userEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_userEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_userEdit, row, 1); QString w = i18n("A username and password is required to access the CrossRef service."); label->setWhatsThis(w); m_userEdit->setWhatsThis(w); label->setBuddy(m_userEdit); label = new QLabel(i18n("&Password: "), optionsWidget()); l->addWidget(label, ++row, 0); m_passEdit = new QLineEdit(optionsWidget()); // m_passEdit->setEchoMode(QLineEdit::PasswordEchoOnEdit); - connect(m_passEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_passEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_passEdit, row, 1); label->setWhatsThis(w); m_passEdit->setWhatsThis(w); label->setBuddy(m_passEdit); label = new QLabel(i18n("For some accounts, only an email address is required."), optionsWidget()); l->addWidget(label, ++row, 0, 1, 2); label = new QLabel(i18n("Email: "), optionsWidget()); l->addWidget(label, ++row, 0); m_emailEdit = new QLineEdit(optionsWidget()); - connect(m_emailEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_emailEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_emailEdit, row, 1); label->setBuddy(m_emailEdit); if(fetcher_) { fetcher_->readWallet(); // make sure that the wallet values are read m_userEdit->setText(fetcher_->m_user); m_passEdit->setText(fetcher_->m_password); m_emailEdit->setText(fetcher_->m_email); } } void CrossRefFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString s = m_userEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("User", s); } s = m_passEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("Password", s); } s = m_emailEdit->text().trimmed(); if(!s.isEmpty()) { config_.writeEntry("Email", s); } - - slotSetModified(false); } QString CrossRefFetcher::ConfigWidget::preferredName() const { return CrossRefFetcher::defaultName(); } diff --git a/src/fetch/discogsfetcher.cpp b/src/fetch/discogsfetcher.cpp index cc12c1b5..01f9dfc5 100644 --- a/src/fetch/discogsfetcher.cpp +++ b/src/fetch/discogsfetcher.cpp @@ -1,466 +1,466 @@ /*************************************************************************** Copyright (C) 2008-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include // for TELLICO_VERSION #include "discogsfetcher.h" #include "../collections/musiccollection.h" #include "../images/imagefactory.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../core/filehandler.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int DISCOGS_MAX_RETURNS_TOTAL = 20; static const char* DISCOGS_API_URL = "https://api.discogs.com"; } using namespace Tellico; using Tellico::Fetch::DiscogsFetcher; DiscogsFetcher::DiscogsFetcher(QObject* parent_) : Fetcher(parent_) , m_started(false) { } DiscogsFetcher::~DiscogsFetcher() { } QString DiscogsFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool DiscogsFetcher::canSearch(FetchKey k) const { return k == Title || k == Person || k == Keyword; } bool DiscogsFetcher::canFetch(int type) const { return type == Data::Collection::Album; } void DiscogsFetcher::readConfigHook(const KConfigGroup& config_) { QString k = config_.readEntry("API Key"); if(!k.isEmpty()) { m_apiKey = k; } } void DiscogsFetcher::search() { m_started = true; if(m_apiKey.isEmpty()) { myDebug() << "empty API key"; message(i18n("An access key is required to use this data source.") + QLatin1Char(' ') + i18n("Those values must be entered in the data source settings."), MessageHandler::Error); stop(); return; } QUrl u(QString::fromLatin1(DISCOGS_API_URL)); QUrlQuery q; switch(request().key) { case Title: u.setPath(QStringLiteral("/database/search")); q.addQueryItem(QStringLiteral("release_title"), request().value); q.addQueryItem(QStringLiteral("type"), QStringLiteral("release")); break; case Person: u.setPath(QStringLiteral("/database/search")); q.addQueryItem(QStringLiteral("artist"), request().value); q.addQueryItem(QStringLiteral("type"), QStringLiteral("release")); break; case Keyword: u.setPath(QStringLiteral("/database/search")); q.addQueryItem(QStringLiteral("q"), request().value); break; case Raw: u.setPath(QStringLiteral("/database/search")); q.setQuery(request().value); break; default: myWarning() << "key not recognized:" << request().key; stop(); return; } q.addQueryItem(QStringLiteral("token"), m_apiKey); u.setQuery(q); // myDebug() << "url: " << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); m_job->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("Tellico/%1") .arg(QStringLiteral(TELLICO_VERSION))); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &DiscogsFetcher::slotComplete); } void DiscogsFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } Tellico::Data::EntryPtr DiscogsFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries.value(uid_); if(!entry) { myWarning() << "no entry in dict"; return Data::EntryPtr(); } QString id = entry->field(QStringLiteral("discogs-id")); if(!id.isEmpty()) { // quiet QUrl u(QString::fromLatin1(DISCOGS_API_URL)); u.setPath(QStringLiteral("/releases/%1").arg(id)); QByteArray data = FileHandler::readDataFile(u, true); #if 0 myWarning() << "Remove debug2 from discogsfetcher.cpp (/tmp/test2.json)"; QFile f(QString::fromLatin1("/tmp/test2.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); if(error.error == QJsonParseError::NoError) { populateEntry(entry, doc.object().toVariantMap(), true); } else { myDebug() << "Bad JSON results"; } } const QString image_id = entry->field(QStringLiteral("cover")); // if it's still a url, we need to load it if(image_id.contains(QLatin1Char('/'))) { const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */); if(id.isEmpty()) { myDebug() << "empty id for" << image_id; message(i18n("The cover image could not be loaded."), MessageHandler::Warning); } // empty image ID is ok entry->setField(QStringLiteral("cover"), id); } // don't want to include ID field entry->setField(QStringLiteral("discogs-id"), QString()); return entry; } Tellico::Fetch::FetchRequest DiscogsFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Title, title); } QString artist = entry_->field(QStringLiteral("artist")); if(!artist.isEmpty()) { return FetchRequest(Person, artist); } return FetchRequest(); } void DiscogsFetcher::slotComplete(KJob*) { if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from discogsfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif Data::CollPtr coll(new Data::MusicCollection(true)); // always add ID for fetchEntryHook Data::FieldPtr field(new Data::Field(QStringLiteral("discogs-id"), QStringLiteral("Discogs ID"), Data::Field::Line)); field->setCategory(i18n("General")); coll->addField(field); if(optionalFields().contains(QStringLiteral("discogs"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("discogs"), i18n("Discogs Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); } if(optionalFields().contains(QStringLiteral("nationality"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("nationality"), i18n("Nationality"))); field->setCategory(i18n("General")); field->setFlags(Data::Field::AllowCompletion | Data::Field::AllowMultiple | Data::Field::AllowGrouped); field->setFormatType(FieldFormat::FormatPlain); coll->addField(field); } if(optionalFields().contains(QStringLiteral("producer"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("producer"), i18n("Producer"))); field->setCategory(i18n("General")); field->setFlags(Data::Field::AllowCompletion | Data::Field::AllowMultiple | Data::Field::AllowGrouped); field->setFormatType(FieldFormat::FormatName); coll->addField(field); } QJsonDocument doc = QJsonDocument::fromJson(data); // const QVariantMap resultMap = doc.object().toVariantMap().value(QStringLiteral("feed")).toMap(); const QVariantMap resultMap = doc.object().toVariantMap(); if(mapValue(resultMap, "message").startsWith(QLatin1String("Invalid consumer token"))) { message(i18n("The Discogs.com server reports a token error."), MessageHandler::Error); stop(); return; } int count = 0; foreach(const QVariant& result, resultMap.value(QLatin1String("results")).toList()) { if(count >= DISCOGS_MAX_RETURNS_TOTAL) { break; } // myDebug() << "found result:" << result; Data::EntryPtr entry(new Data::Entry(coll)); populateEntry(entry, result.toMap(), false); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); ++count; } stop(); } void DiscogsFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& resultMap_, bool fullData_) { entry_->setField(QStringLiteral("discogs-id"), mapValue(resultMap_, "id")); entry_->setField(QStringLiteral("title"), mapValue(resultMap_, "title")); entry_->setField(QStringLiteral("year"), mapValue(resultMap_, "year")); entry_->setField(QStringLiteral("genre"), mapValue(resultMap_, "genres")); QStringList artists; foreach(const QVariant& artist, resultMap_.value(QLatin1String("artists")).toList()) { artists << mapValue(artist.toMap(), "name"); } entry_->setField(QStringLiteral("artist"), artists.join(FieldFormat::delimiterString())); QStringList labels; foreach(const QVariant& label, resultMap_.value(QLatin1String("labels")).toList()) { labels << mapValue(label.toMap(), "name"); } entry_->setField(QStringLiteral("label"), labels.join(FieldFormat::delimiterString())); /* thumb value is not always in the full data, so go ahead and set it now */ QString coverUrl = mapValue(resultMap_, "thumb"); if(!coverUrl.isEmpty()) { entry_->setField(QStringLiteral("cover"), coverUrl); } // if we only need cursory data, then we're done if(!fullData_) { return; } // check the formats, it could have multiple // if there is a CD, prefer that in the track list bool hasCD = false; foreach(const QVariant& format, resultMap_.value(QLatin1String("formats")).toList()) { if(mapValue(format.toMap(), "name") == QLatin1String("CD")) { entry_->setField(QStringLiteral("medium"), i18n("Compact Disc")); hasCD = true; } else if(mapValue(format.toMap(), "name") == QLatin1String("Vinyl")) { entry_->setField(QStringLiteral("medium"), i18n("Vinyl")); } else if(mapValue(format.toMap(), "name") == QLatin1String("Cassette")) { entry_->setField(QStringLiteral("medium"), i18n("Cassette")); } else if(!hasCD && mapValue(format.toMap(), "name") == QLatin1String("DVD")) { // sometimes a CD and DVD both are included. If we're using the CD, ignore the DVD entry_->setField(QStringLiteral("medium"), i18n("DVD")); } } QStringList tracks; foreach(const QVariant& track, resultMap_.value(QLatin1String("tracklist")).toList()) { const QVariantMap trackMap = track.toMap(); if(mapValue(trackMap, "type_") != QLatin1String("track")) { continue; } // Releases might include a CD and a DVD, for example // prefer only the tracks on the CD. Allow positions of just numbers if(hasCD && !(mapValue(trackMap, "position").at(0).isNumber() || mapValue(trackMap, "position").startsWith(QLatin1String("CD")))) { continue; } QStringList trackInfo; trackInfo << mapValue(trackMap, "title"); if(trackMap.contains(QStringLiteral("artists"))) { QStringList artists; foreach(const QVariant& artist, trackMap.value(QLatin1String("artists")).toList()) { artists << mapValue(artist.toMap(), "name"); } trackInfo << artists.join(FieldFormat::delimiterString()); } else { trackInfo << entry_->field(QStringLiteral("artist")); } trackInfo << mapValue(trackMap, "duration"); tracks << trackInfo.join(FieldFormat::columnDelimiterString()); } entry_->setField(QStringLiteral("track"), tracks.join(FieldFormat::rowDelimiterString())); if(entry_->collection()->hasField(QStringLiteral("discogs"))) { entry_->setField(QStringLiteral("discogs"), mapValue(resultMap_, "uri")); } if(entry_->collection()->hasField(QStringLiteral("nationality"))) { entry_->setField(QStringLiteral("nationality"), mapValue(resultMap_, "country")); } if(entry_->collection()->hasField(QStringLiteral("producer"))) { QStringList producers; foreach(const QVariant& extraartist, resultMap_.value(QLatin1String("extraartists")).toList()) { if(mapValue(extraartist.toMap(), "role").contains(QStringLiteral("Producer"))) { producers << mapValue(extraartist.toMap(), "name"); } } entry_->setField(QStringLiteral("producer"), producers.join(FieldFormat::delimiterString())); } entry_->setField(QStringLiteral("comments"), mapValue(resultMap_, "notes")); } Tellico::Fetch::ConfigWidget* DiscogsFetcher::configWidget(QWidget* parent_) const { return new DiscogsFetcher::ConfigWidget(parent_, this); } QString DiscogsFetcher::defaultName() { return i18n("Discogs Audio Search"); } QString DiscogsFetcher::defaultIcon() { return favIcon("http://www.discogs.com"); } Tellico::StringHash DiscogsFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("producer")] = i18n("Producer"); hash[QStringLiteral("nationality")] = i18n("Nationality"); hash[QStringLiteral("discogs")] = i18n("Discogs Link"); return hash; } DiscogsFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const DiscogsFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QLatin1String("https://www.discogs.com/developers/#page:authentication")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("User token: "), optionsWidget()); l->addWidget(label, ++row, 0); m_apiKeyEdit = new QLineEdit(optionsWidget()); - connect(m_apiKeyEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_apiKeyEdit, row, 1); label->setBuddy(m_apiKeyEdit); l->setRowStretch(++row, 10); if(fetcher_) { m_apiKeyEdit->setText(fetcher_->m_apiKey); } // now add additional fields widget addFieldsWidget(DiscogsFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } void DiscogsFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString apiKey = m_apiKeyEdit->text().trimmed(); if(!apiKey.isEmpty()) { config_.writeEntry("API Key", apiKey); } } QString DiscogsFetcher::ConfigWidget::preferredName() const { return DiscogsFetcher::defaultName(); } diff --git a/src/fetch/doubanfetcher.cpp b/src/fetch/doubanfetcher.cpp index 72e52633..b50f892b 100644 --- a/src/fetch/doubanfetcher.cpp +++ b/src/fetch/doubanfetcher.cpp @@ -1,529 +1,529 @@ /*************************************************************************** Copyright (C) 2011 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "doubanfetcher.h" #include "../collections/bookcollection.h" #include "../collections/videocollection.h" #include "../collections/musiccollection.h" #include "../images/imagefactory.h" #include "../core/filehandler.h" #include "../utils/guiproxy.h" #include "../utils/isbnvalidator.h" #include "../utils/string_utils.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int DOUBAN_MAX_RETURNS_TOTAL = 20; static const char* DOUBAN_API_URL = "https://api.douban.com/v2/"; // old and unused //static const char* DOUBAN_API_KEY = "0bd1672394eb1ebf2374356abec15c3d"; } using namespace Tellico; using Tellico::Fetch::DoubanFetcher; DoubanFetcher::DoubanFetcher(QObject* parent_) : Fetcher(parent_) , m_started(false) { } DoubanFetcher::~DoubanFetcher() { } QString DoubanFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool DoubanFetcher::canSearch(FetchKey k) const { return k == Keyword || k == ISBN; } bool DoubanFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex || type == Data::Collection::Video || type == Data::Collection::Album; } void DoubanFetcher::readConfigHook(const KConfigGroup& config_) { Q_UNUSED(config_); } void DoubanFetcher::search() { m_started = true; QUrl u(QString::fromLatin1(DOUBAN_API_URL)); switch(request().collectionType) { case Data::Collection::Book: case Data::Collection::Bibtex: u.setPath(u.path() + QLatin1String("book/")); break; case Data::Collection::Video: u.setPath(u.path() + QLatin1String("movie/")); break; case Data::Collection::Album: u.setPath(u.path() + QLatin1String("music/")); break; default: myWarning() << "bad collection type:" << request().collectionType; } QUrlQuery q; switch(request().key) { case ISBN: u.setPath(u.path() + QLatin1String("isbn/")); { QStringList isbns = FieldFormat::splitValue(request().value); if(!isbns.isEmpty()) { u.setPath(u.path() + ISBNValidator::cleanValue(isbns.front())); } } break; case Keyword: u.setPath(u.path() + QLatin1String("search")); q.addQueryItem(QStringLiteral("q"), request().value); q.addQueryItem(QStringLiteral("count"), QString::number(DOUBAN_MAX_RETURNS_TOTAL)); break; default: myWarning() << "key not recognized:" << request().key; } // q.addQueryItem(QLatin1String("start"), QString::number(0)); u.setQuery(q); // myDebug() << "url:" << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); if(request().key == ISBN) { - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotCompleteISBN(KJob*))); + connect(m_job.data(), &KJob::result, this, &DoubanFetcher::slotCompleteISBN); } else { - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &DoubanFetcher::slotComplete); } } void DoubanFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void DoubanFetcher::slotCompleteISBN(KJob* job_) { KIO::StoredTransferJob* job = static_cast(job_); if(job->error()) { job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; QJsonDocument doc = QJsonDocument::fromJson(data); const QVariantMap resultMap = doc.object().toVariantMap(); // code == 6000 for no result if(mapValue(resultMap, "code") == QLatin1String("6000")) { message(mapValue(resultMap, "msg"), MessageHandler::Error); } else { Data::EntryPtr entry = createEntry(resultMap); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); m_matches.insert(r->uid, QUrl(mapValue(resultMap, "url"))); emit signalResultFound(r); } stop(); } void DoubanFetcher::slotComplete(KJob* job_) { KIO::StoredTransferJob* job = static_cast(job_); if(job->error()) { job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from doubanfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test-douban1.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); const QVariantMap resultsMap = doc.object().toVariantMap(); switch(request().collectionType) { case Data::Collection::Book: case Data::Collection::Bibtex: foreach(const QVariant& v, resultsMap.value(QLatin1String("books")).toList()) { const QVariantMap resultMap = v.toMap(); FetchResult* r = new FetchResult(Fetcher::Ptr(this), mapValue(resultMap, "title"), mapValue(resultMap, "author") + QLatin1Char('/') + mapValue(resultMap, "publisher") + QLatin1Char('/') + mapValue(resultMap, "pubdate").left(4)); m_matches.insert(r->uid, QUrl(mapValue(resultMap, "url"))); emit signalResultFound(r); } break; case Data::Collection::Video: foreach(const QVariant& v, resultsMap.value(QLatin1String("subjects")).toList()) { const QVariantMap resultMap = v.toMap(); FetchResult* r = new FetchResult(Fetcher::Ptr(this), mapValue(resultMap, "title"), mapValue(resultMap, "directors", "name") + QLatin1Char('/') + mapValue(resultMap, "year")); // movie results don't appear to have a url field m_matches.insert(r->uid, QUrl(QLatin1String(DOUBAN_API_URL) + QLatin1String("movie/subject/") + mapValue(resultMap, "id"))); emit signalResultFound(r); } break; case Data::Collection::Album: foreach(const QVariant& v, resultsMap.value(QLatin1String("musics")).toList()) { const QVariantMap resultMap = v.toMap(); FetchResult* r = new FetchResult(Fetcher::Ptr(this), mapValue(resultMap, "title"), mapValue(resultMap, "attrs", "singer") + QLatin1Char('/') + mapValue(resultMap, "attrs", "publisher") + QLatin1Char('/') + mapValue(resultMap, "attrs", "pubdate").left(4)); // movie results don't appear to have a url field m_matches.insert(r->uid, QUrl(QLatin1String(DOUBAN_API_URL) + QLatin1String("music/") + mapValue(resultMap, "id"))); emit signalResultFound(r); } break; default: break; } stop(); } Tellico::Data::EntryPtr DoubanFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries.value(uid_); if(entry) { return entry; } QUrl url = m_matches.value(uid_); QByteArray data = FileHandler::readDataFile(url, true); #if 0 myWarning() << "Remove output debug from doubanfetcher.cpp"; QFile f(QLatin1String("/tmp/test-douban2.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); entry = createEntry(doc.object().toVariantMap()); if(entry) { m_entries.insert(uid_, entry); } return entry; } Tellico::Data::EntryPtr DoubanFetcher::createEntry(const QVariantMap& resultMap_) { Data::CollPtr coll; Data::EntryPtr entry; switch(request().collectionType) { case Data::Collection::Book: case Data::Collection::Bibtex: coll = new Data::BookCollection(true); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "origin_title").isEmpty() && !coll->hasField(QStringLiteral("origtitle"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); f->setFormatType(FieldFormat::FormatTitle); coll->addField(f); } if(optionalFields().contains(QStringLiteral("douban")) && !mapValue(resultMap_, "alt").isEmpty() && !coll->hasField(QStringLiteral("douban"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("douban"), i18n("Douban Link"), Data::Field::URL)); f->setCategory(i18n("General")); coll->addField(f); } entry = new Data::Entry(coll); populateBookEntry(entry, resultMap_); break; case Data::Collection::Video: coll = new Data::VideoCollection(true); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "original_title").isEmpty() && !coll->hasField(QStringLiteral("origtitle"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); f->setFormatType(FieldFormat::FormatTitle); coll->addField(f); } if(optionalFields().contains(QStringLiteral("douban")) && !mapValue(resultMap_, "alt").isEmpty() && !coll->hasField(QStringLiteral("douban"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("douban"), i18n("Douban Link"), Data::Field::URL)); f->setCategory(i18n("General")); coll->addField(f); } entry = new Data::Entry(coll); populateVideoEntry(entry, resultMap_); break; case Data::Collection::Album: coll = new Data::MusicCollection(true); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "original_title").isEmpty() && !coll->hasField(QStringLiteral("origtitle"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); f->setFormatType(FieldFormat::FormatTitle); coll->addField(f); } if(optionalFields().contains(QStringLiteral("douban")) && !mapValue(resultMap_, "alt").isEmpty() && !coll->hasField(QStringLiteral("douban"))) { Data::FieldPtr f(new Data::Field(QStringLiteral("douban"), i18n("Douban Link"), Data::Field::URL)); f->setCategory(i18n("General")); coll->addField(f); } entry = new Data::Entry(coll); populateMusicEntry(entry, resultMap_); break; default: break; } return entry; } void DoubanFetcher::populateBookEntry(Data::EntryPtr entry, const QVariantMap& resultMap_) { entry->setField(QStringLiteral("title"), mapValue(resultMap_, "title")); entry->setField(QStringLiteral("subtitle"), mapValue(resultMap_, "subtitle")); entry->setField(QStringLiteral("author"), mapValue(resultMap_, "author")); entry->setField(QStringLiteral("translator"), mapValue(resultMap_, "translator")); entry->setField(QStringLiteral("publisher"), mapValue(resultMap_, "publisher")); const QString binding = mapValue(resultMap_, "binding"); if(binding == QStringLiteral("精装")) { entry->setField(QStringLiteral("binding"), i18n("Hardback")); } else if(binding == QStringLiteral("平装")) { entry->setField(QStringLiteral("binding"), i18n("Paperback")); } entry->setField(QStringLiteral("pub_year"), mapValue(resultMap_, "pubdate").left(4)); entry->setField(QStringLiteral("isbn"), mapValue(resultMap_, "isbn10")); entry->setField(QStringLiteral("pages"), mapValue(resultMap_, "pages")); entry->setField(QStringLiteral("cover"), mapValue(resultMap_, "image")); entry->setField(QStringLiteral("keyword"), mapValue(resultMap_, "tags", "title")); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "origin_title").isEmpty()) { entry->setField(QStringLiteral("origtitle"), mapValue(resultMap_, "origin_title")); } if(optionalFields().contains(QStringLiteral("douban"))) { entry->setField(QStringLiteral("douban"), mapValue(resultMap_, "alt")); } entry->setField(QStringLiteral("plot"), mapValue(resultMap_, "summary")); } void DoubanFetcher::populateVideoEntry(Data::EntryPtr entry, const QVariantMap& resultMap_) { entry->setField(QStringLiteral("title"), mapValue(resultMap_, "title")); entry->setField(QStringLiteral("genre"), mapValue(resultMap_, "genres")); entry->setField(QStringLiteral("director"), mapValue(resultMap_, "directors", "name")); entry->setField(QStringLiteral("writer"), mapValue(resultMap_, "writers", "name")); entry->setField(QStringLiteral("year"), mapValue(resultMap_, "year")); entry->setField(QStringLiteral("cover"), mapValue(resultMap_, "images", "medium")); entry->setField(QStringLiteral("plot"), mapValue(resultMap_, "summary")); QStringList actors; foreach(const QVariant& v, resultMap_.value(QLatin1String("casts")).toList()) { actors << v.toMap().value(QStringLiteral("name")).toString(); } entry->setField(QStringLiteral("cast"), actors.join(FieldFormat::rowDelimiterString())); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "original_title").isEmpty()) { entry->setField(QStringLiteral("origtitle"), mapValue(resultMap_, "original_title")); } if(optionalFields().contains(QStringLiteral("douban"))) { entry->setField(QStringLiteral("douban"), mapValue(resultMap_, "alt")); } } void DoubanFetcher::populateMusicEntry(Data::EntryPtr entry, const QVariantMap& resultMap_) { entry->setField(QStringLiteral("title"), mapValue(resultMap_, "title")); entry->setField(QStringLiteral("cover"), mapValue(resultMap_, "image")); entry->setField(QStringLiteral("artist"), mapValue(resultMap_, "attrs", "singer")); entry->setField(QStringLiteral("label"), mapValue(resultMap_, "attrs", "publisher")); entry->setField(QStringLiteral("year"), mapValue(resultMap_, "attrs", "pubdate").left(4)); if(mapValue(resultMap_, "attrs", "media") == QLatin1String("Audio CD") || mapValue(resultMap_, "attrs", "media") == QLatin1String("CD")) { entry->setField(QStringLiteral("medium"), i18n("Compact Disc")); } QStringList values, tracks; foreach(const QVariant& v, resultMap_.value(QLatin1String("attrs")) .toMap().value(QLatin1String("tracks")).toList()) { // some cases have all the tracks in one item, separated by "\n" and using 01. track numbers if(v.toString().contains(QLatin1Char('\n'))) { values << v.toString().split(QStringLiteral("\n")); } else { values << v.toString(); } } QRegExp trackNumRx(QLatin1String("^\\d+[.\\s]{2}")); QRegExp trackDurRx(QLatin1String("\\d+:\\d{2}")); foreach(QString value, values) { // can't be const // might starts with track number QStringList l = value.remove(trackNumRx).split(QStringLiteral(" - ")); if(l.size() == 1) { // might be split by tab characters and have track length at end l = value.remove(trackNumRx).split(QRegExp(QLatin1String("[\t\n]+"))); if(trackDurRx.exactMatch(l.last())) { tracks << l.first() + FieldFormat::columnDelimiterString() + entry->field(QStringLiteral("artist")) + FieldFormat::columnDelimiterString() + l.last(); } else { tracks << l.first(); } } else if(l.size() > 1) { const QString last = l.takeLast(); tracks << l.join(QLatin1String(" - ")) + FieldFormat::columnDelimiterString() + last; } } entry->setField(QStringLiteral("track"), tracks.join(FieldFormat::rowDelimiterString())); if(optionalFields().contains(QStringLiteral("origtitle")) && !mapValue(resultMap_, "original_title").isEmpty()) { entry->setField(QStringLiteral("origtitle"), mapValue(resultMap_, "original_title")); } if(optionalFields().contains(QStringLiteral("douban"))) { entry->setField(QStringLiteral("douban"), mapValue(resultMap_, "alt")); } } Tellico::Fetch::FetchRequest DoubanFetcher::updateRequest(Data::EntryPtr entry_) { QString isbn = entry_->field(QStringLiteral("isbn")); if(!isbn.isEmpty()) { return FetchRequest(ISBN, isbn); } QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Title, title); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* DoubanFetcher::configWidget(QWidget* parent_) const { return new DoubanFetcher::ConfigWidget(parent_, this); } QString DoubanFetcher::defaultName() { return QStringLiteral("Douban.com"); } QString DoubanFetcher::defaultIcon() { return favIcon("http://www.douban.com"); } Tellico::StringHash DoubanFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("origtitle")] = i18n("Original Title"); hash[QStringLiteral("douban")] = i18n("Douban Link"); return hash; } DoubanFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const DoubanFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(DoubanFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } void DoubanFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { Q_UNUSED(config_); } QString DoubanFetcher::ConfigWidget::preferredName() const { return DoubanFetcher::defaultName(); } diff --git a/src/fetch/entrezfetcher.cpp b/src/fetch/entrezfetcher.cpp index e3e66ab2..aa4704e1 100644 --- a/src/fetch/entrezfetcher.cpp +++ b/src/fetch/entrezfetcher.cpp @@ -1,488 +1,488 @@ /*************************************************************************** Copyright (C) 2005-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "entrezfetcher.h" #include "../utils/guiproxy.h" #include "../collection.h" #include "../entry.h" #include "../fieldformat.h" #include "../core/filehandler.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/datafileregistry.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include namespace { static const int ENTREZ_MAX_RETURNS_TOTAL = 25; static const char* ENTREZ_BASE_URL = "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/"; static const char* ENTREZ_SEARCH_CGI = "esearch.fcgi"; static const char* ENTREZ_SUMMARY_CGI = "esummary.fcgi"; static const char* ENTREZ_FETCH_CGI = "efetch.fcgi"; static const char* ENTREZ_LINK_CGI = "elink.fcgi"; static const char* ENTREZ_DEFAULT_DATABASE = "pubmed"; } using namespace Tellico; using namespace Tellico::Fetch; using Tellico::Fetch::EntrezFetcher; EntrezFetcher::EntrezFetcher(QObject* parent_) : Fetcher(parent_), m_xsltHandler(nullptr), m_start(1), m_total(-1), m_step(Begin), m_started(false) { } EntrezFetcher::~EntrezFetcher() { } QString EntrezFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool EntrezFetcher::canSearch(FetchKey k) const { return k == Title || k == Person || k == Keyword || k == Raw || k == PubmedID || k == DOI; } bool EntrezFetcher::canFetch(int type) const { return type == Data::Collection::Bibtex; } void EntrezFetcher::readConfigHook(const KConfigGroup& config_) { QString s = config_.readEntry("Database", ENTREZ_DEFAULT_DATABASE); // default to pubmed if(!s.isEmpty()) { m_dbname = s; } } void EntrezFetcher::search() { m_started = true; m_start = 1; m_total = -1; if(m_dbname.isEmpty()) { m_dbname = QLatin1String(ENTREZ_DEFAULT_DATABASE); } QUrl u(QString::fromLatin1(ENTREZ_BASE_URL)); u.setPath(u.path() + QLatin1String(ENTREZ_SEARCH_CGI)); QUrlQuery q; q.addQueryItem(QStringLiteral("tool"), QStringLiteral("Tellico")); q.addQueryItem(QStringLiteral("retmode"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("usehistory"), QStringLiteral("y")); q.addQueryItem(QStringLiteral("retmax"), QStringLiteral("1")); // we're just getting the count q.addQueryItem(QStringLiteral("db"), m_dbname); q.addQueryItem(QStringLiteral("term"), request().value); switch(request().key) { case Title: q.addQueryItem(QStringLiteral("field"), QStringLiteral("titl")); break; case Person: q.addQueryItem(QStringLiteral("field"), QStringLiteral("auth")); break; case Keyword: // for Tellico Keyword searches basically mean search for any field matching // q.addQueryItem(QLatin1String("field"), QLatin1String("word")); break; case PubmedID: q.addQueryItem(QStringLiteral("field"), QStringLiteral("pmid")); break; case DOI: case Raw: // for DOI, enough to match any field to DOI value //q.setQuery(u.query() + QLatin1Char('&') + request().value); break; default: myWarning() << "key not supported:" << request().key; stop(); return; } u.setQuery(q); m_step = Search; // myLog() << "search url: " << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &EntrezFetcher::slotComplete); } void EntrezFetcher::continueSearch() { m_started = true; doSummary(); } void EntrezFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; m_step = Begin; emit signalDone(this); } void EntrezFetcher::slotComplete(KJob*) { Q_ASSERT(m_job); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from entrezfetcher.cpp: " << __LINE__; QFile f(QLatin1String("/tmp/test.xml")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif switch(m_step) { case Search: searchResults(data); break; case Summary: summaryResults(data); break; case Begin: case Fetch: default: myLog() << "wrong step =" << m_step; stop(); break; } } void EntrezFetcher::searchResults(const QByteArray& data_) { QDomDocument dom; if(!dom.setContent(data_, false)) { myWarning() << "server did not return valid XML."; stop(); return; } // find Count, QueryKey, and WebEnv elements int count = 0; for(QDomNode n = dom.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement e = n.toElement(); if(e.isNull()) { continue; } if(e.tagName() == QLatin1String("Count")) { m_total = e.text().toInt(); ++count; } else if(e.tagName() == QLatin1String("QueryKey")) { m_queryKey = e.text(); ++count; } else if(e.tagName() == QLatin1String("WebEnv")) { m_webEnv = e.text(); ++count; } if(count >= 3) { break; // found them all } } doSummary(); } void EntrezFetcher::doSummary() { QUrl u(QString::fromLatin1(ENTREZ_BASE_URL)); u.setPath(u.path() + QLatin1String(ENTREZ_SUMMARY_CGI)); QUrlQuery q; q.addQueryItem(QStringLiteral("tool"), QStringLiteral("Tellico")); q.addQueryItem(QStringLiteral("retmode"), QStringLiteral("xml")); if(m_start > 1) { q.addQueryItem(QStringLiteral("retstart"), QString::number(m_start)); } q.addQueryItem(QStringLiteral("retmax"), QString::number(qMin(m_total-m_start-1, ENTREZ_MAX_RETURNS_TOTAL))); q.addQueryItem(QStringLiteral("usehistory"), QStringLiteral("y")); q.addQueryItem(QStringLiteral("db"), m_dbname); q.addQueryItem(QStringLiteral("query_key"), m_queryKey); q.addQueryItem(QStringLiteral("WebEnv"), m_webEnv); u.setQuery(q); m_step = Summary; // myLog() << "summary url:" << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &EntrezFetcher::slotComplete); } void EntrezFetcher::summaryResults(const QByteArray& data_) { QDomDocument dom; if(!dom.setContent(data_, false)) { myWarning() << "server did not return valid XML."; stop(); return; } // top child is eSummaryResult // all children are DocSum for(QDomNode n = dom.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) { QDomElement e = n.toElement(); if(e.isNull() || e.tagName() != QLatin1String("DocSum")) { continue; } QDomNodeList nodes = e.elementsByTagName(QStringLiteral("Id")); if(nodes.count() == 0) { myDebug() << "no Id elements"; continue; } int id = nodes.item(0).toElement().text().toInt(); QString title, pubdate, authors; nodes = e.elementsByTagName(QStringLiteral("Item")); for(int j = 0; j < nodes.count(); ++j) { if(nodes.item(j).toElement().attribute(QStringLiteral("Name")) == QLatin1String("Title")) { title = nodes.item(j).toElement().text(); } else if(nodes.item(j).toElement().attribute(QStringLiteral("Name")) == QLatin1String("PubDate")) { pubdate = nodes.item(j).toElement().text(); } else if(nodes.item(j).toElement().attribute(QStringLiteral("Name")) == QLatin1String("AuthorList")) { QStringList list; for(QDomNode aNode = nodes.item(j).firstChild(); !aNode.isNull(); aNode = aNode.nextSibling()) { // lazy, assume all children Items are authors if(aNode.nodeName() == QLatin1String("Item")) { list << aNode.toElement().text(); } } authors = list.join(FieldFormat::delimiterString()); } if(!title.isEmpty() && !pubdate.isEmpty() && !authors.isEmpty()) { break; // done now } } FetchResult* r = new FetchResult(Fetcher::Ptr(this), title, pubdate + QLatin1Char('/') + authors); m_matches.insert(r->uid, id); emit signalResultFound(r); } m_start = m_matches.count() + 1; m_hasMoreResults = m_start <= m_total; stop(); // done searching } Tellico::Data::EntryPtr EntrezFetcher::fetchEntryHook(uint uid_) { // if we already grabbed this one, then just pull it out of the dict Data::EntryPtr entry = m_entries[uid_]; if(entry) { return entry; } if(!m_matches.contains(uid_)) { return Data::EntryPtr(); } if(!m_xsltHandler) { initXSLTHandler(); if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading stop(); return Data::EntryPtr(); } } int id = m_matches[uid_]; QUrl u(QString::fromLatin1(ENTREZ_BASE_URL)); u.setPath(u.path() + QLatin1String(ENTREZ_FETCH_CGI)); QUrlQuery q; q.addQueryItem(QStringLiteral("tool"), QStringLiteral("Tellico")); q.addQueryItem(QStringLiteral("retmode"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("rettype"), QStringLiteral("abstract")); q.addQueryItem(QStringLiteral("db"), m_dbname); q.addQueryItem(QStringLiteral("id"), QString::number(id)); u.setQuery(q); // now it's synchronous QString xmlOutput = FileHandler::readXMLFile(u, true /*quiet*/); if(xmlOutput.isEmpty()) { myWarning() << "unable to download " << u; return Data::EntryPtr(); } #if 0 myWarning() << "turn me off in entrezfetcher.cpp!"; QFile f1(QLatin1String("/tmp/test-entry.xml")); if(f1.open(QIODevice::WriteOnly)) { QTextStream t(&f1); t.setCodec("UTF-8"); t << xmlOutput; } f1.close(); #endif QString str = m_xsltHandler->applyStylesheet(xmlOutput); Import::TellicoImporter imp(str); Data::CollPtr coll = imp.collection(); if(!coll) { myWarning() << "invalid collection"; return Data::EntryPtr(); } if(coll->entryCount() == 0) { myDebug() << "no entries in collection"; return Data::EntryPtr(); } else if(coll->entryCount() > 1) { myDebug() << "collection has multiple entries, taking first one"; } Data::EntryPtr e = coll->entries().front(); // try to get a link, but only if necessary if(optionalFields().contains(QStringLiteral("url"))) { QUrl link(QString::fromLatin1(ENTREZ_BASE_URL)); link.setPath(link.path() + QLatin1String(ENTREZ_LINK_CGI)); QUrlQuery q; q.addQueryItem(QStringLiteral("tool"), QStringLiteral("Tellico")); q.addQueryItem(QStringLiteral("cmd"), QStringLiteral("llinks")); q.addQueryItem(QStringLiteral("db"), m_dbname); q.addQueryItem(QStringLiteral("dbfrom"), m_dbname); q.addQueryItem(QStringLiteral("id"), QString::number(id)); link.setQuery(q); QDomDocument linkDom = FileHandler::readXMLDocument(link, false /* namespace */, true /* quiet */); // need eLinkResult/LinkSet/IdUrlList/IdUrlSet/ObjUrl/Url QDomNode linkNode = linkDom.namedItem(QStringLiteral("eLinkResult")) .namedItem(QStringLiteral("LinkSet")) .namedItem(QStringLiteral("IdUrlList")) .namedItem(QStringLiteral("IdUrlSet")) .namedItem(QStringLiteral("ObjUrl")) .namedItem(QStringLiteral("Url")); if(!linkNode.isNull()) { QString u = linkNode.toElement().text(); // myDebug() << u; if(!u.isEmpty()) { if(!coll->hasField(QStringLiteral("url"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("url"), i18n("URL"), Data::Field::URL)); field->setCategory(i18n("Miscellaneous")); coll->addField(field); } e->setField(QStringLiteral("url"), u); } } } m_entries.insert(uid_, e); return e; } void EntrezFetcher::initXSLTHandler() { QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("pubmed2tellico.xsl")); if(xsltfile.isEmpty()) { myWarning() << "can not locate pubmed2tellico.xsl."; return; } QUrl u = QUrl::fromLocalFile(xsltfile); if(!m_xsltHandler) { m_xsltHandler = new XSLTHandler(u); } if(!m_xsltHandler->isValid()) { myWarning() << "error in pubmed2tellico.xsl."; delete m_xsltHandler; m_xsltHandler = nullptr; return; } } Tellico::Fetch::FetchRequest EntrezFetcher::updateRequest(Data::EntryPtr entry_) { // myDebug(); QString s = entry_->field(QStringLiteral("pmid")); if(!s.isEmpty()) { return FetchRequest(PubmedID, s); } s = entry_->field(QStringLiteral("doi")); if(!s.isEmpty()) { return FetchRequest(DOI, s); } s = entry_->field(QStringLiteral("title")); if(!s.isEmpty()) { return FetchRequest(Title, s); } return FetchRequest(); } QString EntrezFetcher::defaultName() { return i18n("Entrez Database"); } QString EntrezFetcher::defaultIcon() { return favIcon("http://www.ncbi.nlm.nih.gov"); } //static Tellico::StringHash EntrezFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("institution")] = i18n("Institution"); hash[QStringLiteral("abstract")] = i18n("Abstract"); hash[QStringLiteral("url")] = i18n("URL"); return hash; } Tellico::Fetch::ConfigWidget* EntrezFetcher::configWidget(QWidget* parent_) const { return new EntrezFetcher::ConfigWidget(parent_, this); } EntrezFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const EntrezFetcher* fetcher_/*=0*/) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(EntrezFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } QString EntrezFetcher::ConfigWidget::preferredName() const { return EntrezFetcher::defaultName(); } diff --git a/src/fetch/execexternalfetcher.cpp b/src/fetch/execexternalfetcher.cpp index 541f4bf1..a2de8ae5 100644 --- a/src/fetch/execexternalfetcher.cpp +++ b/src/fetch/execexternalfetcher.cpp @@ -1,550 +1,551 @@ /*************************************************************************** Copyright (C) 2005-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "execexternalfetcher.h" #include "fetchmanager.h" #include "../collection.h" #include "../entry.h" #include "../fieldformat.h" #include "../derivedvalue.h" #include "../tellico_debug.h" #include "../gui/combobox.h" #include "../gui/lineedit.h" #include "../gui/collectiontypecombo.h" #include "../utils/cursorsaver.h" #include "../newstuff/manager.h" #include "../translators/translators.h" #include "../translators/tellicoimporter.h" #include "../translators/bibteximporter.h" #include "../translators/xsltimporter.h" #include "../translators/risimporter.h" #include "../utils/datafileregistry.h" #include #include #include #include #include #include #include #include #include #include using namespace Tellico; using Tellico::Fetch::ExecExternalFetcher; QStringList ExecExternalFetcher::parseArguments(const QString& str_) { // matching escaped quotes is too hard... :( // QRegExp quotes(QLatin1String("[^\\\\](['\"])(.*[^\\\\])\\1")); QRegExp quotes(QLatin1String("(['\"])(.*)\\1")); quotes.setMinimal(true); QRegExp spaces(QLatin1String("\\s+")); spaces.setMinimal(true); QStringList args; int pos = 0; for(int nextPos = quotes.indexIn(str_); nextPos > -1; pos = nextPos+1, nextPos = quotes.indexIn(str_, pos)) { // a non-quotes arguments runs from pos to nextPos args += str_.mid(pos, nextPos-pos).split(spaces, QString::SkipEmptyParts); // move nextpos marker to end of match pos = quotes.pos(2); // skip quotation mark nextPos += quotes.matchedLength(); args += str_.mid(pos, nextPos-pos-1); } // catch the end stuff args += str_.mid(pos).split(spaces, QString::SkipEmptyParts); return args; } ExecExternalFetcher::ExecExternalFetcher(QObject* parent_) : Fetcher(parent_), m_started(false), m_collType(-1), m_formatType(-1), m_canUpdate(false), m_process(nullptr), m_deleteOnRemove(false) { } ExecExternalFetcher::~ExecExternalFetcher() { stop(); } QString ExecExternalFetcher::source() const { return m_name; } bool ExecExternalFetcher::canFetch(int type_) const { return m_collType == -1 ? false : m_collType == type_; } void ExecExternalFetcher::readConfigHook(const KConfigGroup& config_) { QString s = config_.readPathEntry("ExecPath", QString()); if(!s.isEmpty()) { m_path = s; } QList argKeys; if(config_.hasKey("ArgumentKeys")) { argKeys = config_.readEntry("ArgumentKeys", argKeys); } else { myDebug() << "appending default keyword argument"; argKeys.append(Keyword); } QStringList args = config_.readEntry("Arguments", QStringList()); if(argKeys.count() != args.count()) { myWarning() << "unequal number of arguments and keys"; } int n = qMin(argKeys.count(), args.count()); for(int i = 0; i < n; ++i) { m_args.insert(static_cast(argKeys[i]), args[i]); } if(config_.hasKey("UpdateArgs")) { m_canUpdate = true; m_updateArgs = config_.readEntry("UpdateArgs"); } else { m_canUpdate = false; } m_collType = config_.readEntry("CollectionType", -1); m_formatType = config_.readEntry("FormatType", -1); m_deleteOnRemove = config_.readEntry("DeleteOnRemove", false); m_newStuffName = config_.readEntry("NewStuffName"); } void ExecExternalFetcher::search() { m_started = true; if(request().key != ExecUpdate && !m_args.contains(request().key)) { myDebug() << "stopping: not an update and no matching argument for search key"; stop(); return; } if(request().key == ExecUpdate) { // because the rowDelimiterString() is used below QStringList args = FieldFormat::splitTable(request().value); startSearch(args); return; } // should KShell::quoteArg() be used? // %1 gets replaced by the search value, but since the arguments are going to be split // the search value needs to be enclosed in quotation marks // but first check to make sure the user didn't do that already // AND the "%1" wasn't used in the settings QString value = request().value; if(request().key == ISBN) { value.remove(QLatin1Char('-')); // remove hyphens from isbn values // shouldn't hurt and might keep from confusing stupid search sources } QRegExp rx1(QLatin1String("['\"].*\\1")); if(!rx1.exactMatch(value)) { value = QLatin1Char('"') + value + QLatin1Char('"'); } QString args = m_args.value(request().key); QRegExp rx2(QLatin1String("['\"]%1\\1")); args.replace(rx2, QStringLiteral("%1")); startSearch(parseArguments(args.arg(value))); // replace %1 with search value } void ExecExternalFetcher::startSearch(const QStringList& args_) { if(m_path.isEmpty()) { Q_ASSERT(!m_path.isEmpty()); stop(); return; } m_process = new KProcess(); - connect(m_process, SIGNAL(readyReadStandardOutput()), SLOT(slotData())); - connect(m_process, SIGNAL(readyReadStandardError()), SLOT(slotError())); - connect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(slotProcessExited())); + connect(m_process, &QProcess::readyReadStandardOutput, this, &ExecExternalFetcher::slotData); + connect(m_process, &QProcess::readyReadStandardError, this, &ExecExternalFetcher::slotError); + void (QProcess::* finished)(int, QProcess::ExitStatus) = &QProcess::finished; + connect(m_process, finished, this, &ExecExternalFetcher::slotProcessExited); m_process->setOutputChannelMode(KProcess::SeparateChannels); m_process->setProgram(m_path, args_); if(m_process && m_process->execute() < 0) { myDebug() << "process failed to start"; stop(); } } void ExecExternalFetcher::stop() { if(!m_started) { return; } if(m_process) { m_process->kill(); m_process->deleteLater(); m_process = nullptr; } m_data.clear(); m_started = false; m_errors.clear(); emit signalDone(this); } void ExecExternalFetcher::slotData() { m_data.append(m_process->readAllStandardOutput()); } void ExecExternalFetcher::slotError() { GUI::CursorSaver cs(Qt::ArrowCursor); QString msg = QString::fromLocal8Bit(m_process->readAllStandardError()); msg.prepend(source() + QLatin1String(": ")); if(msg.endsWith(QChar::fromLatin1('\n'))) { msg.truncate(msg.length()-1); } myDebug() << msg; m_errors << msg; } void ExecExternalFetcher::slotProcessExited() { // DEBUG_LINE; if(m_process->exitStatus() != QProcess::NormalExit || m_process->exitCode() != 0) { myDebug() << source() << ": process did not exit successfully"; if(!m_errors.isEmpty()) { message(m_errors.join(QChar::fromLatin1('\n')), MessageHandler::Error); } stop(); return; } if(!m_errors.isEmpty()) { message(m_errors.join(QChar::fromLatin1('\n')), MessageHandler::Warning); } if(m_data.isEmpty()) { myDebug() << source() << ": no data"; stop(); return; } const QString text = QString::fromUtf8(m_data.constData(), m_data.size()); Import::Format format = static_cast(m_formatType > -1 ? m_formatType : Import::TellicoXML); Import::Importer* imp = nullptr; // only 4 formats re supported here switch(format) { case Import::TellicoXML: imp = new Import::TellicoImporter(text); break; case Import::Bibtex: imp = new Import::BibtexImporter(text); break; case Import::MODS: imp = new Import::XSLTImporter(text); { QString xsltFile = DataFileRegistry::self()->locate(QStringLiteral("mods2tellico.xsl")); if(!xsltFile.isEmpty()) { QUrl u = QUrl::fromLocalFile(xsltFile); static_cast(imp)->setXSLTURL(u); } else { myWarning() << "unable to find mods2tellico.xml!"; delete imp; imp = nullptr; } } break; case Import::RIS: imp = new Import::RISImporter(text); break; default: break; } if(!imp) { stop(); return; } Data::CollPtr coll = imp->collection(); if(!coll) { if(!imp->statusMessage().isEmpty()) { message(imp->statusMessage(), MessageHandler::Status); } myDebug() << source() << ": no collection pointer"; delete imp; stop(); return; } delete imp; if(coll->entryCount() == 0) { // myDebug() << "no results"; stop(); return; } Data::EntryList entries = coll->entries(); foreach(Data::EntryPtr entry, entries) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } stop(); // be sure to call this } Tellico::Data::EntryPtr ExecExternalFetcher::fetchEntryHook(uint uid_) { return m_entries[uid_]; } Tellico::Fetch::FetchRequest ExecExternalFetcher::updateRequest(Data::EntryPtr entry_) { if(!m_canUpdate) { return FetchRequest(); } QStringList args = parseArguments(m_updateArgs); for(QStringList::Iterator it = args.begin(); it != args.end(); ++it) { Data::DerivedValue dv(*it); *it = dv.value(entry_, false); } return FetchRequest(ExecUpdate, args.join(FieldFormat::rowDelimiterString())); } Tellico::Fetch::ConfigWidget* ExecExternalFetcher::configWidget(QWidget* parent_) const { return new ExecExternalFetcher::ConfigWidget(parent_, this); } QString ExecExternalFetcher::defaultName() { return i18n("External Application"); } QString ExecExternalFetcher::defaultIcon() { return QStringLiteral("application-x-executable"); } ExecExternalFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ExecExternalFetcher* fetcher_/*=0*/) : Fetch::ConfigWidget(parent_), m_deleteOnRemove(false) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* label = new QLabel(i18n("Collection &type:"), optionsWidget()); l->addWidget(label, ++row, 0); m_collCombo = new GUI::CollectionTypeCombo(optionsWidget()); - connect(m_collCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; + connect(m_collCombo, activatedInt, this, &ConfigWidget::slotSetModified); l->addWidget(m_collCombo, row, 1); QString w = i18n("Set the collection type of the data returned from the external application."); label->setWhatsThis(w); m_collCombo->setWhatsThis(w); label->setBuddy(m_collCombo); label = new QLabel(i18n("&Result type: "), optionsWidget()); l->addWidget(label, ++row, 0); m_formatCombo = new GUI::ComboBox(optionsWidget()); m_formatCombo->addItem(QStringLiteral("Tellico"), Import::TellicoXML); m_formatCombo->addItem(QStringLiteral("Bibtex"), Import::Bibtex); m_formatCombo->addItem(QStringLiteral("MODS"), Import::MODS); m_formatCombo->addItem(QStringLiteral("RIS"), Import::RIS); - connect(m_formatCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + connect(m_formatCombo, activatedInt, this, &ExecExternalFetcher::ConfigWidget::slotSetModified); l->addWidget(m_formatCombo, row, 1); w = i18n("Set the result type of the data returned from the external application."); label->setWhatsThis(w); m_formatCombo->setWhatsThis(w); label->setBuddy(m_formatCombo); label = new QLabel(i18n("Application &path: "), optionsWidget()); l->addWidget(label, ++row, 0); m_pathEdit = new KUrlRequester(optionsWidget()); - connect(m_pathEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_pathEdit, &KUrlRequester::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_pathEdit, row, 1); w = i18n("Set the path of the application to run that should output a valid Tellico data file."); label->setWhatsThis(w); m_pathEdit->setWhatsThis(w); label->setBuddy(m_pathEdit); w = i18n("Select the search keys supported by the data source."); // in this string, the %1 is not a placeholder, it's an example QString w2 = i18n("Add any arguments that may be needed. %1 will be replaced by the search term."); // krazy:exclude=i18ncheckarg QGroupBox* gbox = new QGroupBox(i18n("Arguments"), optionsWidget()); ++row; l->addWidget(gbox, row, 0, 1, 2); QGridLayout* gridLayout = new QGridLayout(gbox); gridLayout->setSpacing(2); row = -1; const Fetch::KeyMap keyMap = Fetch::Manager::self()->keyMap(); for(Fetch::KeyMap::ConstIterator it = keyMap.begin(); it != keyMap.end(); ++it) { FetchKey key = it.key(); if(key == Raw) { continue; } QCheckBox* cb = new QCheckBox(it.value(), gbox); gridLayout->addWidget(cb, ++row, 0); m_cbDict.insert(key, cb); GUI::LineEdit* le = new GUI::LineEdit(gbox); le->setPlaceholderText(QStringLiteral("%1")); // for example le->completionObject()->addItem(QStringLiteral("%1")); gridLayout->addWidget(le, row, 1); m_leDict.insert(key, le); if(fetcher_ && fetcher_->m_args.contains(key)) { cb->setChecked(true); le->setEnabled(true); le->setText(fetcher_->m_args.value(key)); } else { cb->setChecked(false); le->setEnabled(false); } - connect(cb, SIGNAL(toggled(bool)), le, SLOT(setEnabled(bool))); + connect(cb, &QAbstractButton::toggled, le, &QWidget::setEnabled); cb->setWhatsThis(w); le->setWhatsThis(w2); } m_cbUpdate = new QCheckBox(i18n("Update"), gbox); gridLayout->addWidget(m_cbUpdate, ++row, 0); m_leUpdate = new GUI::LineEdit(gbox); m_leUpdate->setPlaceholderText(QStringLiteral("%{title}")); // for example m_leUpdate->completionObject()->addItem(QStringLiteral("%{title}")); m_leUpdate->completionObject()->addItem(QStringLiteral("%{isbn}")); gridLayout->addWidget(m_leUpdate, row, 1); /* TRANSLATORS: Do not translate %{author}. */ w2 = i18n("

    Enter the arguments which should be used to search for available updates to an entry.

    " "The format is the same as for fields with derived values, where field names " "are contained inside braces, such as %{author}. See the documentation for details.

    "); m_cbUpdate->setWhatsThis(w); m_leUpdate->setWhatsThis(w2); if(fetcher_ && fetcher_->m_canUpdate) { m_cbUpdate->setChecked(true); m_leUpdate->setEnabled(true); m_leUpdate->setText(fetcher_->m_updateArgs); } else { m_cbUpdate->setChecked(false); m_leUpdate->setEnabled(false); } - connect(m_cbUpdate, SIGNAL(toggled(bool)), m_leUpdate, SLOT(setEnabled(bool))); + connect(m_cbUpdate, &QAbstractButton::toggled, m_leUpdate, &QWidget::setEnabled); l->setRowStretch(++row, 1); if(fetcher_) { m_pathEdit->setUrl(QUrl::fromLocalFile(fetcher_->m_path)); m_newStuffName = fetcher_->m_newStuffName; } if(fetcher_ && fetcher_->m_collType > -1) { m_collCombo->setCurrentType(fetcher_->m_collType); } else { m_collCombo->setCurrentType(Data::Collection::Book); } if(fetcher_ && fetcher_->m_formatType > -1) { m_formatCombo->setCurrentData(fetcher_->m_formatType); } else { m_formatCombo->setCurrentData(Import::TellicoXML); } m_deleteOnRemove = fetcher_ && fetcher_->m_deleteOnRemove; KAcceleratorManager::manage(optionsWidget()); } ExecExternalFetcher::ConfigWidget::~ConfigWidget() { } void ExecExternalFetcher::ConfigWidget::readConfig(const KConfigGroup& config_) { m_pathEdit->setUrl(QUrl::fromLocalFile(config_.readPathEntry("ExecPath", QString()))); QList argKeys = config_.readEntry("ArgumentKeys", QList()); QStringList argValues = config_.readEntry("Arguments", QStringList()); if(argKeys.count() != argValues.count()) { myWarning() << "unequal number of arguments and keys"; } int n = qMin(argKeys.count(), argValues.count()); QMap args; for(int i = 0; i < n; ++i) { args[static_cast(argKeys[i])] = argValues[i]; } for(QList::Iterator it = argKeys.begin(); it != argKeys.end(); ++it) { if(*it == Raw) { continue; } FetchKey key = static_cast(*it); QCheckBox* cb = m_cbDict[key]; QLineEdit* le = m_leDict[key]; if(cb && le) { if(args.contains(key)) { cb->setChecked(true); le->setEnabled(true); le->setText(args[key]); } else { cb->setChecked(false); le->setEnabled(false); le->clear(); } } } if(config_.hasKey("UpdateArgs")) { m_cbUpdate->setChecked(true); m_leUpdate->setEnabled(true); m_leUpdate->setText(config_.readEntry("UpdateArgs")); } else { m_cbUpdate->setChecked(false); m_leUpdate->setEnabled(false); m_leUpdate->clear(); } int collType = config_.readEntry("CollectionType", -1); m_collCombo->setCurrentType(collType); int formatType = config_.readEntry("FormatType", -1); m_formatCombo->setCurrentData(static_cast(formatType)); m_deleteOnRemove = config_.readEntry("DeleteOnRemove", false); m_name = config_.readEntry("Name"); m_newStuffName = config_.readEntry("NewStuffName"); } void ExecExternalFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QUrl u = m_pathEdit->url(); if(!u.isEmpty()) { config_.writePathEntry("ExecPath", u.path()); } QList keys; QStringList args; QHash::const_iterator it = m_cbDict.constBegin(); for( ; it != m_cbDict.constEnd(); ++it) { if(it.value()->isChecked()) { keys << it.key(); args << m_leDict[it.key()]->text(); } } config_.writeEntry("ArgumentKeys", keys); config_.writeEntry("Arguments", args); if(m_cbUpdate->isChecked()) { config_.writeEntry("UpdateArgs", m_leUpdate->text()); } else { config_.deleteEntry("UpdateArgs"); } config_.writeEntry("CollectionType", m_collCombo->currentType()); config_.writeEntry("FormatType", m_formatCombo->currentData().toInt()); config_.writeEntry("DeleteOnRemove", m_deleteOnRemove); if(!m_newStuffName.isEmpty()) { config_.writeEntry("NewStuffName", m_newStuffName); } - slotSetModified(false); } void ExecExternalFetcher::ConfigWidget::removed() { if(!m_deleteOnRemove) { return; } if(!m_newStuffName.isEmpty()) { NewStuff::Manager::self()->removeScript(m_newStuffName); } } QString ExecExternalFetcher::ConfigWidget::preferredName() const { return m_name.isEmpty() ? ExecExternalFetcher::defaultName() : m_name; } diff --git a/src/fetch/fetcherjob.cpp b/src/fetch/fetcherjob.cpp index c405ae54..b0165e58 100644 --- a/src/fetch/fetcherjob.cpp +++ b/src/fetch/fetcherjob.cpp @@ -1,95 +1,95 @@ /*************************************************************************** Copyright (C) 2005-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "fetcherjob.h" #include "../entry.h" #include "../tellico_debug.h" #include using namespace Tellico; using namespace Tellico::Fetch; using Tellico::Fetch::FetcherJob; FetcherJob::FetcherJob(QObject* parent_, Fetcher::Ptr fetcher_, const FetchRequest& request_) : KJob(parent_), m_fetcher(fetcher_), m_request(request_), m_maximumResults(0) { - connect(m_fetcher.data(), SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*)), - SLOT(slotResult(Tellico::Fetch::FetchResult*))); - connect(m_fetcher.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher*)), - SLOT(slotDone())); + connect(m_fetcher.data(), &Fetcher::signalResultFound, + this, &FetcherJob::slotResult); + connect(m_fetcher.data(), &Fetcher::signalDone, + this, &FetcherJob::slotDone); } FetcherJob::~FetcherJob() { qDeleteAll(m_results); m_results.clear(); } Tellico::Data::EntryList FetcherJob::entries() { Data::EntryList list; foreach(FetchResult* result, m_results) { Data::EntryPtr entry = result->fetchEntry(); if(entry) { list << entry; } } return list; } void FetcherJob::setMaximumResults(int count_) { Q_ASSERT(count_ >= 0); m_maximumResults = count_; } void FetcherJob::start() { - QTimer::singleShot(0, this, SLOT(startSearch())); + QTimer::singleShot(0, this, &FetcherJob::startSearch); } void FetcherJob::startSearch() { m_fetcher->startSearch(m_request); } void FetcherJob::slotResult(Tellico::Fetch::FetchResult* result_) { if(!result_) { myDebug() << "null result"; return; } m_results.append(result_); if(m_maximumResults > 0 && m_results.count() >= m_maximumResults) { doKill(); } } void FetcherJob::slotDone() { // only continue if more results were specifically asked for if(m_fetcher->hasMoreResults() && m_results.count() < m_maximumResults) { m_fetcher->continueSearch(); } else { emitResult(); } } bool FetcherJob::doKill() { m_fetcher->stop(); return true; } diff --git a/src/fetch/fetchmanager.cpp b/src/fetch/fetchmanager.cpp index 9fc88758..8405796c 100644 --- a/src/fetch/fetchmanager.cpp +++ b/src/fetch/fetchmanager.cpp @@ -1,586 +1,586 @@ /*************************************************************************** Copyright (C) 2003-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include #include "fetchmanager.h" #include "configwidget.h" #include "messagehandler.h" #include "../entry.h" #include "../collection.h" #include "../document.h" #include "../utils/string_utils.h" #include "../utils/tellico_utils.h" #include "../tellico_debug.h" #ifdef HAVE_YAZ #include "z3950fetcher.h" #endif #include "srufetcher.h" #include "execexternalfetcher.h" #include #include #include #include #include #include #include #define LOAD_ICON(name, group, size) \ KIconLoader::global()->loadIcon(name, static_cast(group), size_) using Tellico::Fetch::Manager; Manager* Manager::s_self = nullptr; Manager::Manager() : QObject(), m_currentFetcherIndex(-1), m_messager(new ManagerMessage()), m_count(0), m_loadDefaults(false) { // must create static pointer first Q_ASSERT(!s_self); s_self = this; // no need to load fetchers since the initializer does it for us // m_keyMap.insert(FetchFirst, QString()); m_keyMap.insert(Title, i18n("Title")); m_keyMap.insert(Person, i18n("Person")); m_keyMap.insert(ISBN, i18n("ISBN")); m_keyMap.insert(UPC, i18n("UPC/EAN")); m_keyMap.insert(Keyword, i18n("Keyword")); m_keyMap.insert(DOI, i18n("DOI")); m_keyMap.insert(ArxivID, i18n("arXiv ID")); m_keyMap.insert(PubmedID, i18n("PubMed ID")); m_keyMap.insert(LCCN, i18n("LCCN")); m_keyMap.insert(Raw, i18n("Raw Query")); // m_keyMap.insert(FetchLast, QString()); } Manager::~Manager() { delete m_messager; } void Manager::registerFunction(int type_, const FetcherFunction& func_) { functionRegistry.insert(type_, func_); } void Manager::loadFetchers() { m_fetchers.clear(); m_uuidHash.clear(); KSharedConfigPtr config = KSharedConfig::openConfig(); if(config->hasGroup(QStringLiteral("Data Sources"))) { KConfigGroup configGroup(config, QStringLiteral("Data Sources")); int nSources = configGroup.readEntry("Sources Count", 0); for(int i = 0; i < nSources; ++i) { QString group = QStringLiteral("Data Source %1").arg(i); Fetcher::Ptr f = createFetcher(config, group); if(f) { m_fetchers.append(f); f->setMessageHandler(m_messager); m_uuidHash.insert(f->uuid(), f); } } m_loadDefaults = false; } else { // add default sources m_fetchers = defaultFetchers(); m_loadDefaults = true; } } const Tellico::Fetch::FetcherVec& Manager::fetchers() const { return m_fetchers; } Tellico::Fetch::FetcherVec Manager::fetchers(int type_) { FetcherVec vec; foreach(Fetcher::Ptr fetcher, m_fetchers) { if(fetcher->canFetch(type_)) { vec.append(fetcher); } } return vec; } Tellico::Fetch::Fetcher::Ptr Manager::fetcherByUuid(const QString& uuid_) { return m_uuidHash.contains(uuid_) ? m_uuidHash[uuid_] : Fetcher::Ptr(); } Tellico::Fetch::KeyMap Manager::keyMap(const QString& source_) const { // an empty string means return all if(source_.isEmpty()) { return m_keyMap; } // assume there's only one fetcher match Fetcher::Ptr foundFetcher; foreach(Fetcher::Ptr fetcher, m_fetchers) { if(source_ == fetcher->source()) { foundFetcher = fetcher; break; } } if(!foundFetcher) { myWarning() << "no fetcher found!"; return KeyMap(); } KeyMap map; for(KeyMap::ConstIterator it = m_keyMap.constBegin(); it != m_keyMap.constEnd(); ++it) { if(foundFetcher->canSearch(it.key())) { map.insert(it.key(), it.value()); } } return map; } void Manager::startSearch(const QString& source_, Tellico::Fetch::FetchKey key_, const QString& value_) { if(value_.isEmpty()) { emit signalDone(); return; } FetchRequest request(Data::Document::self()->collection()->type(), key_, value_); // assume there's only one fetcher match int i = 0; m_currentFetcherIndex = -1; foreach(Fetcher::Ptr fetcher, m_fetchers) { if(source_ == fetcher->source()) { ++m_count; // Fetcher::search() might emit done(), so increment before calling search() - connect(fetcher.data(), SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*)), - SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*))); - connect(fetcher.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher*)), - SLOT(slotFetcherDone(Tellico::Fetch::Fetcher*))); + connect(fetcher.data(), &Fetcher::signalResultFound, + this, &Manager::signalResultFound); + connect(fetcher.data(), &Fetcher::signalDone, + this, &Manager::slotFetcherDone); fetcher->startSearch(request); m_currentFetcherIndex = i; break; } ++i; } } void Manager::continueSearch() { if(m_currentFetcherIndex < 0 || m_currentFetcherIndex >= static_cast(m_fetchers.count())) { myDebug() << "can't continue!"; emit signalDone(); return; } Fetcher::Ptr fetcher = m_fetchers[m_currentFetcherIndex]; if(fetcher && fetcher->hasMoreResults()) { ++m_count; - connect(fetcher.data(), SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*)), - SIGNAL(signalResultFound(Tellico::Fetch::FetchResult*))); - connect(fetcher.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher*)), - SLOT(slotFetcherDone(Tellico::Fetch::Fetcher*))); + connect(fetcher.data(), &Fetcher::signalResultFound, + this, &Manager::signalResultFound); + connect(fetcher.data(), &Fetcher::signalDone, + this, &Manager::slotFetcherDone); fetcher->continueSearch(); } else { emit signalDone(); } } bool Manager::hasMoreResults() const { if(m_currentFetcherIndex < 0 || m_currentFetcherIndex >= static_cast(m_fetchers.count())) { return false; } Fetcher::Ptr fetcher = m_fetchers[m_currentFetcherIndex]; return fetcher && fetcher->hasMoreResults(); } void Manager::stop() { // DEBUG_LINE; foreach(Fetcher::Ptr fetcher, m_fetchers) { if(fetcher->isSearching()) { fetcher->stop(); fetcher->saveConfig(); } } #ifndef NDEBUG if(m_count != 0) { myDebug() << "count should be 0!"; } #endif m_count = 0; } void Manager::slotFetcherDone(Tellico::Fetch::Fetcher* fetcher_) { // myDebug() << (fetcher_ ? fetcher_->source() : QString()) << ":" << m_count; fetcher_->disconnect(); // disconnect all signals fetcher_->saveConfig(); --m_count; if(m_count <= 0) { emit signalDone(); } } bool Manager::canFetch() const { foreach(Fetcher::Ptr fetcher, m_fetchers) { if(fetcher->canFetch(Data::Document::self()->collection()->type())) { return true; } } return false; } Tellico::Fetch::Fetcher::Ptr Manager::createFetcher(KSharedConfigPtr config_, const QString& group_) { if(!config_->hasGroup(group_)) { myDebug() << "no config group for " << group_; return Fetcher::Ptr(); } KConfigGroup config(config_, group_); int fetchType = config.readEntry("Type", int(Fetch::Unknown)); if(fetchType == Fetch::Unknown) { myDebug() << "unknown type " << fetchType << ", skipping"; return Fetcher::Ptr(); } // special case: the BoardGameGeek fetcher was originally implemented as a Ruby script // now, it's available with an XML API, so prefer the new version // so check for fetcher version and switch to the XML if version is missing or lower if(fetchType == Fetch::ExecExternal && config.readPathEntry("ExecPath", QString()).endsWith(QLatin1String("boardgamegeek.rb"))) { KConfigGroup generalConfig(config_, QStringLiteral("General Options")); if(generalConfig.readEntry("FetchVersion", 0) < 1) { fetchType = Fetch::BoardGameGeek; generalConfig.writeEntry("FetchVersion", 1); } } // special case: the Bedetheque fetcher was originally implemented as a Python script // now, it's available as a builtin data source, so prefer the new version // so check for fetcher version and switch to the newer if version is missing or lower if(fetchType == Fetch::ExecExternal && config.readPathEntry("ExecPath", QString()).endsWith(QStringLiteral("bedetheque.py"))) { KConfigGroup generalConfig(config_, QStringLiteral("General Options")); if(generalConfig.readEntry("FetchVersion", 0) < 2) { fetchType = Fetch::Bedetheque; generalConfig.writeEntry("FetchVersion", 2); } } Fetcher::Ptr f; if(functionRegistry.contains(fetchType)) { f = functionRegistry.value(fetchType).create(this); f->readConfig(config, group_); } return f; } #define FETCHER_ADD(type) \ do { \ if(functionRegistry.contains(type)) { \ vec.append(functionRegistry.value(type).create(this)); \ } \ } while(false) // static Tellico::Fetch::FetcherVec Manager::defaultFetchers() { FetcherVec vec; vec.append(SRUFetcher::libraryOfCongress(this)); // books FETCHER_ADD(ISBNdb); FETCHER_ADD(OpenLibrary); FETCHER_ADD(GoogleBook); // comic books FETCHER_ADD(AnimeNfo); FETCHER_ADD(Bedetheque); FETCHER_ADD(ComicVine); // bibliographic FETCHER_ADD(Arxiv); FETCHER_ADD(GoogleScholar); FETCHER_ADD(BiblioShare); FETCHER_ADD(DBLP); FETCHER_ADD(HathiTrust); // music FETCHER_ADD(MusicBrainz); // video games FETCHER_ADD(TheGamesDB); FETCHER_ADD(IGDB); FETCHER_ADD(VNDB); FETCHER_ADD(VideoGameGeek); // board games FETCHER_ADD(BoardGameGeek); // movies FETCHER_ADD(TheMovieDB); #ifdef ENABLE_IMDB FETCHER_ADD(IMDB); #endif QStringList langs = QLocale().uiLanguages(); if(langs.first().contains(QLatin1Char('-'))) { // I'm not sure QT always include two-letter locale codes langs << langs.first().section(QLatin1Char('-'), 0, 0); } // only add IBS if user includes italian if(langs.contains(QStringLiteral("it"))) { FETCHER_ADD(IBS); } if(langs.contains(QStringLiteral("fr"))) { FETCHER_ADD(DVDFr); FETCHER_ADD(Allocine); } if(langs.contains(QStringLiteral("ru"))) { FETCHER_ADD(KinoPoisk); } if(langs.contains(QStringLiteral("ua"))) { FETCHER_ADD(KinoTeatr); } if(langs.contains(QStringLiteral("de"))) { FETCHER_ADD(Kino); } if(langs.contains(QStringLiteral("cn"))) { FETCHER_ADD(Douban); } if(langs.contains(QStringLiteral("dk"))) { FETCHER_ADD(DBC); } return vec; } #undef FETCHER_ADD Tellico::Fetch::FetcherVec Manager::createUpdateFetchers(int collType_) { if(m_loadDefaults) { return defaultFetchers(); } FetcherVec vec; KConfigGroup config(KSharedConfig::openConfig(), "Data Sources"); int nSources = config.readEntry("Sources Count", 0); for(int i = 0; i < nSources; ++i) { QString group = QStringLiteral("Data Source %1").arg(i); // needs the KConfig* Fetcher::Ptr fetcher = createFetcher(KSharedConfig::openConfig(), group); if(fetcher && fetcher->canFetch(collType_) && fetcher->canUpdate()) { vec.append(fetcher); } } return vec; } Tellico::Fetch::FetcherVec Manager::createUpdateFetchers(int collType_, Tellico::Fetch::FetchKey key_) { FetcherVec fetchers; // creates new fetchers FetcherVec allFetchers = createUpdateFetchers(collType_); foreach(Fetcher::Ptr fetcher, allFetchers) { if(fetcher->canSearch(key_)) { fetchers.append(fetcher); } } return fetchers; } Tellico::Fetch::Fetcher::Ptr Manager::createUpdateFetcher(int collType_, const QString& source_) { Fetcher::Ptr newFetcher; // creates new fetchers FetcherVec fetchers = createUpdateFetchers(collType_); foreach(Fetcher::Ptr fetcher, fetchers) { if(fetcher->source() == source_) { newFetcher = fetcher; break; } } return newFetcher; } void Manager::updateStatus(const QString& message_) { emit signalStatus(message_); } Tellico::Fetch::NameTypeMap Manager::nameTypeMap() { Fetch::NameTypeMap map; FunctionRegistry::const_iterator it = functionRegistry.constBegin(); while(it != functionRegistry.constEnd()) { map.insert(functionRegistry.value(it.key()).name(), static_cast(it.key())); ++it; } // now find all the scripts distributed with tellico QStringList files = Tellico::locateAllFiles(QStringLiteral("tellico/data-sources/*.spec")); foreach(const QString& file, files) { KConfig spec(file, KConfig::SimpleConfig); KConfigGroup specConfig(&spec, QString()); QString name = specConfig.readEntry("Name"); if(name.isEmpty()) { myDebug() << "no name for" << file; continue; } bool enabled = specConfig.readEntry("Enabled", true); if(!enabled || !bundledScriptHasExecPath(file, specConfig)) { // no available exec continue; } map.insert(name, ExecExternal); m_scriptMap.insert(name, file); } return map; } // called when creating a new fetcher Tellico::Fetch::ConfigWidget* Manager::configWidget(QWidget* parent_, Tellico::Fetch::Type type_, const QString& name_) { ConfigWidget* w = nullptr; if(functionRegistry.contains(type_)) { w = functionRegistry.value(type_).configWidget(parent_); } else { myWarning() << "no widget defined for type =" << type_; } if(w && type_ == ExecExternal) { if(!name_.isEmpty() && m_scriptMap.contains(name_)) { // bundledScriptHasExecPath() actually needs to write the exec path // back to the config so the configWidget can read it. But if the spec file // is not readable, that doesn't work. So work around it with a copy to a temp file QTemporaryFile tmpFile; tmpFile.open(); QUrl from = QUrl::fromLocalFile(m_scriptMap[name_]); QUrl to = QUrl::fromLocalFile(tmpFile.fileName()); // have to overwrite since QTemporaryFile already created it KIO::Job* job = KIO::file_copy(from, to, -1, KIO::Overwrite); if(!job->exec()) { myDebug() << job->errorString(); } KConfig spec(to.path(), KConfig::SimpleConfig); KConfigGroup specConfig(&spec, QString()); // pass actual location of spec file if(name_ == specConfig.readEntry("Name") && bundledScriptHasExecPath(m_scriptMap[name_], specConfig)) { w->readConfig(specConfig); } else { myWarning() << "Can't read config file for " << to.path(); } } } return w; } // static QString Manager::typeName(Tellico::Fetch::Type type_) { if(self()->functionRegistry.contains(type_)) { return self()->functionRegistry.value(type_).name(); } myWarning() << "none found for" << type_; return QString(); } QPixmap Manager::fetcherIcon(Tellico::Fetch::Fetcher::Ptr fetcher_, int group_, int size_) { if(fetcher_->type() == Fetch::Z3950) { #ifdef HAVE_YAZ const Fetch::Z3950Fetcher* f = static_cast(fetcher_.data()); QUrl u; u.setScheme(QStringLiteral("http")); u.setHost(f->host()); QString icon = Fetcher::favIcon(u); if(!icon.isEmpty()) { return LOAD_ICON(icon, group_, size_); } #endif } else if(fetcher_->type() == Fetch::ExecExternal) { const Fetch::ExecExternalFetcher* f = static_cast(fetcher_.data()); const QString p = f->execPath(); QUrl u; if(p.contains(QStringLiteral("allocine"))) { u = QUrl(QStringLiteral("http://www.allocine.fr")); } else if(p.contains(QStringLiteral("ministerio_de_cultura"))) { u = QUrl(QStringLiteral("http://www.mcu.es")); } else if(p.contains(QStringLiteral("dark_horse_comics"))) { u = QUrl(QStringLiteral("http://www.darkhorse.com")); } else if(p.contains(QStringLiteral("boardgamegeek"))) { u = QUrl(QStringLiteral("http://www.boardgamegeek.com")); } else if(p.contains(QStringLiteral("supercat"))) { u = QUrl(QStringLiteral("https://evergreen-ils.org")); } else if(f->source().contains(QStringLiteral("amarok"), Qt::CaseInsensitive)) { return LOAD_ICON(QStringLiteral("amarok"), group_, size_); } if(!u.isEmpty() && u.isValid()) { QString icon = Fetcher::favIcon(u); if(!icon.isEmpty()) { return LOAD_ICON(icon, group_, size_); } } } return fetcherIcon(fetcher_->type(), group_, size_); } QPixmap Manager::fetcherIcon(Tellico::Fetch::Type type_, int group_, int size_) { QString name; if(self()->functionRegistry.contains(type_)) { name = self()->functionRegistry.value(type_).icon(); } else { myWarning() << "no pixmap defined for type =" << type_; } if(name.isEmpty()) { // use default tellico application icon name = QStringLiteral("tellico"); } QPixmap pix = KIconLoader::global()->loadIcon(name, static_cast(group_), size_, KIconLoader::DefaultState, QStringList(), nullptr, true); if(pix.isNull()) { QIcon icon = QIcon::fromTheme(name); const int groupSize = KIconLoader::global()->currentSize(static_cast(group_)); size_ = size_ == 0 ? groupSize : size_; pix = icon.pixmap(size_, size_); } if(pix.isNull()) { pix = BarIcon(name); } return pix; } Tellico::StringHash Manager::optionalFields(Type type_) { if(self()->functionRegistry.contains(type_)) { return self()->functionRegistry.value(type_).optionalFields(); } return StringHash(); } bool Manager::bundledScriptHasExecPath(const QString& specFile_, KConfigGroup& config_) { // make sure ExecPath is set and executable // for the bundled scripts, either the exec name is not set, in which case it is the // name of the spec file, minus the .spec, or the exec is set, and is local to the dir // if not, look for it QFileInfo specInfo(specFile_); QString exec = config_.readPathEntry("ExecPath", QString()); QFileInfo execInfo(exec); if(exec.isEmpty() || !execInfo.exists()) { exec = specInfo.canonicalPath() + QDir::separator() + specInfo.completeBaseName(); // remove ".spec" } else if(execInfo.isRelative()) { exec = specInfo.canonicalPath() + QDir::separator() + exec; } else if(!execInfo.isExecutable()) { myWarning() << "not executable:" << specFile_; return false; } execInfo.setFile(exec); if(!execInfo.exists() || !execInfo.isExecutable()) { myWarning() << "no exec file for" << specFile_; myWarning() << "exec =" << exec; return false; // we're not ok } config_.writePathEntry("ExecPath", exec); config_.sync(); // might be readonly, but that's ok return true; } diff --git a/src/fetch/filmasterfetcher.cpp b/src/fetch/filmasterfetcher.cpp index aed124ac..14f1df2c 100644 --- a/src/fetch/filmasterfetcher.cpp +++ b/src/fetch/filmasterfetcher.cpp @@ -1,329 +1,329 @@ /*************************************************************************** Copyright (C) 2011 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "filmasterfetcher.h" #include "../collections/videocollection.h" #include "../images/imagefactory.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../entry.h" #include "../core/filehandler.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const char* FILMASTER_API_URL = "http://api.filmaster.com"; static const char* FILMASTER_QUERY_URL = "http://api.filmaster.com/1.1/search/"; } using namespace Tellico; using Tellico::Fetch::FilmasterFetcher; FilmasterFetcher::FilmasterFetcher(QObject* parent_) : Fetcher(parent_), m_started(false) { } FilmasterFetcher::~FilmasterFetcher() { } QString FilmasterFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } QString FilmasterFetcher::attribution() const { return i18n("This data is licensed under specific terms.", QLatin1String("http://filmaster.com/license/")); } bool FilmasterFetcher::canSearch(FetchKey k) const { return k == Title || k == Person || k == Keyword; } bool FilmasterFetcher::canFetch(int type) const { return type == Data::Collection::Video; } void FilmasterFetcher::readConfigHook(const KConfigGroup&) { } void FilmasterFetcher::search() { m_started = true; QUrl u(QString::fromLatin1(FILMASTER_QUERY_URL)); switch(request().key) { case Title: u.setPath(u.path() + QLatin1String("film/")); break; case Person: u.setPath(u.path() + QLatin1String("person/")); break; default: break; } QUrlQuery q; q.addQueryItem(QStringLiteral("phrase"), request().value); u.setQuery(q); // myDebug() << "url:" << u; QPointer job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(job, GUI::Proxy::widget()); - connect(job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(job.data(), &KJob::result, this, &FilmasterFetcher::slotComplete); } void FilmasterFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } Tellico::Data::EntryPtr FilmasterFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries.value(uid_); if(!entry) { myWarning() << "no entry in dict"; return Data::EntryPtr(); } const QString image = entry->field(QStringLiteral("cover")); if(image.contains(QLatin1Char('/'))) { QUrl imageUrl; if(image.startsWith(QLatin1String("//"))) { imageUrl = QUrl(QLatin1String("http:") + image); } else { imageUrl = QUrl(QString::fromLatin1(FILMASTER_API_URL)); imageUrl.setPath(imageUrl.path() + image); } const QString id = ImageFactory::addImage(imageUrl, true); if(id.isEmpty()) { myDebug() << "Failed to load" << imageUrl; message(i18n("The cover image could not be loaded."), MessageHandler::Warning); } // empty image ID is ok entry->setField(QStringLiteral("cover"), id); } return entry; } Tellico::Fetch::FetchRequest FilmasterFetcher::updateRequest(Data::EntryPtr entry_) { const QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Title, title); } return FetchRequest(); } void FilmasterFetcher::slotComplete(KJob* job_) { KIO::StoredTransferJob* job = static_cast(job_); // myDebug(); if(job->error()) { job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from filmasterfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); QVariantMap resultsMap = doc.object().toVariantMap(); QVariantList resultList; switch(request().key) { case Title: resultList = resultsMap.value(QStringLiteral("best_results")).toList() + resultsMap.value(QStringLiteral("results")).toList(); break; case Person: { const QVariantList personList = resultsMap.value(QStringLiteral("best_results")).toList(); QStringList uris; foreach(const QVariant& person, personList) { const QVariantMap personMap = person.toMap(); uris << mapValue(personMap, "films_played_uri"); uris << mapValue(personMap, "films_directed_uri"); } foreach(const QString& uri, uris) { QUrl u(QString::fromLatin1(FILMASTER_API_URL)); u.setPath(uri); QString output = FileHandler::readTextFile(u, false /*quiet*/, true /*utf8*/); QJsonDocument doc2 = QJsonDocument::fromJson(output.toUtf8()); resultList += doc2.object().toVariantMap().value(QStringLiteral("objects")).toList(); } } break; case Keyword: resultList = resultsMap.value(QStringLiteral("films")).toMap().value(QStringLiteral("best_results")).toList(); break; default: break; } if(resultList.isEmpty()) { myDebug() << "no results"; stop(); return; } Data::CollPtr coll(new Data::VideoCollection(true)); if(!coll->hasField(QStringLiteral("filmaster")) && optionalFields().contains(QStringLiteral("filmaster"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("filmaster"), i18n("Filmaster Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); } foreach(const QVariant& result, resultList) { // myDebug() << "found result:" << result; Data::EntryPtr entry(new Data::Entry(coll)); populateEntry(entry, result.toMap()); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } // m_start = m_entries.count(); // m_hasMoreResults = m_start <= m_total; m_hasMoreResults = false; // for now, no continued searches stop(); } void FilmasterFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& result_) { entry_->setField(QStringLiteral("title"), mapValue(result_, "title")); entry_->setField(QStringLiteral("year"), mapValue(result_, "release_year")); entry_->setField(QStringLiteral("genre"), mapValue(result_, "tags")); entry_->setField(QStringLiteral("nationality"), mapValue(result_, "production_country_list")); entry_->setField(QStringLiteral("cover"), mapValue(result_, "image")); entry_->setField(QStringLiteral("plot"), mapValue(result_, "description")); QStringList directors; foreach(const QVariant& director, result_.value(QLatin1String("directors")).toList()) { const QVariantMap directorMap = director.toMap(); directors << mapValue(directorMap, "name") + QLatin1Char(' ') + mapValue(directorMap, "surname"); } if(!directors.isEmpty()) { entry_->setField(QStringLiteral("director"), directors.join(FieldFormat::delimiterString())); } const QString castUri = mapValue(result_, "characters_uri"); if(!castUri.isEmpty()) { QUrl u(QString::fromLatin1(FILMASTER_API_URL)); u.setPath(castUri); QString output = FileHandler::readTextFile(u, false /*quiet*/, true /*utf8*/); QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8()); QVariantList castList = doc.object().toVariantMap().value(QStringLiteral("objects")).toList(); QStringList castLines; foreach(const QVariant& castResult, castList) { const QVariantMap castMap = castResult.toMap(); const QVariantMap nameMap = castMap.value(QStringLiteral("person")).toMap(); castLines << mapValue(nameMap, "name") + QLatin1Char(' ') + mapValue(nameMap, "surname") + FieldFormat::columnDelimiterString() + mapValue(castMap, "character"); } if(!castLines.isEmpty()) { entry_->setField(QStringLiteral("cast"), castLines.join(FieldFormat::rowDelimiterString())); } } if(optionalFields().contains(QStringLiteral("filmaster"))) { entry_->setField(QStringLiteral("filmaster"), QLatin1String("http://filmaster.com/film/") + mapValue(result_, "permalink")); } } Tellico::Fetch::ConfigWidget* FilmasterFetcher::configWidget(QWidget* parent_) const { return new FilmasterFetcher::ConfigWidget(parent_, this); } QString FilmasterFetcher::defaultName() { return QStringLiteral("Filmaster"); // no translation } QString FilmasterFetcher::defaultIcon() { return favIcon("http://www.filmaster.com"); } Tellico::StringHash FilmasterFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("filmaster")] = i18n("Filmaster Link"); return hash; } FilmasterFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const FilmasterFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(FilmasterFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } void FilmasterFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) { } QString FilmasterFetcher::ConfigWidget::preferredName() const { return FilmasterFetcher::defaultName(); } diff --git a/src/fetch/gcstarpluginfetcher.cpp b/src/fetch/gcstarpluginfetcher.cpp index ffc4e61f..181c956d 100644 --- a/src/fetch/gcstarpluginfetcher.cpp +++ b/src/fetch/gcstarpluginfetcher.cpp @@ -1,482 +1,483 @@ /*************************************************************************** Copyright (C) 2005-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "gcstarpluginfetcher.h" #include "gcstarthread.h" #include "fetchmanager.h" #include "../collection.h" #include "../entry.h" #include "../translators/gcstarimporter.h" #include "../gui/combobox.h" #include "../gui/collectiontypecombo.h" #include "../utils/cursorsaver.h" #include "../core/filehandler.h" #include "../utils/guiproxy.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Tellico; using Tellico::Fetch::GCstarPluginFetcher; GCstarPluginFetcher::CollectionPlugins GCstarPluginFetcher::collectionPlugins; GCstarPluginFetcher::PluginParse GCstarPluginFetcher::pluginParse = NotYet; //static GCstarPluginFetcher::PluginList GCstarPluginFetcher::plugins(int collType_) { if(!collectionPlugins.contains(collType_)) { GUI::CursorSaver cs; QString gcstar = QStandardPaths::findExecutable(QStringLiteral("gcstar")); if(pluginParse == NotYet) { KProcess proc; proc.setProgram(gcstar, QStringList() << QStringLiteral("--version")); proc.setOutputChannelMode(KProcess::OnlyStdoutChannel); // wait 5 seconds at most, just a sanity thing, never want to block completely if(proc.execute(5000) > -1) { QString output = QString::fromLocal8Bit(proc.readAllStandardOutput()); if(!output.isEmpty()) { // always going to be x.y[.z] ? QRegExp versionRx(QLatin1String("(\\d+)\\.(\\d+)(?:\\.(\\d+))?")); if(versionRx.indexIn(output) > -1) { int x = versionRx.cap(1).toInt(); int y = versionRx.cap(2).toInt(); int z = versionRx.cap(3).toInt(); // ok to be empty myDebug() << QStringLiteral("found %1.%2.%3").arg(x).arg(y).arg(z); // --list-plugins argument was added for 1.3 release pluginParse = (x >= 1 && y >=3) ? New : Old; } } } // if still zero, then we should use old in future if(pluginParse == NotYet) { pluginParse = Old; } } if(pluginParse == New) { readPluginsNew(collType_, gcstar); } else { readPluginsOld(collType_, gcstar); } } return collectionPlugins.contains(collType_) ? collectionPlugins.value(collType_) : GCstarPluginFetcher::PluginList(); } void GCstarPluginFetcher::readPluginsNew(int collType_, const QString& gcstar_) { PluginList plugins; const QString gcstarCollection = gcstarType(collType_); if(gcstarCollection.isEmpty()) { collectionPlugins.insert(collType_, plugins); return; } QStringList args; args << QStringLiteral("--execute") << QStringLiteral("--list-plugins") << QStringLiteral("--collection") << gcstarCollection; KProcess proc; proc.setProgram(gcstar_, args); proc.setOutputChannelMode(KProcess::OnlyStdoutChannel); if(proc.execute() < 0) { myWarning() << "can't start"; return; } bool hasName = false; PluginInfo info; QTextStream stream(&proc); for(QString line = stream.readLine(); !stream.atEnd(); line = stream.readLine()) { if(line.isEmpty()) { if(hasName) { plugins << info; } hasName = false; info.clear(); } else { // authors have \t at beginning line = line.trimmed(); if(!hasName) { info.insert(QStringLiteral("name"), line); hasName = true; } else { info.insert(QStringLiteral("author"), line); } // myDebug() << line; } } collectionPlugins.insert(collType_, plugins); } void GCstarPluginFetcher::readPluginsOld(int collType_, const QString& gcstar_) { QDir dir(gcstar_, QStringLiteral("GC*.pm")); dir.cd(QStringLiteral("../../lib/gcstar/GCPlugins/")); QRegExp rx(QLatin1String("get(Name|Author|Lang)\\s*\\{\\s*return\\s+['\"](.+)['\"]")); rx.setMinimal(true); PluginList plugins; const QString dirName = gcstarType(collType_); if(dirName.isEmpty()) { collectionPlugins.insert(collType_, plugins); return; } foreach(const QString& file, dir.entryList()) { QUrl u = QUrl::fromLocalFile(dir.filePath(file)); PluginInfo info; QString text = FileHandler::readTextFile(u); for(int pos = rx.indexIn(text); pos > -1; pos = rx.indexIn(text, pos+rx.matchedLength())) { info.insert(rx.cap(1).toLower(), rx.cap(2)); } // only add if it has a name if(info.contains(QStringLiteral("name"))) { plugins << info; } } // inserting empty list is ok collectionPlugins.insert(collType_, plugins); } QString GCstarPluginFetcher::gcstarType(int collType_) { switch(collType_) { case Data::Collection::Book: return QStringLiteral("GCbooks"); case Data::Collection::Video: return QStringLiteral("GCfilms"); case Data::Collection::Album: return QStringLiteral("GCmusics"); case Data::Collection::ComicBook: return QStringLiteral("GCcomics"); case Data::Collection::Wine: return QStringLiteral("GCwines"); case Data::Collection::Coin: return QStringLiteral("GCcoins"); case Data::Collection::Stamp: return QStringLiteral("GCstamps"); case Data::Collection::Game: return QStringLiteral("GCgames"); case Data::Collection::BoardGame: return QStringLiteral("GCboardgames"); default: break; } return QString(); } GCstarPluginFetcher::GCstarPluginFetcher(QObject* parent_) : Fetcher(parent_), m_started(false), m_collType(-1), m_thread(nullptr) { } GCstarPluginFetcher::~GCstarPluginFetcher() { stop(); } QString GCstarPluginFetcher::source() const { return m_name; } bool GCstarPluginFetcher::canFetch(int type_) const { return m_collType == -1 ? false : m_collType == type_; } void GCstarPluginFetcher::readConfigHook(const KConfigGroup& config_) { m_collType = config_.readEntry("CollectionType", -1); m_plugin = config_.readEntry("Plugin"); } void GCstarPluginFetcher::search() { m_started = true; if(m_plugin.isEmpty() || m_collType == -1) { myWarning() << "no plugin information!"; myDebug() << m_collType << m_plugin; stop(); return; } m_data.clear(); const QString gcstar = QStandardPaths::findExecutable(QStringLiteral("gcstar")); if(gcstar.isEmpty()) { myWarning() << "gcstar not found!"; stop(); return; } QStringList args; args << QStringLiteral("--execute") << QStringLiteral("--collection") << gcstarType(m_collType) << QStringLiteral("--export") << QStringLiteral("TarGz") << QStringLiteral("--exportprefs") << QStringLiteral("collection=>/tmp/test.gcs,file=>/tmp/test1.tar.gz") << QStringLiteral("--website") << m_plugin << QStringLiteral("--download") << KShell::quoteArg(request().value); myLog() << args; m_thread = new GCstarThread(this); m_thread->setProgram(gcstar, args); - connect(m_thread, SIGNAL(standardOutput(QByteArray)), SLOT(slotData(QByteArray))); - connect(m_thread, SIGNAL(standardError(QByteArray)), SLOT(slotError(QByteArray))); - connect(m_thread, SIGNAL(finished()), SLOT(slotProcessExited())); + connect(m_thread, &GCstarThread::standardOutput, this, &GCstarPluginFetcher::slotData); + connect(m_thread, &GCstarThread::standardError, this, &GCstarPluginFetcher::slotError); + connect(m_thread, &QThread::finished, this, &GCstarPluginFetcher::slotProcessExited); m_thread->start(); } void GCstarPluginFetcher::stop() { if(!m_started) { return; } if(m_thread) { if(m_thread->isRunning()) { m_thread->terminate(); m_thread->wait(); } delete m_thread; m_thread = nullptr; } m_data.clear(); m_started = false; m_errors.clear(); emit signalDone(this); } void GCstarPluginFetcher::slotData(const QByteArray& data_) { m_data.append(data_); } void GCstarPluginFetcher::slotError(const QByteArray& data_) { QString msg = QString::fromLocal8Bit(data_); msg.prepend(source() + QLatin1String(": ")); myDebug() << msg; m_errors << msg; } void GCstarPluginFetcher::slotProcessExited() { // if stop() is called and the thread terminated // the finished() signal will still fire if(!m_started) { return; } if(!m_errors.isEmpty()) { message(m_errors.join(QLatin1String("\n")), MessageHandler::Warning); } if(m_data.isEmpty()) { myDebug() << source() << ": no data"; stop(); return; } QBuffer filterBuffer(&m_data); KCompressionDevice::CompressionType compressionType = KFilterDev::compressionTypeForMimeType(QStringLiteral("application/x-gzip")); KCompressionDevice filter(&filterBuffer, false, compressionType); if(!filter.open(QIODevice::ReadOnly)) { myWarning() << "unable to open gzip filter"; stop(); return; } QByteArray tarData = filter.readAll(); QBuffer buffer(&tarData); KTar tar(&buffer); if(!tar.open(QIODevice::ReadOnly)) { myWarning() << "unable to open tar file"; stop(); return; } const KArchiveDirectory* dir = tar.directory(); if(!dir) { myWarning() << "unable to open tar directory"; stop(); return; } QTemporaryDir tempDir; dir->copyTo(tempDir.path()); // KDE seems to have a bug (#252821) for gcstar files where the images are not in the images/ directory foreach(const QString& filename, dir->entries()) { if(dir->entry(filename)->isFile() && filename != QLatin1String("collection.gcs")) { const KArchiveFile* f = static_cast(dir->entry(filename)); f->copyTo(tempDir.path() + QLatin1String("/images")); } } QUrl gcsUrl = QUrl::fromLocalFile(tempDir.path()); gcsUrl = gcsUrl.adjusted(QUrl::StripTrailingSlash); gcsUrl.setPath(gcsUrl.path() + QLatin1String("/collection.gcs")); Import::GCstarImporter imp(gcsUrl); imp.setHasRelativeImageLinks(true); Data::CollPtr coll = imp.collection(); if(!coll) { if(!imp.statusMessage().isEmpty()) { message(imp.statusMessage(), MessageHandler::Status); } myWarning() << "no collection pointer"; stop(); return; } foreach(Data::EntryPtr entry, coll->entries()) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); if(!m_started) { return; } } stop(); // be sure to call this } Tellico::Data::EntryPtr GCstarPluginFetcher::fetchEntryHook(uint uid_) { return m_entries[uid_]; } Tellico::Fetch::FetchRequest GCstarPluginFetcher::updateRequest(Data::EntryPtr entry_) { // ry searching for title and rely on Collection::sameEntry() to figure things out QString t = entry_->field(QStringLiteral("title")); if(!t.isEmpty()) { return FetchRequest(Fetch::Title, t); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* GCstarPluginFetcher::configWidget(QWidget* parent_) const { return new GCstarPluginFetcher::ConfigWidget(parent_, this); } QString GCstarPluginFetcher::defaultName() { return i18n("GCstar Plugin"); } QString GCstarPluginFetcher::defaultIcon() { return QStringLiteral("gcstar"); } GCstarPluginFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GCstarPluginFetcher* fetcher_/*=0*/) : Fetch::ConfigWidget(parent_), m_needPluginList(true) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* label = new QLabel(i18n("Collection &type:"), optionsWidget()); l->addWidget(label, ++row, 0); m_collCombo = new GUI::CollectionTypeCombo(optionsWidget()); - connect(m_collCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); - connect(m_collCombo, SIGNAL(activated(int)), SLOT(slotTypeChanged())); + void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated; + connect(m_collCombo, activatedInt, this, &ConfigWidget::slotSetModified); + connect(m_collCombo, activatedInt, this, &ConfigWidget::slotTypeChanged); l->addWidget(m_collCombo, row, 1, 1, 3); QString w = i18n("Set the collection type of the data returned from the plugin."); label->setWhatsThis(w); m_collCombo->setWhatsThis(w); label->setBuddy(m_collCombo); label = new QLabel(i18n("&Plugin: "), optionsWidget()); l->addWidget(label, ++row, 0); m_pluginCombo = new GUI::ComboBox(optionsWidget()); - connect(m_pluginCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); - connect(m_pluginCombo, SIGNAL(activated(int)), SLOT(slotPluginChanged())); + connect(m_pluginCombo, activatedInt, this, &ConfigWidget::slotSetModified); + connect(m_pluginCombo, activatedInt, this, &ConfigWidget::slotPluginChanged); l->addWidget(m_pluginCombo, row, 1, 1, 3); w = i18n("Select the GCstar plugin used for the data source."); label->setWhatsThis(w); m_pluginCombo->setWhatsThis(w); label->setBuddy(m_pluginCombo); label = new QLabel(i18n("Author: "), optionsWidget()); l->addWidget(label, ++row, 0); m_authorLabel = new QLabel(optionsWidget()); l->addWidget(m_authorLabel, row, 1); if(fetcher_) { if(fetcher_->m_collType > -1) { m_collCombo->setCurrentType(fetcher_->m_collType); } else { m_collCombo->setCurrentType(fetcher_->collectionType()); } m_originalPluginName = fetcher_->m_plugin; } else { // default to Book for now m_collCombo->setCurrentType(Data::Collection::Book); } KAcceleratorManager::manage(optionsWidget()); } GCstarPluginFetcher::ConfigWidget::~ConfigWidget() { } void GCstarPluginFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { config_.writeEntry("CollectionType", m_collCombo->currentType()); config_.writeEntry("Plugin", m_pluginCombo->currentText()); } QString GCstarPluginFetcher::ConfigWidget::preferredName() const { QString plugin = m_pluginCombo->currentText(); return plugin.isEmpty() ? plugin : QLatin1String("GCstar - ") + plugin; } void GCstarPluginFetcher::ConfigWidget::slotTypeChanged() { int collType = m_collCombo->currentType(); m_pluginCombo->clear(); QStringList pluginNames; GCstarPluginFetcher::PluginList list = GCstarPluginFetcher::plugins(collType); foreach(const GCstarPluginFetcher::PluginInfo& info, list) { pluginNames << info.value(QStringLiteral("name")).toString(); m_pluginCombo->addItem(pluginNames.last(), info); } slotPluginChanged(); emit signalName(preferredName()); } void GCstarPluginFetcher::ConfigWidget::slotPluginChanged() { PluginInfo info = m_pluginCombo->currentData().toHash(); m_authorLabel->setText(info[QStringLiteral("author")].toString()); emit signalName(preferredName()); } void GCstarPluginFetcher::ConfigWidget::showEvent(QShowEvent*) { if(m_needPluginList) { m_needPluginList = false; slotTypeChanged(); // update plugin combo box if(!m_originalPluginName.isEmpty()) { m_pluginCombo->setEditText(m_originalPluginName); slotPluginChanged(); } } } diff --git a/src/fetch/gcstarthread.cpp b/src/fetch/gcstarthread.cpp index 175a3f7f..608b52b8 100644 --- a/src/fetch/gcstarthread.cpp +++ b/src/fetch/gcstarthread.cpp @@ -1,54 +1,54 @@ /*************************************************************************** Copyright (C) 2010 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "gcstarthread.h" #include using Tellico::Fetch::GCstarThread; GCstarThread::GCstarThread(QObject* obj) : QThread(obj) { } void GCstarThread::setProgram(const QString& program_, const QStringList& args_) { m_program = program_; m_args = args_; } void GCstarThread::run() { KProcess proc; - connect(&proc, SIGNAL(readyReadStandardOutput()), SLOT(slotData())); - connect(&proc, SIGNAL(readyReadStandardError()), SLOT(slotError())); + connect(&proc, &QProcess::readyReadStandardOutput, this, &GCstarThread::slotData); + connect(&proc, &QProcess::readyReadStandardError, this, &GCstarThread::slotError); proc.setOutputChannelMode(KProcess::SeparateChannels); proc.setProgram(m_program, m_args); proc.execute(); } void GCstarThread::slotData() { emit standardOutput(static_cast(sender())->readAllStandardOutput()); } void GCstarThread::slotError() { emit standardError(static_cast(sender())->readAllStandardError()); } diff --git a/src/fetch/giantbombfetcher.cpp b/src/fetch/giantbombfetcher.cpp index f298c463..9fd379de 100644 --- a/src/fetch/giantbombfetcher.cpp +++ b/src/fetch/giantbombfetcher.cpp @@ -1,261 +1,261 @@ /*************************************************************************** Copyright (C) 2010 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "giantbombfetcher.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include namespace { static const int GIANTBOMB_MAX_RETURNS_TOTAL = 20; static const char* GIANTBOMB_API_URL = "http://api.giantbomb.com"; static const char* GIANTBOMB_API_KEY = "291bfe4b2d77a460e67dd8f90c1e7e56c3e4f05a"; } using namespace Tellico; using Tellico::Fetch::GiantBombFetcher; GiantBombFetcher::GiantBombFetcher(QObject* parent_) : XMLFetcher(parent_) , m_total(-1) , m_apiKey(QLatin1String(GIANTBOMB_API_KEY)) { setLimit(GIANTBOMB_MAX_RETURNS_TOTAL); setXSLTFilename(QStringLiteral("giantbomb2tellico.xsl")); } GiantBombFetcher::~GiantBombFetcher() { } QString GiantBombFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool GiantBombFetcher::canFetch(int type) const { return type == Data::Collection::Game; } void GiantBombFetcher::readConfigHook(const KConfigGroup& config_) { QString k = config_.readEntry("API Key", GIANTBOMB_API_KEY); if(!k.isEmpty()) { m_apiKey = k; } } void GiantBombFetcher::resetSearch() { m_total = -1; } QUrl GiantBombFetcher::searchUrl() { QUrl u(QString::fromLatin1(GIANTBOMB_API_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("format"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("api_key"), m_apiKey); switch(request().key) { case Keyword: u.setPath(QStringLiteral("/search")); q.addQueryItem(QStringLiteral("query"), request().value); q.addQueryItem(QStringLiteral("resources"), QStringLiteral("game")); break; default: myWarning() << "key not recognized: " << request().key; return QUrl(); } u.setQuery(q); // myDebug() << "url: " << u.url(); return u; } void GiantBombFetcher::parseData(QByteArray& data_) { Q_UNUSED(data_); #if 0 if(m_total == -1) { QDomDocument dom; if(!dom.setContent(data, false)) { myWarning() << "server did not return valid XML."; return; } // total is /resp/fetchresults/@numResults QDomNode n = dom.documentElement().namedItem(QLatin1String("resp")) .namedItem(QLatin1String("fetchresults")); QDomElement e = n.toElement(); if(!e.isNull()) { m_total = e.attribute(QLatin1String("numResults")).toInt(); myDebug() << "total = " << m_total; } } m_start = m_entries.count() + 1; // not sure how to specify start in the REST url // m_hasMoreResults = m_start <= m_total; #endif } Tellico::Data::EntryPtr GiantBombFetcher::fetchEntryHookData(Data::EntryPtr entry_) { Q_ASSERT(entry_); const QString id = entry_->field(QStringLiteral("giantbomb-id")); if(id.isEmpty()) { myDebug() << "no giantbomb id found"; return entry_; } QUrl u(QString::fromLatin1(GIANTBOMB_API_URL)); u.setPath(QStringLiteral("/game/%1/").arg(id)); QUrlQuery q; q.addQueryItem(QStringLiteral("format"), QStringLiteral("xml")); q.addQueryItem(QStringLiteral("api_key"), m_apiKey); u.setQuery(q); // myDebug() << "url: " << u; // quiet QString output = FileHandler::readXMLFile(u, true); #if 0 myWarning() << "Remove output debug from giantbombfetcher.cpp"; QFile f(QStringLiteral("/tmp/test2.xml")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << output; } f.close(); #endif Import::TellicoImporter imp(xsltHandler()->applyStylesheet(output)); // be quiet when loading images imp.setOptions(imp.options() ^ Import::ImportShowImageErrors); Data::CollPtr coll = imp.collection(); // getTracks(entry); if(!coll) { myWarning() << "no collection pointer"; return entry_; } if(coll->entryCount() > 1) { myDebug() << "weird, more than one entry found"; } // don't want to include id coll->removeField(QStringLiteral("giantbomb-id")); return coll->entries().front(); } Tellico::Fetch::FetchRequest GiantBombFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Keyword, title); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* GiantBombFetcher::configWidget(QWidget* parent_) const { return new GiantBombFetcher::ConfigWidget(parent_, this); } QString GiantBombFetcher::defaultName() { return QStringLiteral("Giant Bomb"); } QString GiantBombFetcher::defaultIcon() { return favIcon("http://www.giantbomb.com"); } Tellico::StringHash GiantBombFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("giantbomb")] = i18n("GiantBomb Link"); hash[QStringLiteral("pegi")] = i18n("PEGI Rating"); return hash; } GiantBombFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GiantBombFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QLatin1String("http://api.giantbomb.com")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_apiKeyEdit = new QLineEdit(optionsWidget()); - connect(m_apiKeyEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_apiKeyEdit, row, 1); QString w = i18n("The default Tellico key may be used, but searching may fail due to reaching access limits."); label->setWhatsThis(w); m_apiKeyEdit->setWhatsThis(w); label->setBuddy(m_apiKeyEdit); l->setRowStretch(++row, 10); // now add additional fields widget addFieldsWidget(GiantBombFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); if(fetcher_) { // only show the key if it is not the default Tellico one... // that way the user is prompted to apply for their own if(fetcher_->m_apiKey != QLatin1String(GIANTBOMB_API_KEY)) { m_apiKeyEdit->setText(fetcher_->m_apiKey); } } } void GiantBombFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString apiKey = m_apiKeyEdit->text().trimmed(); if(!apiKey.isEmpty()) { config_.writeEntry("API Key", apiKey); } } QString GiantBombFetcher::ConfigWidget::preferredName() const { return GiantBombFetcher::defaultName(); } diff --git a/src/fetch/googlebookfetcher.cpp b/src/fetch/googlebookfetcher.cpp index f350c720..12eb563f 100644 --- a/src/fetch/googlebookfetcher.cpp +++ b/src/fetch/googlebookfetcher.cpp @@ -1,423 +1,423 @@ /*************************************************************************** Copyright (C) 2011 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "googlebookfetcher.h" #include "../collections/bookcollection.h" #include "../entry.h" #include "../images/imagefactory.h" #include "../utils/isbnvalidator.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../core/filehandler.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const int GOOGLEBOOK_MAX_RETURNS = 20; static const char* GOOGLEBOOK_API_URL = "https://www.googleapis.com/books/v1/volumes"; static const char* GOOGLEBOOK_API_KEY = "AIzaSyBdsa_DEGpDQ6PzZyYHHHokRIBY8thOdUQ"; } using namespace Tellico; using Tellico::Fetch::GoogleBookFetcher; GoogleBookFetcher::GoogleBookFetcher(QObject* parent_) : Fetcher(parent_) , m_started(false) , m_start(0) , m_total(0) , m_apiKey(QLatin1String(GOOGLEBOOK_API_KEY)) { } GoogleBookFetcher::~GoogleBookFetcher() { } QString GoogleBookFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool GoogleBookFetcher::canSearch(FetchKey k) const { return k == Title || k == Person || k == ISBN || k == Keyword; } bool GoogleBookFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex; } void GoogleBookFetcher::readConfigHook(const KConfigGroup& config_) { // allow an empty key if the config key does exist m_apiKey = config_.readEntry("API Key", GOOGLEBOOK_API_KEY); } void GoogleBookFetcher::search() { m_start = 0; m_total = -1; continueSearch(); } void GoogleBookFetcher::continueSearch() { m_started = true; // we only split ISBN and LCCN values QStringList searchTerms; if(request().key == ISBN) { searchTerms = FieldFormat::splitValue(request().value); } else { searchTerms += request().value; } foreach(const QString& searchTerm, searchTerms) { doSearch(searchTerm); } if(m_jobs.isEmpty()) { stop(); } } void GoogleBookFetcher::doSearch(const QString& term_) { QUrl u(QString::fromLatin1(GOOGLEBOOK_API_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("maxResults"), QString::number(GOOGLEBOOK_MAX_RETURNS)); q.addQueryItem(QStringLiteral("startIndex"), QString::number(m_start)); q.addQueryItem(QStringLiteral("printType"), QStringLiteral("books")); // we don't require a key, cause it might work without it if(!m_apiKey.isEmpty()) { q.addQueryItem(QStringLiteral("key"), m_apiKey); } switch(request().key) { case Title: q.addQueryItem(QStringLiteral("q"), QLatin1String("intitle:") + term_); break; case Person: q.addQueryItem(QStringLiteral("q"), QLatin1String("inauthor:") + term_); break; case ISBN: { const QString isbn = ISBNValidator::cleanValue(term_); q.addQueryItem(QStringLiteral("q"), QLatin1String("isbn:") + isbn); } break; case Keyword: q.addQueryItem(QStringLiteral("q"), term_); break; default: myWarning() << "key not recognized:" << request().key; return; } u.setQuery(q); // myDebug() << "url:" << u; QPointer job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(job, GUI::Proxy::widget()); - connect(job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(job.data(), &KJob::result, this, &GoogleBookFetcher::slotComplete); m_jobs << job; } void GoogleBookFetcher::endJob(KIO::StoredTransferJob* job_) { m_jobs.removeOne(job_); if(m_jobs.isEmpty()) { stop(); } } void GoogleBookFetcher::stop() { if(!m_started) { return; } foreach(QPointer job, m_jobs) { if(job) { job->kill(); } } m_jobs.clear(); m_started = false; emit signalDone(this); } Tellico::Data::EntryPtr GoogleBookFetcher::fetchEntryHook(uint uid_) { Data::EntryPtr entry = m_entries.value(uid_); if(!entry) { myWarning() << "no entry in dict"; return Data::EntryPtr(); } QString gbs = entry->field(QStringLiteral("gbs-link")); if(!gbs.isEmpty()) { // quiet QByteArray data = FileHandler::readDataFile(QUrl::fromUserInput(gbs), true); QJsonDocument doc = QJsonDocument::fromJson(data); populateEntry(entry, doc.object().toVariantMap()); } const QString image_id = entry->field(QStringLiteral("cover")); // if it's still a url, we need to load it if(image_id.startsWith(QLatin1String("http"))) { const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true); if(id.isEmpty()) { message(i18n("The cover image could not be loaded."), MessageHandler::Warning); entry->setField(QStringLiteral("cover"), QString()); } else { entry->setField(QStringLiteral("cover"), id); } } // don't want to include gbs json link entry->setField(QStringLiteral("gbs-link"), QString()); return entry; } Tellico::Fetch::FetchRequest GoogleBookFetcher::updateRequest(Data::EntryPtr entry_) { const QString isbn = entry_->field(QStringLiteral("isbn")); if(!isbn.isEmpty()) { return FetchRequest(ISBN, isbn); } QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Title, title); } return FetchRequest(); } void GoogleBookFetcher::slotComplete(KJob* job_) { KIO::StoredTransferJob* job = static_cast(job_); // myDebug(); if(job->error()) { job->uiDelegate()->showErrorMessage(); endJob(job); return; } QByteArray data = job->data(); if(data.isEmpty()) { myDebug() << "no data"; endJob(job); return; } #if 0 myWarning() << "Remove debug from googlebookfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif Data::CollPtr coll(new Data::BookCollection(true)); // always add the gbs-link for fetchEntryHook Data::FieldPtr field(new Data::Field(QStringLiteral("gbs-link"), QStringLiteral("GBS Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); if(!coll->hasField(QStringLiteral("googlebook")) && optionalFields().contains(QStringLiteral("googlebook"))) { Data::FieldPtr field(new Data::Field(QStringLiteral("googlebook"), i18n("Google Book Link"), Data::Field::URL)); field->setCategory(i18n("General")); coll->addField(field); } QJsonDocument doc = QJsonDocument::fromJson(data); QVariantMap result = doc.object().toVariantMap(); m_total = result.value(QStringLiteral("totalItems")).toInt(); // myDebug() << "total:" << m_total; QVariantList resultList = result.value(QStringLiteral("items")).toList(); if(resultList.isEmpty()) { myDebug() << "no results"; endJob(job); return; } foreach(const QVariant& result, resultList) { // myDebug() << "found result:" << result; Data::EntryPtr entry(new Data::Entry(coll)); populateEntry(entry, result.toMap()); FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } m_start = m_entries.count(); m_hasMoreResults = request().key != ISBN && m_start <= m_total; endJob(job); } void GoogleBookFetcher::populateEntry(Data::EntryPtr entry, const QVariantMap& resultMap) { if(entry->collection()->hasField(QStringLiteral("gbs-link"))) { entry->setField(QStringLiteral("gbs-link"), mapValue(resultMap, "selfLink")); } const QVariantMap volumeMap = resultMap.value(QStringLiteral("volumeInfo")).toMap(); entry->setField(QStringLiteral("title"), mapValue(volumeMap, "title")); entry->setField(QStringLiteral("subtitle"), mapValue(volumeMap, "subtitle")); entry->setField(QStringLiteral("pub_year"), mapValue(volumeMap, "publishedDate").left(4)); entry->setField(QStringLiteral("author"), mapValue(volumeMap, "authors")); // workaround for bug, where publisher can be enclosed in quotes QString pub = mapValue(volumeMap, "publisher"); if(pub.startsWith(QLatin1Char('"')) && pub.endsWith(QLatin1Char('"'))) { pub.chop(1); pub = pub.remove(0, 1); } entry->setField(QStringLiteral("publisher"), pub); entry->setField(QStringLiteral("pages"), mapValue(volumeMap, "pageCount")); entry->setField(QStringLiteral("language"), mapValue(volumeMap, "language")); entry->setField(QStringLiteral("comments"), mapValue(volumeMap, "description")); QStringList catList = volumeMap.value(QStringLiteral("categories")).toStringList(); // google is going to give us a lot of categories QSet cats; foreach(const QString& cat, catList) { cats += cat.split(QRegExp(QLatin1String("\\s*/\\s*"))).toSet(); } // remove General cats.remove(QStringLiteral("General")); catList = cats.toList(); catList.sort(); entry->setField(QStringLiteral("keyword"), catList.join(FieldFormat::delimiterString())); QString isbn; foreach(const QVariant& idVariant, volumeMap.value(QLatin1String("industryIdentifiers")).toList()) { const QVariantMap idMap = idVariant.toMap(); if(mapValue(idMap, "type") == QLatin1String("ISBN_10")) { isbn = mapValue(idMap, "identifier"); break; } else if(mapValue(idMap, "type") == QLatin1String("ISBN_13")) { isbn = mapValue(idMap, "identifier"); // allow isbn10 to override, so don't break here } } if(!isbn.isEmpty()) { ISBNValidator val(this); val.fixup(isbn); entry->setField(QStringLiteral("isbn"), isbn); } const QVariantMap imageMap = volumeMap.value(QStringLiteral("imageLinks")).toMap(); if(imageMap.contains(QStringLiteral("small"))) { entry->setField(QStringLiteral("cover"), mapValue(imageMap, "small")); } else if(imageMap.contains(QStringLiteral("thumbnail"))) { entry->setField(QStringLiteral("cover"), mapValue(imageMap, "thumbnail")); } else if(imageMap.contains(QStringLiteral("smallThumbnail"))) { entry->setField(QStringLiteral("cover"), mapValue(imageMap, "smallThumbnail")); } if(optionalFields().contains(QStringLiteral("googlebook"))) { entry->setField(QStringLiteral("googlebook"), mapValue(volumeMap, "infoLink")); } } Tellico::Fetch::ConfigWidget* GoogleBookFetcher::configWidget(QWidget* parent_) const { return new GoogleBookFetcher::ConfigWidget(parent_, this); } QString GoogleBookFetcher::defaultName() { return i18n("Google Book Search"); } QString GoogleBookFetcher::defaultIcon() { return favIcon("http://books.google.com"); } Tellico::StringHash GoogleBookFetcher::allOptionalFields() { StringHash hash; hash[QStringLiteral("googlebook")] = i18n("Google Book Link"); return hash; } GoogleBookFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GoogleBookFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QGridLayout* l = new QGridLayout(optionsWidget()); l->setSpacing(4); l->setColumnStretch(1, 10); int row = -1; QLabel* al = new QLabel(i18n("Registration is required for accessing the %1 data source. " "If you agree to the terms and conditions, sign " "up for an account, and enter your information below.", preferredName(), QLatin1String("https://code.google.com/apis/console")), optionsWidget()); al->setOpenExternalLinks(true); al->setWordWrap(true); ++row; l->addWidget(al, row, 0, 1, 2); // richtext gets weird with size al->setMinimumWidth(al->sizeHint().width()); QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); l->addWidget(label, ++row, 0); m_apiKeyEdit = new QLineEdit(optionsWidget()); - connect(m_apiKeyEdit, SIGNAL(textChanged(QString)), SLOT(slotSetModified())); + connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); l->addWidget(m_apiKeyEdit, row, 1); QString w = i18n("The default Tellico key may be used, but searching may fail due to reaching access limits."); label->setWhatsThis(w); m_apiKeyEdit->setWhatsThis(w); label->setBuddy(m_apiKeyEdit); l->setRowStretch(++row, 10); // now add additional fields widget addFieldsWidget(GoogleBookFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); if(fetcher_ && fetcher_->m_apiKey != QLatin1String(GOOGLEBOOK_API_KEY)) { // only show the key if it is not the default Tellico one... // that way the user is prompted to apply for their own m_apiKeyEdit->setText(fetcher_->m_apiKey); } } void GoogleBookFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { QString apiKey = m_apiKeyEdit->text().trimmed(); if(!apiKey.isEmpty()) { config_.writeEntry("API Key", apiKey); } } QString GoogleBookFetcher::ConfigWidget::preferredName() const { return GoogleBookFetcher::defaultName(); } diff --git a/src/fetch/googlescholarfetcher.cpp b/src/fetch/googlescholarfetcher.cpp index 55593ed3..b2a05ae0 100644 --- a/src/fetch/googlescholarfetcher.cpp +++ b/src/fetch/googlescholarfetcher.cpp @@ -1,286 +1,286 @@ /*************************************************************************** Copyright (C) 2008-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "googlescholarfetcher.h" #include "../core/filehandler.h" #include "../translators/bibteximporter.h" #include "../collections/bibtexcollection.h" #include "../entry.h" #include "../utils/guiproxy.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include namespace { static const int GOOGLE_MAX_RETURNS_TOTAL = 20; static const char* SCHOLAR_BASE_URL = "http://scholar.google.com/scholar"; static const char* SCHOLAR_SET_CONFIG_URL = "http://scholar.google.com/scholar_settings?hl=en&as_sdt=0,5"; static const char* SCHOLAR_SET_BIBTEX_URL = "http://scholar.google.com/scholar_setprefs?hl=en&num=100&scis=yes&scisf=4&submit="; } using namespace Tellico; using Tellico::Fetch::GoogleScholarFetcher; GoogleScholarFetcher::GoogleScholarFetcher(QObject* parent_) : Fetcher(parent_), m_limit(GOOGLE_MAX_RETURNS_TOTAL), m_start(0), m_total(0), m_job(nullptr), m_started(false), m_cookieIsSet(false) { m_bibtexRx = QRegExp(QLatin1String("]*scholar\\.bib[^>]*)\"")); m_bibtexRx.setMinimal(true); } GoogleScholarFetcher::~GoogleScholarFetcher() { } QString GoogleScholarFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool GoogleScholarFetcher::canFetch(int type) const { return type == Data::Collection::Bibtex; } void GoogleScholarFetcher::readConfigHook(const KConfigGroup& config_) { Q_UNUSED(config_); } void GoogleScholarFetcher::search() { if(!m_cookieIsSet) { setBibtexCookie(); } m_started = true; m_start = 0; m_total = -1; doSearch(); } void GoogleScholarFetcher::continueSearch() { m_started = true; doSearch(); } void GoogleScholarFetcher::doSearch() { // myDebug() << "value = " << value_; QUrl u(QString::fromLatin1(SCHOLAR_BASE_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("start"), QString::number(m_start)); QString value = request().value; if(!value.startsWith(QLatin1Char('"'))) { value = QLatin1Char('"') + value; } if(!value.endsWith(QLatin1Char('"'))) { value += QLatin1Char('"'); } switch(request().key) { case Title: q.addQueryItem(QStringLiteral("q"), QStringLiteral("allintitle:%1").arg(request().value)); break; case Keyword: q.addQueryItem(QStringLiteral("q"), request().value); break; case Person: q.addQueryItem(QStringLiteral("q"), QStringLiteral("author:%1").arg(request().value)); break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } u.setQuery(q); // myDebug() << "url: " << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), - SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, + this, &GoogleScholarFetcher::slotComplete); } void GoogleScholarFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void GoogleScholarFetcher::slotComplete(KJob*) { // myDebug(); if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; const QString text = QString::fromUtf8(data.constData(), data.size()); #if 0 myWarning() << "Remove debug from googlescholarfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << text; } f.close(); #endif QString bibtex; int count = 0; for(int pos = m_bibtexRx.indexIn(text); count < m_limit && pos > -1; pos = m_bibtexRx.indexIn(text, pos+m_bibtexRx.matchedLength()), ++count) { // for some reason, KIO and google don't return bibtex when '&' is escaped QString url = m_bibtexRx.cap(1).replace(QLatin1String("&"), QLatin1String("&")); QUrl bibtexUrl = QUrl(QString::fromLatin1(SCHOLAR_BASE_URL)).resolved(QUrl(url)); // myDebug() << bibtexUrl; bibtex += FileHandler::readTextFile(bibtexUrl, true); } Import::BibtexImporter imp(bibtex); // quiet warnings... imp.setCurrentCollection(Data::CollPtr(new Data::BibtexCollection(true))); Data::CollPtr coll = imp.collection(); if(!coll) { myDebug() << "no collection pointer"; stop(); return; } count = 0; Data::EntryList entries = coll->entries(); foreach(Data::EntryPtr entry, entries) { if(count >= m_limit) { break; } if(!m_started) { // might get aborted break; } FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, Data::EntryPtr(entry)); emit signalResultFound(r); ++count; } m_start = m_entries.count(); // m_hasMoreResults = m_start <= m_total; m_hasMoreResults = false; // for now, no continued searches stop(); // required } Tellico::Data::EntryPtr GoogleScholarFetcher::fetchEntryHook(uint uid_) { return m_entries[uid_]; } Tellico::Fetch::FetchRequest GoogleScholarFetcher::updateRequest(Data::EntryPtr entry_) { QString title = entry_->field(QStringLiteral("title")); if(!title.isEmpty()) { return FetchRequest(Title, title); } return FetchRequest(); } Tellico::Fetch::ConfigWidget* GoogleScholarFetcher::configWidget(QWidget* parent_) const { return new GoogleScholarFetcher::ConfigWidget(parent_, this); } QString GoogleScholarFetcher::defaultName() { // no i18n return QStringLiteral("Google Scholar"); } QString GoogleScholarFetcher::defaultIcon() { return favIcon("http://scholar.google.com"); } void GoogleScholarFetcher::setBibtexCookie() { // it appears that the series of url reads are necessary to get the correct cookie set // have to set preferences to have bibtex output const QString text = FileHandler::readTextFile(QUrl(QString::fromLatin1(SCHOLAR_SET_CONFIG_URL)), true); // find hidden input variables QRegExp inputRx(QLatin1String("]*\\s*type\\s*=\\s*\"hidden\"\\s+[^>]+>")); inputRx.setMinimal(true); QRegExp pairRx(QLatin1String("([^=\\s<]+)\\s*=\\s*\"?([^=\\s\">]+)\"?")); QHash nameValues; for(int pos = inputRx.indexIn(text); pos > -1; pos = inputRx.indexIn(text, pos+inputRx.matchedLength())) { const QString input = inputRx.cap(0); QString name, value; for(int pos2 = pairRx.indexIn(input); pos2 > -1; pos2 = pairRx.indexIn(input, pos2+pairRx.matchedLength())) { if(pairRx.cap(1).toLower() == QLatin1String("name")) { name = pairRx.cap(2); } else if(pairRx.cap(1).toLower() == QLatin1String("value")) { value = pairRx.cap(2); } } if(!name.isEmpty() && !value.isEmpty()) { nameValues.insert(name, value); } } QString newUrl = QLatin1String(SCHOLAR_SET_BIBTEX_URL); for(QHash::const_iterator i = nameValues.constBegin(); i != nameValues.constEnd(); ++i) { newUrl += QLatin1Char('&') + i.key() + QLatin1Char('=') + i.value(); } FileHandler::readTextFile(QUrl(newUrl), true); m_cookieIsSet = true; } GoogleScholarFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GoogleScholarFetcher* /*=0*/) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); } QString GoogleScholarFetcher::ConfigWidget::preferredName() const { return GoogleScholarFetcher::defaultName(); } diff --git a/src/fetch/hathitrustfetcher.cpp b/src/fetch/hathitrustfetcher.cpp index 38367e6f..4b180e3c 100644 --- a/src/fetch/hathitrustfetcher.cpp +++ b/src/fetch/hathitrustfetcher.cpp @@ -1,325 +1,325 @@ /*************************************************************************** Copyright (C) 2012 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "hathitrustfetcher.h" #include "../translators/xslthandler.h" #include "../translators/tellicoimporter.h" #include "../utils/isbnvalidator.h" #include "../utils/lccnvalidator.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../utils/datafileregistry.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const char* HATHITRUST_QUERY_URL = "http://catalog.hathitrust.org/api/volumes/full/json/"; } using namespace Tellico; using Tellico::Fetch::HathiTrustFetcher; HathiTrustFetcher::HathiTrustFetcher(QObject* parent_) : Fetcher(parent_), m_started(false), m_MARC21XMLHandler(nullptr), m_MODSHandler(nullptr) { } HathiTrustFetcher::~HathiTrustFetcher() { delete m_MARC21XMLHandler; m_MARC21XMLHandler = nullptr; delete m_MODSHandler; m_MODSHandler = nullptr; } QString HathiTrustFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool HathiTrustFetcher::canSearch(FetchKey k) const { return k == ISBN || k == LCCN; } bool HathiTrustFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex; } void HathiTrustFetcher::readConfigHook(const KConfigGroup&) { } void HathiTrustFetcher::search() { m_started = true; doSearch(); } void HathiTrustFetcher::doSearch() { QUrl u(QString::fromLatin1(HATHITRUST_QUERY_URL)); QStringList searchValues; // we split ISBN and LCCN values, which are the only ones we accept anyway const QStringList searchTerms = FieldFormat::splitValue(request().value); foreach(const QString& searchTerm, searchTerms) { if(request().key == ISBN) { searchValues += QStringLiteral("isbn:%1").arg(ISBNValidator::cleanValue(searchTerm)); } else { searchValues += QStringLiteral("lccn:%1").arg(LCCNValidator::formalize(searchTerm)); } } u.setPath(u.path() + searchValues.join(QLatin1String("|"))); // myDebug() << u; m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &HathiTrustFetcher::slotComplete); } void HathiTrustFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); } m_started = false; emit signalDone(this); } bool HathiTrustFetcher::initMARC21Handler() { if(m_MARC21XMLHandler) { return true; } QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("MARC21slim2MODS3.xsl")); if(xsltfile.isEmpty()) { myWarning() << "can not locate MARC21slim2MODS3.xsl."; return false; } QUrl u = QUrl::fromLocalFile(xsltfile); m_MARC21XMLHandler = new XSLTHandler(u); if(!m_MARC21XMLHandler->isValid()) { myWarning() << "error in MARC21slim2MODS3.xsl."; delete m_MARC21XMLHandler; m_MARC21XMLHandler = nullptr; return false; } return true; } bool HathiTrustFetcher::initMODSHandler() { if(m_MODSHandler) { return true; } QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("mods2tellico.xsl")); if(xsltfile.isEmpty()) { myWarning() << "can not locate mods2tellico.xsl."; return false; } QUrl u = QUrl::fromLocalFile(xsltfile); m_MODSHandler = new XSLTHandler(u); if(!m_MODSHandler->isValid()) { myWarning() << "error in mods2tellico.xsl."; delete m_MODSHandler; m_MODSHandler = nullptr; // no use in keeping the MARC handlers now delete m_MARC21XMLHandler; m_MARC21XMLHandler = nullptr; return false; } return true; } Tellico::Data::EntryPtr HathiTrustFetcher::fetchEntryHook(uint uid_) { return m_entries.value(uid_); } Tellico::Fetch::FetchRequest HathiTrustFetcher::updateRequest(Data::EntryPtr entry_) { const QString isbn = entry_->field(QStringLiteral("isbn")); if(!isbn.isEmpty()) { return FetchRequest(ISBN, isbn); } const QString lccn = entry_->field(QStringLiteral("lccn")); if(!lccn.isEmpty()) { return FetchRequest(LCCN, lccn); } return FetchRequest(); } void HathiTrustFetcher::slotComplete(KJob* job_) { KIO::StoredTransferJob* job = static_cast(job_); if(!initMARC21Handler() || !initMODSHandler()) { // debug messages are taken care of in the specific methods stop(); return; } if(job->error()) { job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } // see bug 319662. If fetcher is cancelled, job is killed // if the pointer is retained, it gets double-deleted m_job = nullptr; #if 0 myWarning() << "Remove debug from hathitrustfetcher.cpp"; QFile f(QString::fromLatin1("/tmp/test.json")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << data; } f.close(); #endif QJsonDocument doc = QJsonDocument::fromJson(data); QVariantMap resultMap = doc.object().toVariantMap(); if(resultMap.isEmpty()) { myDebug() << "no results"; stop(); return; } QVariantMap::const_iterator i = resultMap.constBegin(); for( ; i != resultMap.constEnd(); ++i) { const QVariantMap recordMap = i.value().toMap().value(QStringLiteral("records")).toMap(); if(recordMap.isEmpty()) { myDebug() << "empty result map"; continue; } // we know there's a record, so no need to check for existence of first iterator in map QVariantMap::const_iterator ri = recordMap.constBegin(); if(ri == recordMap.constEnd()) { myWarning() << "no iterator in record"; continue; } QString marcxml = ri.value().toMap().value(QStringLiteral("marc-xml")).toString(); // HathiTrust doesn't always include the XML NS in the JSON results. Assume it's always // MARC XML and check that QDomDocument dom; if(dom.setContent(marcxml, true /* namespace processing */) && dom.documentElement().namespaceURI().isEmpty()) { const QString rootName = dom.documentElement().tagName(); myDebug() << "no namespace, attempting to set on" << rootName << "element"; QRegExp rootRx(QLatin1Char('<') + rootName + QLatin1Char('>')); QString newRoot = QLatin1Char('<') + rootName + QLatin1String(" xmlns=\"http://www.loc.gov/MARC21/slim\">"); marcxml.replace(rootRx, newRoot); } const QString modsxml = m_MARC21XMLHandler->applyStylesheet(marcxml); Import::TellicoImporter imp(m_MODSHandler->applyStylesheet(modsxml)); imp.setOptions(imp.options() ^ Import::ImportProgress); // no progress needed Data::CollPtr coll = imp.collection(); if(!coll) { myWarning() << "no coll pointer"; continue; } // since the Dewey and LoC field titles have a context in their i18n call here // but not in the mods2tellico.xsl stylesheet where the field is actually created // update the field titles here QHashIterator i(allOptionalFields()); while(i.hasNext()) { i.next(); Data::FieldPtr field = coll->fieldByName(i.key()); if(field) { field->setTitle(i.value()); coll->modifyField(field); } } foreach(Data::EntryPtr entry, coll->entries()) { FetchResult* r = new FetchResult(Fetcher::Ptr(this), entry); m_entries.insert(r->uid, entry); emit signalResultFound(r); } } m_hasMoreResults = false; // for now, no continued searches stop(); } Tellico::Fetch::ConfigWidget* HathiTrustFetcher::configWidget(QWidget* parent_) const { return new HathiTrustFetcher::ConfigWidget(parent_, this); } QString HathiTrustFetcher::defaultName() { return QStringLiteral("HathiTrust"); // no translation } QString HathiTrustFetcher::defaultIcon() { return favIcon("http://www.hathitrust.org"); } Tellico::StringHash HathiTrustFetcher::allOptionalFields() { // same ones as z3950fetcher StringHash hash; hash[QStringLiteral("address")] = i18n("Address"); hash[QStringLiteral("abstract")] = i18n("Abstract"); hash[QStringLiteral("illustrator")] = i18n("Illustrator"); hash[QStringLiteral("dewey")] = i18nc("Dewey Decimal classification system", "Dewey Decimal"); hash[QStringLiteral("lcc")] = i18nc("Library of Congress classification system", "LoC Classification"); return hash; } HathiTrustFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const HathiTrustFetcher* fetcher_) : Fetch::ConfigWidget(parent_) { QVBoxLayout* l = new QVBoxLayout(optionsWidget()); l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); l->addStretch(); // now add additional fields widget addFieldsWidget(HathiTrustFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); } void HathiTrustFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) { } QString HathiTrustFetcher::ConfigWidget::preferredName() const { return HathiTrustFetcher::defaultName(); } diff --git a/src/fetch/ibsfetcher.cpp b/src/fetch/ibsfetcher.cpp index 37b7c254..332b3f76 100644 --- a/src/fetch/ibsfetcher.cpp +++ b/src/fetch/ibsfetcher.cpp @@ -1,366 +1,366 @@ /*************************************************************************** Copyright (C) 2006-2009 Robby Stephenson ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ #include "ibsfetcher.h" #include "../utils/guiproxy.h" #include "../utils/string_utils.h" #include "../collections/bookcollection.h" #include "../entry.h" #include "../fieldformat.h" #include "../core/filehandler.h" #include "../images/imagefactory.h" #include "../utils/isbnvalidator.h" #include "../tellico_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { static const char* IBS_BASE_URL = "https://www.ibs.it/search/"; } using namespace Tellico; using Tellico::Fetch::IBSFetcher; IBSFetcher::IBSFetcher(QObject* parent_) : Fetcher(parent_), m_total(0), m_started(false) { } IBSFetcher::~IBSFetcher() { } QString IBSFetcher::source() const { return m_name.isEmpty() ? defaultName() : m_name; } bool IBSFetcher::canFetch(int type) const { return type == Data::Collection::Book || type == Data::Collection::Bibtex; } // No UPC or Raw for now. bool IBSFetcher::canSearch(FetchKey k) const { return k == Title || k == Person || k == ISBN; } void IBSFetcher::readConfigHook(const KConfigGroup& config_) { Q_UNUSED(config_); } void IBSFetcher::search() { m_started = true; m_matches.clear(); QUrl u(QString::fromLatin1(IBS_BASE_URL)); QUrlQuery q; q.addQueryItem(QStringLiteral("ts"), QStringLiteral("as")); switch(request().key) { case Title: { // can't have ampersands QString s = request().value; s.remove(QLatin1Char('&')); q.addQueryItem(QStringLiteral("query"), s); } break; case ISBN: { QString s = request().value; // limit to first isbn s = s.section(QLatin1Char(';'), 0, 0); // isbn13 search doesn't work? s = ISBNValidator::isbn10(s); // dashes don't work s.remove(QLatin1Char('-')); q.addQueryItem(QStringLiteral("query"), s); } break; case Keyword: q.addQueryItem(QStringLiteral("query"), request().value); break; default: myWarning() << "key not recognized: " << request().key; stop(); return; } u.setQuery(q); // myDebug() << "url: " << u.url(); m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); - connect(m_job, SIGNAL(result(KJob*)), SLOT(slotComplete(KJob*))); + connect(m_job.data(), &KJob::result, this, &IBSFetcher::slotComplete); } void IBSFetcher::stop() { if(!m_started) { return; } if(m_job) { m_job->kill(); m_job = nullptr; } m_started = false; emit signalDone(this); } void IBSFetcher::slotComplete(KJob*) { if(m_job->error()) { m_job->uiDelegate()->showErrorMessage(); stop(); return; } QByteArray data = m_job->data(); if(data.isEmpty()) { myDebug() << "no data"; stop(); return; } QString s = Tellico::decodeHTML(data); // really specific regexp QRegExp itemRx(QLatin1String("class=\"item \">(.*)class=\"price")); itemRx.setMinimal(true); QRegExp titleRx(QLatin1String("
    \\s*(.*)
    ")); titleRx.setMinimal(true); QRegExp yearRx(QLatin1String("(.*)")); tagRx.setMinimal(true); QString url, title, year; for(int pos = itemRx.indexIn(s); m_started && pos > -1; pos = itemRx.indexIn(s, pos+itemRx.matchedLength())) { QString s = itemRx.cap(1); if(s.contains(titleRx)) { url = titleRx.cap(1); title = titleRx.cap(2).remove(tagRx).simplified(); } if(s.contains(yearRx)) { year = yearRx.cap(1).remove(tagRx).simplified(); } if(!url.isEmpty() && !title.isEmpty()) { // the url probable contains & so be careful QUrl u = m_job->url(); u = u.resolved(QUrl(url.replace(QLatin1String("&"), QLatin1String("&")))); FetchResult* r = new FetchResult(Fetcher::Ptr(this), title, year); m_matches.insert(r->uid, u); emit signalResultFound(r); } } // since the fetch is done, don't worry about holding the job pointer m_job = nullptr; stop(); } Tellico::Data::EntryPtr IBSFetcher::fetchEntryHook(uint uid_) { // if we already grabbed this one, then just pull it out of the dict Data::EntryPtr entry = m_entries[uid_]; if(entry) { return entry; } QUrl url = m_matches[uid_]; if(url.isEmpty()) { myWarning() << "no url in map"; return Data::EntryPtr(); } QString results = Tellico::decodeHTML(FileHandler::readDataFile(url, true)); if(results.isEmpty()) { myDebug() << "no text results"; return Data::EntryPtr(); } // myDebug() << url.url(); #if 0 myWarning() << "Remove debug from ibsfetcher.cpp"; QFile f(QLatin1String("/tmp/test.html")); if(f.open(QIODevice::WriteOnly)) { QTextStream t(&f); t.setCodec("UTF-8"); t << results; } f.close(); #endif entry = parseEntry(results); if(!entry) { myDebug() << "error in processing entry"; return Data::EntryPtr(); } m_entries.insert(uid_, entry); // keep for later return entry; } Tellico::Data::EntryPtr IBSFetcher::parseEntry(const QString& str_) { QRegExp jsonRx(QLatin1String("